sensotwin.defect_placement.ui
1import ipywidgets as widgets 2from . import configuration 3from . import style 4from .. import global_style 5from .. import store_connection 6import functools 7import pyvista as pv 8import numpy as np 9import matplotlib.pyplot as plt 10import matplotlib.colors as pltcolor 11import copy 12from .. import composite_layup 13from traitlets.utils.bunch import Bunch 14import pandas as pd 15 16 17class DefectPlacementUIElements: 18 """Contains all UI functionality of material curing process configuration UI (Step 2). 19 20 In order to use, show the top level widget 'self.dashboard' in one cell of a notebook 21 and the apply css styling calling 'self.apply_styling()' an another cell 22 """ 23 MESH_FILE_PATH = 'Inputs/input_mesh_17052024.vtu' 24 SHELL_DATA_KEY = 'WebShellAssignment' 25 SECTION_DATA_KEY = 'SectionAssignment' 26 MESH_DATA = {} 27 28 LAYER_FILE_PATH = "Inputs/composite_layup_db.vtt" 29 LAYER_ID_KEY = 'layer_id' 30 LAYER_MATERIAL_KEY = 'layer_material' 31 LAYER_THICKNESS_KEY = 'layer_thickness_mm' 32 LAYER_ANGLE_KEY = 'layer_angle_deg' 33 34 def __init__(self): 35 """Initialize UI objects and associated UI functionality.""" 36 self.init_global_data() 37 self.dashboard = widgets.Tab().add_class("global_tab_container") 38 self.dashboard.children = [ 39 self.init_data_source_box(), 40 self.init_placement_box() 41 ] 42 tab_titles = ['Input', 'Simulation'] 43 for i in range(len(tab_titles)): 44 self.dashboard.set_title(i, tab_titles[i]) 45 # manually trigger change event to properly render intial defect configuration display 46 self.on_defect_type_changed() 47 48 def init_global_data(self): 49 """Set necessary global parameters for UI and load static data like 3D mesh and 3D mesh layer data.""" 50 self.available_defects = {} 51 for defect in configuration.DefectInputDataSet().get_available_defect_types(): 52 self.available_defects[defect.type_id] = defect 53 self.new_defect_to_place = self.available_defects[min(self.available_defects.keys())] 54 self.MULTIPLE_DEFECTS_COLOR_ID = len(self.available_defects) + 1 55 self.SELECTION_COLOR_ID = self.MULTIPLE_DEFECTS_COLOR_ID + 1 56 self.COLOR_DEFINITIONS = self.create_scalar_color_definitions() 57 self.selected_cells = { 58 "center": None, 59 "all": None, 60 "grow_counter" : 0 61 } 62 self.active_scalar_display = 'state' 63 self.layer_data = configuration.CellLayersDataSet(self.LAYER_FILE_PATH) 64 self.MESH_DATA = self.create_display_mesh_data() 65 66 def init_data_source_box(self) -> widgets.Box: 67 """Initialize first tab of dashboard, choosing data source.""" 68 local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline") 69 use_local_data_button = widgets.Button( 70 description='Use local database', 71 disabled=False, 72 tooltip='Use local database', 73 icon='play', 74 ).add_class("global_load_data_button").add_class("global_basic_button") 75 use_local_data_button.on_click(self.load_local_data) 76 77 remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline") 78 self.remote_database_input = widgets.Text( 79 value=None, 80 placeholder="insert SPARQL url here", 81 description="SPARQL Endpoint:", 82 disabled=False 83 ).add_class("global_url_input").add_class("global_basic_input") 84 use_remote_data_button = widgets.Button( 85 description='Use remote database', 86 disabled=False, 87 tooltip='Use remote database', 88 icon='play', 89 ).add_class("global_load_data_button").add_class("global_basic_button") 90 use_remote_data_button.on_click(self.load_remote_data) 91 local_data_box = widgets.VBox([ 92 local_database_label, 93 use_local_data_button 94 ]) 95 96 remote_data_box = widgets.VBox([ 97 remote_database_label, 98 self.remote_database_input, 99 use_remote_data_button 100 ]) 101 data_source_box = widgets.VBox([ 102 local_data_box, 103 remote_data_box 104 ]).add_class("global_data_tab_container") 105 return data_source_box 106 107 def init_pyvista_render(self) -> widgets.Output: 108 """Initialize Jupyter Output widget for rendering 3D mesh via PyVista.""" 109 OUTPUT_RENDER_HEIGHT = 500 110 OUTPUT_RENDER_WIDTH= 1000 111 self.plotter = pv.Plotter() 112 for id, data in self.MESH_DATA.items(): 113 data["mesh"] = self.plotter.add_mesh(data["input"], show_edges=True, show_scalar_bar=False, clim=data["value_ranges"]['state'], cmap=self.COLOR_DEFINITIONS['state'], scalars="color") 114 self.plotter.camera_position = [(-30.765771120379302, -28.608772602676154, 39.46235706090557), 115 (0.9572034500000001, 0.0005481500000000805, -30.4), 116 (-0.16976192468426815, -0.8825527410211623, -0.43849919982085056)] 117 self.plotter.window_size = [OUTPUT_RENDER_WIDTH, OUTPUT_RENDER_HEIGHT] 118 self.plotter.enable_element_picking(callback=self.cell_selected, show_message=False) 119 legend_labels = [(defect.display_name, defect.display_color) for _, defect in self.available_defects.items()] 120 legend_labels.append(["Multiple Defects", (1.0, 0.0, 0.0)]) 121 self.plotter.add_legend(labels=legend_labels, bcolor=None, size=(0.2,0.2), loc="upper right", face=None) 122 render_widget = widgets.Output(layout={'height': '{}px'.format(OUTPUT_RENDER_HEIGHT+15), 123 'width': '{}px'.format(OUTPUT_RENDER_WIDTH+10)}) 124 with render_widget: 125 self.plotter.show(jupyter_backend='trame') 126 return render_widget 127 128 def init_placement_box(self) -> widgets.Box: 129 """Initialize second tab of dashboard, configuring construction defects.""" 130 self.selected_id_widget = widgets.IntText( 131 value=None, 132 description="Selected Cell", 133 disabled=True 134 ).add_class("global_basic_input").add_class("defect_place_layer_input") 135 self.layer_dropdown_widget = widgets.Dropdown( 136 options=['Select cell first'], 137 value='Select cell first', 138 description='Show Layer', 139 disabled=False, 140 ).add_class("global_basic_input").add_class("defect_place_layer_input") 141 self.cell_layer_widget = widgets.FloatText( 142 placeholder='select cell', 143 description="Layer ID", 144 disabled=True 145 ).add_class("global_basic_input").add_class("defect_place_layer_input") 146 self.layer_material_widget = widgets.Text( 147 placeholder='select cell', 148 description="Material", 149 disabled=True 150 ).add_class("global_basic_input").add_class("defect_place_layer_input") 151 self.layer_thickness_widget = widgets.FloatText( 152 placeholder='select cell', 153 description="Thickness (mm)", 154 disabled=True 155 ).add_class("global_basic_input").add_class("defect_place_layer_input") 156 self.layer_angle_widget = widgets.FloatText( 157 placeholder='select cell', 158 description="Angle (°)", 159 disabled=True 160 ).add_class("global_basic_input").add_class("defect_place_layer_input") 161 162 rendering_toggle_buttons = [] 163 show_all_button = widgets.Button( 164 description='Show all', 165 disabled=False 166 ).add_class("global_basic_button") 167 show_all_button.on_click(self.show_mesh) 168 rendering_toggle_buttons.append(show_all_button) 169 for id, data in self.MESH_DATA.items(): 170 current_button = widgets.Button( 171 description='Show ' + data["name"], 172 disabled=False 173 ).add_class("global_basic_button") 174 current_button.on_click(functools.partial(self.show_mesh, selected_meshes=tuple([id]))) 175 rendering_toggle_buttons.append(current_button) 176 toggle_button_box = widgets.HBox([x for x in rendering_toggle_buttons]) 177 show_defects_button = widgets.Button( 178 description='Show Defects', 179 disabled=False 180 ).add_class("global_basic_button") 181 show_defects_button.on_click(functools.partial(self.change_scalar_display_mode, scalar_name="state")) 182 show_sections_button = widgets.Button( 183 description='Show Sections', 184 disabled=False 185 ).add_class("global_basic_button") 186 show_sections_button.on_click(functools.partial(self.change_scalar_display_mode, scalar_name="section")) 187 scalar_button_box = widgets.HBox([show_defects_button, show_sections_button]) 188 189 place_defect_button = widgets.Button( 190 description='Place Defect', 191 disabled=False 192 ).add_class("global_basic_button") 193 place_defect_button.on_click(self.place_defect) 194 remove_defects_button = widgets.Button( 195 description='Remove Defects', 196 disabled=False 197 ).add_class("global_basic_button") 198 remove_defects_button.on_click(self.remove_defects) 199 grow_selection_button = widgets.Button( 200 description='Grow Selection', 201 disabled=False 202 ).add_class("global_basic_button") 203 grow_selection_button.on_click(self.grow_selection) 204 shrink_selection_button = widgets.Button( 205 description='Shrink Selection', 206 disabled=False 207 ).add_class("global_basic_button") 208 shrink_selection_button.on_click(self.shrink_selection) 209 self.defect_id_widget = widgets.Dropdown( 210 options=[(defect.display_name, defect_id) for defect_id, defect in self.available_defects.items()], 211 value=min(self.available_defects.keys()), 212 description='Defect:', 213 disabled=False, 214 ).add_class("global_basic_input") 215 self.defect_id_widget.observe(self.on_defect_type_changed, names=['value']) 216 self.defect_param_output = widgets.Output().add_class("defect_param_output_widget") 217 218 self.fig = self.init_cell_layer_rendering() 219 select_displayed_layer_headline = widgets.Label(value="Display Options:").add_class("global_headline") 220 selected_cell_headline = widgets.Label(value="Cell Information:").add_class("global_headline") 221 cell_select_info_box = widgets.VBox([ 222 select_displayed_layer_headline, 223 toggle_button_box, 224 scalar_button_box, 225 selected_cell_headline, 226 self.selected_id_widget, 227 self.layer_dropdown_widget, 228 self.cell_layer_widget, 229 self.layer_material_widget, 230 self.layer_thickness_widget, 231 self.layer_angle_widget 232 ]) 233 defect_configuration_box = widgets.VBox([ 234 widgets.HBox([ 235 place_defect_button, 236 remove_defects_button 237 ]), 238 self.defect_id_widget, 239 self.defect_param_output 240 ]) 241 defect_definition_box = widgets.VBox([ 242 widgets.Label(value="Cell selection:").add_class("global_headline"), 243 widgets.HBox([ 244 grow_selection_button, 245 shrink_selection_button 246 ]), 247 widgets.Label(value="Defect configuration:").add_class("global_headline"), 248 defect_configuration_box 249 ]) 250 layup_headline = widgets.Label(value="Cell Material Layup:").add_class("global_headline").add_class("defect_place_layup_headline") 251 layer_box = widgets.VBox([ 252 layup_headline, 253 self.fig.canvas 254 ]).add_class("defect_place_layer_render_container") 255 256 input_set_selection_label = widgets.Label(value="Select Input Set:").add_class("global_headline") 257 self.input_set_selection = widgets.Select( 258 options=['No data loaded'], 259 value='No data loaded', 260 rows=10, 261 description='Input sets:', 262 disabled=False 263 ).add_class("global_input_set_selection").add_class("global_basic_input") 264 display_input_set_button = widgets.Button( 265 description='Load Input Set', 266 disabled=False, 267 tooltip='Load Input Set', 268 icon='play', 269 ).add_class("global_basic_button") 270 display_input_set_button.on_click(self.display_input_set) 271 input_set_box = widgets.VBox([ 272 input_set_selection_label, 273 self.input_set_selection, 274 display_input_set_button 275 ]) 276 placed_defects_list_label = widgets.Label(value="Defects in cell:").add_class("global_headline") 277 self.defect_in_cell_selection = widgets.Select( 278 options=['Select cell first'], 279 value='Select cell first', 280 rows=10, 281 description='', 282 disabled=False 283 ).add_class("global_input_set_selection").add_class("global_basic_input") 284 defects_in_cell_box = widgets.VBox([ 285 placed_defects_list_label, 286 self.defect_in_cell_selection 287 ]) 288 save_input_set_button = widgets.Button( 289 description='Save configured defects as new input set', 290 disabled=False, 291 tooltip='Save configured defects as new input set', 292 icon='save', 293 ).add_class("global_save_input_set_button").add_class("global_basic_button") 294 save_input_set_button.on_click(self.save_new_input_set) 295 296 simulation_display_box = widgets.HBox([ 297 self.init_pyvista_render(), 298 layer_box 299 ]).add_class("defect_place_simulation_row_container") 300 simulation_input_box = widgets.HBox([ 301 input_set_box, 302 cell_select_info_box, 303 defects_in_cell_box, 304 defect_definition_box 305 ]).add_class("defect_place_simulation_row_container") 306 simulation_box = widgets.VBox([ 307 simulation_display_box, 308 simulation_input_box, 309 save_input_set_button 310 ]) 311 return simulation_box 312 313 def cell_selected(self, cell: pv.UnstructuredGrid): 314 """Update material layup render and layer display with new information. 315 316 Args: 317 cell: Single PyVista cell returned by callback 318 """ 319 self.reset_multi_select() 320 self.selected_cells["center"] = cell 321 cell_id = cell['original_id'][0] 322 available_layers = self.layer_data.get_available_layers_for_cell(cell_id) 323 self.layer_dropdown_widget.options = [("Layer {}: {}".format(x[self.LAYER_ID_KEY], x[self.LAYER_MATERIAL_KEY]), x[self.LAYER_ID_KEY]) 324 for i, x in available_layers.iterrows()] 325 326 self.update_defects_in_cell_display(cell_id) 327 self.update_layers_in_cell_display(cell_id) 328 self.render_cell_layer_structure(available_layers) 329 330 def update_defects_in_cell_display(self, cell_id: int): 331 """Update display of already placed faults in currently selected cell.""" 332 defects_in_cell = self.placed_defects.get_defects_of_cell(cell_id) 333 if defects_in_cell: 334 options = [] 335 for layer_id, defects in defects_in_cell.items(): 336 for defect_id, defect in defects.items(): 337 options.append("Layer {}: '{}'".format(layer_id, defect.generate_display_label())) 338 self.defect_in_cell_selection.options = options 339 else: 340 self.defect_in_cell_selection.options = ['No defects in selected cell'] 341 342 def update_layers_in_cell_display(self, cell_id: int): 343 """Update display of available layers in currently selected cell.""" 344 first_layer = self.layer_data.get_available_layers_for_cell(cell_id).iloc[0] 345 self.layer_dropdown_widget.value = first_layer[self.LAYER_ID_KEY] 346 self.selected_id_widget.value = cell_id 347 self.cell_layer_widget.value = first_layer[self.LAYER_ID_KEY] 348 self.layer_material_widget.value = first_layer[self.LAYER_MATERIAL_KEY] 349 self.layer_thickness_widget.value = first_layer[self.LAYER_THICKNESS_KEY] 350 self.layer_angle_widget.value = first_layer[self.LAYER_ANGLE_KEY] 351 self.layer_dropdown_widget.observe(self.layer_selected, names="value") 352 353 def show_mesh(self, _, selected_meshes: list=[]): 354 """Update visibility of all meshes in PyVista view. 355 356 Args: 357 selected_meshes: ids of meshes to display in case of partial render 358 """ 359 if len(selected_meshes) == 0: 360 selected_meshes = self.MESH_DATA.keys() 361 for id, data in self.MESH_DATA.items(): 362 current_visibility = data["mesh"].GetVisibility() 363 if current_visibility == False and id in selected_meshes: 364 data["mesh"].SetVisibility(True) 365 elif current_visibility == True and id not in selected_meshes: 366 data["mesh"].SetVisibility(False) 367 self.plotter.update() 368 369 def create_display_mesh_data(self) -> dict: 370 """Read 3D mesh data from file and analyse for display in UI. 371 372 Returns: 373 dictionary of mesh data divided into different shells according to SHELL_DATA_KEY 374 """ 375 initial_mesh = pv.read(self.MESH_FILE_PATH).cast_to_unstructured_grid() 376 available_shells = np.unique(initial_mesh[self.SHELL_DATA_KEY]) 377 # IDs in layer file start at 1 378 initial_mesh['original_id'] = [x+1 for x in range(len(initial_mesh[self.SECTION_DATA_KEY]))] 379 display_mesh_data = {} 380 for shell in available_shells: 381 cell_to_remove = np.argwhere(initial_mesh[self.SHELL_DATA_KEY] != shell) 382 extracted_shell = initial_mesh.remove_cells(cell_to_remove) 383 extracted_scalars = extracted_shell[self.SECTION_DATA_KEY] 384 extracted_shell['subshell_id'] = [x for x in range(len(extracted_scalars))] 385 extracted_shell['color'] = [0 for x in range(len(extracted_scalars))] 386 extracted_shell['section'] = extracted_shell[self.SECTION_DATA_KEY] 387 if shell == 0: 388 display_name = "Outer Shell" 389 elif shell == 1: 390 display_name = "Webs" 391 else: 392 display_name = "Layer '{}'".format(shell) 393 display_mesh_data[shell] = { 394 "input": extracted_shell, 395 "name": display_name, 396 "mesh": None, 397 "scalar_arrays": { 398 "section": extracted_scalars, 399 "state": [0 for x in range(len(extracted_scalars))], 400 "selection": [0 for x in range(len(extracted_scalars))] 401 }, 402 "value_ranges": { 403 "section": [0, 68], 404 "state": [0, self.SELECTION_COLOR_ID], 405 "selection": [0, self.SELECTION_COLOR_ID] 406 } 407 } 408 return display_mesh_data 409 410 def layer_selected(self, change: Bunch): 411 """Change displayed layer information on layer dropdown change.""" 412 selected_layer_data = self.layer_data.get_layer_of_cell(self.selected_cells["center"]['original_id'][0], change['new']) 413 self.cell_layer_widget.value = selected_layer_data[self.LAYER_ID_KEY] 414 self.layer_material_widget.value = selected_layer_data[self.LAYER_MATERIAL_KEY] 415 self.layer_thickness_widget.value = selected_layer_data[self.LAYER_THICKNESS_KEY] 416 self.layer_angle_widget.value = selected_layer_data[self.LAYER_ANGLE_KEY] 417 418 def init_cell_layer_rendering(self) -> plt.Figure: 419 """Initialize 3D matplot for rendering material layup and fill with placeholder.""" 420 with plt.ioff(): 421 fig = plt.figure() 422 fig.set_figwidth(4) 423 self.ax = fig.add_subplot(111, projection='3d', computed_zorder=False) 424 fig.set_facecolor((0.0, 0.0, 0.0, 0.0)) 425 self.ax.set_facecolor((0.0, 0.0, 0.0, 0.0)) 426 self.ax.set_axis_off() 427 self.ax.set_title("") 428 fig.canvas.toolbar_visible = False 429 fig.canvas.header_visible = False 430 fig.canvas.footer_visible = False 431 fig.canvas.resizable = False 432 self.ax.bar3d(1, 1, 1, 1, 1, 1, label="Select cell first",color="white", shade=True) 433 handles, labels = self.ax.get_legend_handles_labels() 434 self.ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.05), prop={'size': 12}) 435 self.ax.disable_mouse_rotation() 436 return fig 437 438 def render_cell_layer_structure(self, layers: pd.DataFrame): 439 """Update rendered material layupt 3D matplot. 440 441 Args: 442 layers: Sorted layers to display with structure according to CellLayersDataSet 443 """ 444 self.ax.clear() 445 self.ax.set_axis_off() 446 self.ax.set_title("Cell {}".format(self.selected_id_widget.value), y=-0.01) 447 total_width = 0 448 total_height = 0 449 legend_colors = [] 450 graph_colors = [] 451 current_label = None 452 previous_label = None 453 graph_labels = [] 454 layer_counter = 1 455 layer_counts = [] 456 layer_angles = [] 457 group_width = 0.5 458 alignments = [[]] 459 460 # count layer and material composition 461 for i, (_, layer) in enumerate(layers.iterrows()): 462 color = configuration.get_material_color(layer[self.LAYER_MATERIAL_KEY]) 463 current_label = layer[self.LAYER_MATERIAL_KEY] 464 if current_label != previous_label: 465 graph_labels.append(current_label) 466 legend_colors.append(color) 467 if i > 0: 468 layer_counts.append(layer_counter) 469 layer_counter = 1 470 else: 471 graph_labels.append("_") 472 layer_counter += 1 473 graph_colors.append(color) 474 previous_label = layer[self.LAYER_MATERIAL_KEY] 475 total_height += layer[self.LAYER_THICKNESS_KEY] 476 layer_counts.append(layer_counter) 477 for layer in layer_counts: 478 total_width += group_width 479 480 # add data to graph 481 current_height = total_height + layers.iloc[0][self.LAYER_THICKNESS_KEY] 482 current_width = 0 483 current_layer_index = 0 484 for i, (_, layer) in enumerate(layers.iterrows()): 485 current_label = layer[self.LAYER_MATERIAL_KEY] 486 if current_label != previous_label and i > 0: 487 current_layer_index += 1 488 alignments.append([]) 489 current_height -= layer[self.LAYER_THICKNESS_KEY] 490 if layer_counts[current_layer_index] == 1: 491 width_delta = group_width * 0.66 492 else: 493 width_delta = group_width / layer_counts[current_layer_index] 494 current_width += width_delta 495 p = self.ax.bar3d(1, 1, current_height, current_width, 3, layer[self.LAYER_THICKNESS_KEY], 496 label=graph_labels[i],color=graph_colors[i],zorder=current_height, shade=True) 497 previous_label = layer[self.LAYER_MATERIAL_KEY] 498 alignments[current_layer_index].append(layer[self.LAYER_ANGLE_KEY]) 499 500 # adjust text and colors of legend 501 handles, labels = self.ax.get_legend_handles_labels() 502 for i, layer_count in enumerate(layer_counts): 503 if layer_count > 1: 504 labels[i] = "{} - {}".format(labels[i], composite_layup.generate_composite_layup_description(alignments[i])) 505 self.ax.disable_mouse_rotation() 506 self.ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.30), prop={'size': 10}) 507 for i, color in enumerate(legend_colors): 508 self.ax.get_legend().legend_handles[i].set_color(color) 509 510 plt.show() 511 # manually trigger redraw event - update cycles take several seconds to show up on frontend otherwise 512 self.fig.canvas.draw() 513 514 def create_scalar_color_definitions(self) -> dict: 515 """Create custom color maps for defect and section displays in PyVista 3D render. 516 517 Returns: 518 dict with str key and matplotlib Colormap as value 519 """ 520 color_maps = {} 521 color_maps["section"] = "prism" 522 523 default = np.array([189 / 256, 189 / 256, 189 / 256, 1.0]) 524 multiple_defects = np.array([1.0, 0.0, 0.0, 1.0]) 525 selection = np.array([255 / 256, 3 / 256, 213 / 256, 1.0]) 526 intmapping = np.linspace(0, self.SELECTION_COLOR_ID, 256) 527 intcolors = np.empty((256, 4)) 528 intcolors[intmapping <= (self.SELECTION_COLOR_ID + 0.1)] = selection 529 intcolors[intmapping <= (self.MULTIPLE_DEFECTS_COLOR_ID + 0.1)] = multiple_defects 530 for defect_id, defect in reversed(self.available_defects.items()): 531 defect_color = list(defect.display_color) 532 defect_color.append(1.0) 533 intcolors[intmapping <= (defect_id + 0.1)] = defect_color 534 intcolors[intmapping <= 0.1] = default 535 color_maps["state"] = pltcolor.ListedColormap(intcolors) 536 537 return color_maps 538 539 def change_scalar_display_mode(self, _, scalar_name: str=None): 540 """Change displayed scalar for colors on 3D mesh.""" 541 self.reset_multi_select() 542 for shell_id, data in self.MESH_DATA.items(): 543 scalars = data["scalar_arrays"][scalar_name] 544 value_range = data["value_ranges"][scalar_name] 545 data["input"].cell_data.set_scalars(scalars, "color") 546 data["mesh"].mapper.lookup_table.cmap = self.COLOR_DEFINITIONS[scalar_name] 547 data["mesh"].mapper.scalar_range = value_range[0], value_range[1] 548 self.active_scalar_display = scalar_name 549 self.plotter.update() 550 551 def place_defect(self, ui_element=None): 552 """Place new defect according to currently entered information in UI. 553 554 Args: 555 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 556 """ 557 cells = self.selected_cells["all"] if self.selected_cells["all"] else self.selected_cells["center"] 558 subshell_ids = cells['subshell_id'] 559 vtk_ids = cells['original_id'] 560 params = self.get_new_defect_configuration(self) 561 for i in range(len(subshell_ids)): 562 new_defect = self.new_defect_to_place(vtk_ids[i], self.layer_dropdown_widget.value, **params) 563 self.placed_defects.add_defect_to_cell(vtk_ids[i], self.layer_dropdown_widget.value, new_defect) 564 self.update_defects_in_cell_display(self.selected_cells["center"]['original_id'][0]) 565 self.update_displayed_cell_state(cells) 566 self.reset_multi_select() 567 568 def remove_defects(self, ui_element=None): 569 """Remove all defects from currently selected cell. 570 571 Args: 572 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 573 """ 574 cells = self.selected_cells["all"] if self.selected_cells["all"] else self.selected_cells["center"] 575 subshell_ids = cells['subshell_id'] 576 vtk_ids = cells['original_id'] 577 for i in range(len(subshell_ids)): 578 self.placed_defects.clear_defects_of_cell(vtk_ids[i]) 579 self.update_defects_in_cell_display(self.selected_cells["center"]['original_id'][0]) 580 self.update_displayed_cell_state(cells) 581 self.reset_multi_select() 582 583 def update_displayed_cell_state(self, cells: pv.UnstructuredGrid): 584 """Update colors of selected cells in 3D mesh according to current defect state. 585 586 Args: 587 cells to refresh defect display information for 588 """ 589 shell_id = cells[self.SHELL_DATA_KEY][0] 590 data = self.MESH_DATA[shell_id] 591 subshell_ids = cells['subshell_id'] 592 vtk_ids = cells['original_id'] 593 defect_display_data = self.placed_defects.generate_defect_display_summary() 594 for i, vtk_id in enumerate(vtk_ids): 595 if vtk_id not in defect_display_data["cell_defect_types"]: 596 display_value = 0 597 else: 598 cell_defects = defect_display_data["cell_defect_types"][vtk_id] 599 if len(cell_defects) > 1: 600 display_value = self.MULTIPLE_DEFECTS_COLOR_ID 601 else: 602 display_value = cell_defects[0] 603 data["scalar_arrays"]["state"][subshell_ids[i]] = display_value 604 605 def grow_selection(self, ui_element=None): 606 """Grow current selection of cells in all directions by 1 cell while respecting section borders. 607 608 Args: 609 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 610 """ 611 grow_counter = self.selected_cells["grow_counter"] + 1 612 self.perform_multiselect(grow_counter) 613 614 def shrink_selection(self, ui_element=None): 615 """Shrink current selection of cells in all directions by 1 cell. 616 617 Args: 618 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 619 """ 620 grow_counter = self.selected_cells["grow_counter"] - 1 621 self.perform_multiselect(grow_counter) 622 623 def perform_multiselect(self, grow_counter: int): 624 """Update selection area according to current multi select counter.""" 625 self.reset_multi_select(refresh=False) 626 self.selected_cells["grow_counter"] = grow_counter 627 cell = self.selected_cells["center"] 628 shell_id = cell['WebShellAssignment'][0] 629 search_id = cell['original_id'][0] 630 cell_subshell_id = cell['subshell_id'][0] 631 section_id = cell['section'][0] 632 mesh_data = self.MESH_DATA[shell_id]["input"] 633 mesh_data.cell_data.set_scalars(self.MESH_DATA[shell_id]["scalar_arrays"]["state"], "color") 634 635 cells_found = [cell_subshell_id] 636 for _ in range(grow_counter): 637 new_cells = [] 638 for cell_id in cells_found: 639 result = mesh_data.cell_neighbors(cell_id, "points") 640 extracted_cells = mesh_data.extract_cells(result) 641 section_ids = extracted_cells['section'] 642 subshell_ids = extracted_cells['subshell_id'] 643 for i, item in enumerate(section_ids): 644 if item == section_id: 645 new_cells.append(subshell_ids[i]) 646 cells_found.extend(new_cells) 647 cells_found = list(set(cells_found)) 648 649 grow_cells = mesh_data.extract_cells(cells_found) 650 self.selected_cells["all"] = grow_cells 651 for i in grow_cells['subshell_id']: 652 self.MESH_DATA[shell_id]["scalar_arrays"]["selection"][i] = self.SELECTION_COLOR_ID 653 mesh_data.cell_data.set_scalars(self.MESH_DATA[shell_id]["scalar_arrays"]["selection"], "color") 654 655 self.plotter.update() 656 657 def reset_multi_select(self, refresh: bool=True): 658 """Reset internal storage of currently selected cells and display state of 3D mesh back to baseline. 659 660 Args: 661 refresh: Trigger PyVista re-render or not 662 """ 663 self.selected_cells["all"] = None 664 self.selected_cells["grow_counter"] = 0 665 for key, data in self.MESH_DATA.items(): 666 data["scalar_arrays"]["selection"] = copy.deepcopy(data["scalar_arrays"]["state"]) 667 if self.active_scalar_display == 'state': 668 data["input"].cell_data.set_scalars(data["scalar_arrays"]["state"], "color") 669 if refresh: 670 self.plotter.update() 671 672 def on_defect_type_changed(self, ui_element=None): 673 """Update displayed UI elements for defect configuration after selected defect type has changed. 674 675 Args: 676 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 677 """ 678 selected_defect_type_id = self.defect_id_widget.value 679 self.new_defect_to_place = self.available_defects[selected_defect_type_id] 680 match selected_defect_type_id: 681 case 1: # PlyWaviness 682 self.defect_param_widget = widgets.FloatText( 683 value=0, 684 description="Orientation (°):" 685 ).add_class("defect_param_input").add_class("global_basic_input") 686 self.defect_length_widget = widgets.FloatText( 687 value=0, 688 description="Length (m):" 689 ).add_class("defect_param_input").add_class("global_basic_input") 690 self.defect_amplitude_widget = widgets.FloatText( 691 value=0, 692 description="Amplitude (m):" 693 ).add_class("defect_param_input").add_class("global_basic_input") 694 output = widgets.VBox([ 695 self.defect_param_widget, 696 self.defect_length_widget, 697 self.defect_amplitude_widget 698 ]) 699 def get_param(self): 700 return { 701 "orientation": self.defect_param_widget.value, 702 "length": self.defect_length_widget.value, 703 "amplitude": self.defect_amplitude_widget.value 704 } 705 self.get_new_defect_configuration = get_param 706 case 2: # PlyMisorientation 707 self.defect_param_widget = widgets.FloatText( 708 value=0, 709 description="Offset Angle (°):" 710 ).add_class("defect_param_input").add_class("global_basic_input") 711 output = self.defect_param_widget 712 def get_param(self): 713 return { 714 "angle": self.defect_param_widget.value 715 } 716 self.get_new_defect_configuration = get_param 717 case 3: # MissingPly 718 self.defect_param_widget = None 719 output = None 720 def get_param(self): 721 return {} 722 self.get_new_defect_configuration = get_param 723 case 4: # VaryingThickness 724 self.defect_param_widget = widgets.FloatText( 725 value=0, 726 description="FVC (%):" 727 ).add_class("defect_param_input").add_class("global_basic_input") 728 output = self.defect_param_widget 729 def get_param(self): 730 return { 731 "fvc": self.defect_param_widget.value 732 } 733 self.get_new_defect_configuration = get_param 734 case _: 735 print("Unknown type of defect selected with ID {}".format(self.defect_id_widget.value)) 736 self.defect_param_output.clear_output() 737 if output: 738 with self.defect_param_output: 739 display(output) 740 741 def get_new_defect_configuration(self) -> dict: 742 """Return all parameters of defect currently being defined. 743 744 This method is redefined everytime the selected defect type changes 745 746 Returns: 747 dictionary of all parameters currently entered in UI for defect configuration (can be empty) 748 """ 749 pass 750 751 def load_local_data(self, ui_element=None): 752 """Use locally stored data for UI. 753 754 Args: 755 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 756 """ 757 self.conn = store_connection.LocalStoreConnection("sensotwin_world") 758 self.placed_defects = configuration.DefectInputDataSet(conn=self.conn) 759 self.load_input_set_data() 760 761 def load_remote_data(self, ui_element=None): 762 """Use remotely stored data for UI. 763 764 Args: 765 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 766 """ 767 self.conn = store_connection.FusekiConnection(self.remote_database_input.value) 768 self.placed_defects = configuration.DefectInputDataSet(conn=self.conn) 769 self.load_input_set_data() 770 771 def save_new_input_set(self, ui_element=None): 772 new_id = max(self.input_set_data.keys()) + 1 if self.input_set_data else 1 773 774 self.placed_defects.save_entry_to_store(new_id) 775 self.load_input_set_data() 776 777 def load_input_set_data(self): 778 """Load data from previously set input source and populate UI.""" 779 self.input_set_data = configuration.DefectInputDataSet.get_all_entries_from_store(self.conn) 780 options = [] 781 if self.input_set_data: 782 for key, input_set in self.input_set_data.items(): 783 label = input_set.generate_input_set_display_label() 784 options.append((label, key)) 785 self.input_set_selection.options = options 786 self.input_set_selection.value = list(self.input_set_data.keys())[0] 787 788 def display_input_set(self, ui_element=None): 789 """Display data for currently selected input set in UI. 790 791 Args: 792 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 793 """ 794 self.placed_defects = self.input_set_data[self.input_set_selection.value] 795 for id, data in self.MESH_DATA.items(): 796 self.update_displayed_cell_state(data["input"]) 797 self.reset_multi_select() 798 799 def apply_styling(self): 800 """Apply CSS hack to notebook for better styling than native Jupyter Widget styles.""" 801 css = """ 802 <style> 803 {} 804 {} 805 </style> 806 """.format(global_style.global_css, style.local_css) 807 return widgets.HTML(css) 808 809
18class DefectPlacementUIElements: 19 """Contains all UI functionality of material curing process configuration UI (Step 2). 20 21 In order to use, show the top level widget 'self.dashboard' in one cell of a notebook 22 and the apply css styling calling 'self.apply_styling()' an another cell 23 """ 24 MESH_FILE_PATH = 'Inputs/input_mesh_17052024.vtu' 25 SHELL_DATA_KEY = 'WebShellAssignment' 26 SECTION_DATA_KEY = 'SectionAssignment' 27 MESH_DATA = {} 28 29 LAYER_FILE_PATH = "Inputs/composite_layup_db.vtt" 30 LAYER_ID_KEY = 'layer_id' 31 LAYER_MATERIAL_KEY = 'layer_material' 32 LAYER_THICKNESS_KEY = 'layer_thickness_mm' 33 LAYER_ANGLE_KEY = 'layer_angle_deg' 34 35 def __init__(self): 36 """Initialize UI objects and associated UI functionality.""" 37 self.init_global_data() 38 self.dashboard = widgets.Tab().add_class("global_tab_container") 39 self.dashboard.children = [ 40 self.init_data_source_box(), 41 self.init_placement_box() 42 ] 43 tab_titles = ['Input', 'Simulation'] 44 for i in range(len(tab_titles)): 45 self.dashboard.set_title(i, tab_titles[i]) 46 # manually trigger change event to properly render intial defect configuration display 47 self.on_defect_type_changed() 48 49 def init_global_data(self): 50 """Set necessary global parameters for UI and load static data like 3D mesh and 3D mesh layer data.""" 51 self.available_defects = {} 52 for defect in configuration.DefectInputDataSet().get_available_defect_types(): 53 self.available_defects[defect.type_id] = defect 54 self.new_defect_to_place = self.available_defects[min(self.available_defects.keys())] 55 self.MULTIPLE_DEFECTS_COLOR_ID = len(self.available_defects) + 1 56 self.SELECTION_COLOR_ID = self.MULTIPLE_DEFECTS_COLOR_ID + 1 57 self.COLOR_DEFINITIONS = self.create_scalar_color_definitions() 58 self.selected_cells = { 59 "center": None, 60 "all": None, 61 "grow_counter" : 0 62 } 63 self.active_scalar_display = 'state' 64 self.layer_data = configuration.CellLayersDataSet(self.LAYER_FILE_PATH) 65 self.MESH_DATA = self.create_display_mesh_data() 66 67 def init_data_source_box(self) -> widgets.Box: 68 """Initialize first tab of dashboard, choosing data source.""" 69 local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline") 70 use_local_data_button = widgets.Button( 71 description='Use local database', 72 disabled=False, 73 tooltip='Use local database', 74 icon='play', 75 ).add_class("global_load_data_button").add_class("global_basic_button") 76 use_local_data_button.on_click(self.load_local_data) 77 78 remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline") 79 self.remote_database_input = widgets.Text( 80 value=None, 81 placeholder="insert SPARQL url here", 82 description="SPARQL Endpoint:", 83 disabled=False 84 ).add_class("global_url_input").add_class("global_basic_input") 85 use_remote_data_button = widgets.Button( 86 description='Use remote database', 87 disabled=False, 88 tooltip='Use remote database', 89 icon='play', 90 ).add_class("global_load_data_button").add_class("global_basic_button") 91 use_remote_data_button.on_click(self.load_remote_data) 92 local_data_box = widgets.VBox([ 93 local_database_label, 94 use_local_data_button 95 ]) 96 97 remote_data_box = widgets.VBox([ 98 remote_database_label, 99 self.remote_database_input, 100 use_remote_data_button 101 ]) 102 data_source_box = widgets.VBox([ 103 local_data_box, 104 remote_data_box 105 ]).add_class("global_data_tab_container") 106 return data_source_box 107 108 def init_pyvista_render(self) -> widgets.Output: 109 """Initialize Jupyter Output widget for rendering 3D mesh via PyVista.""" 110 OUTPUT_RENDER_HEIGHT = 500 111 OUTPUT_RENDER_WIDTH= 1000 112 self.plotter = pv.Plotter() 113 for id, data in self.MESH_DATA.items(): 114 data["mesh"] = self.plotter.add_mesh(data["input"], show_edges=True, show_scalar_bar=False, clim=data["value_ranges"]['state'], cmap=self.COLOR_DEFINITIONS['state'], scalars="color") 115 self.plotter.camera_position = [(-30.765771120379302, -28.608772602676154, 39.46235706090557), 116 (0.9572034500000001, 0.0005481500000000805, -30.4), 117 (-0.16976192468426815, -0.8825527410211623, -0.43849919982085056)] 118 self.plotter.window_size = [OUTPUT_RENDER_WIDTH, OUTPUT_RENDER_HEIGHT] 119 self.plotter.enable_element_picking(callback=self.cell_selected, show_message=False) 120 legend_labels = [(defect.display_name, defect.display_color) for _, defect in self.available_defects.items()] 121 legend_labels.append(["Multiple Defects", (1.0, 0.0, 0.0)]) 122 self.plotter.add_legend(labels=legend_labels, bcolor=None, size=(0.2,0.2), loc="upper right", face=None) 123 render_widget = widgets.Output(layout={'height': '{}px'.format(OUTPUT_RENDER_HEIGHT+15), 124 'width': '{}px'.format(OUTPUT_RENDER_WIDTH+10)}) 125 with render_widget: 126 self.plotter.show(jupyter_backend='trame') 127 return render_widget 128 129 def init_placement_box(self) -> widgets.Box: 130 """Initialize second tab of dashboard, configuring construction defects.""" 131 self.selected_id_widget = widgets.IntText( 132 value=None, 133 description="Selected Cell", 134 disabled=True 135 ).add_class("global_basic_input").add_class("defect_place_layer_input") 136 self.layer_dropdown_widget = widgets.Dropdown( 137 options=['Select cell first'], 138 value='Select cell first', 139 description='Show Layer', 140 disabled=False, 141 ).add_class("global_basic_input").add_class("defect_place_layer_input") 142 self.cell_layer_widget = widgets.FloatText( 143 placeholder='select cell', 144 description="Layer ID", 145 disabled=True 146 ).add_class("global_basic_input").add_class("defect_place_layer_input") 147 self.layer_material_widget = widgets.Text( 148 placeholder='select cell', 149 description="Material", 150 disabled=True 151 ).add_class("global_basic_input").add_class("defect_place_layer_input") 152 self.layer_thickness_widget = widgets.FloatText( 153 placeholder='select cell', 154 description="Thickness (mm)", 155 disabled=True 156 ).add_class("global_basic_input").add_class("defect_place_layer_input") 157 self.layer_angle_widget = widgets.FloatText( 158 placeholder='select cell', 159 description="Angle (°)", 160 disabled=True 161 ).add_class("global_basic_input").add_class("defect_place_layer_input") 162 163 rendering_toggle_buttons = [] 164 show_all_button = widgets.Button( 165 description='Show all', 166 disabled=False 167 ).add_class("global_basic_button") 168 show_all_button.on_click(self.show_mesh) 169 rendering_toggle_buttons.append(show_all_button) 170 for id, data in self.MESH_DATA.items(): 171 current_button = widgets.Button( 172 description='Show ' + data["name"], 173 disabled=False 174 ).add_class("global_basic_button") 175 current_button.on_click(functools.partial(self.show_mesh, selected_meshes=tuple([id]))) 176 rendering_toggle_buttons.append(current_button) 177 toggle_button_box = widgets.HBox([x for x in rendering_toggle_buttons]) 178 show_defects_button = widgets.Button( 179 description='Show Defects', 180 disabled=False 181 ).add_class("global_basic_button") 182 show_defects_button.on_click(functools.partial(self.change_scalar_display_mode, scalar_name="state")) 183 show_sections_button = widgets.Button( 184 description='Show Sections', 185 disabled=False 186 ).add_class("global_basic_button") 187 show_sections_button.on_click(functools.partial(self.change_scalar_display_mode, scalar_name="section")) 188 scalar_button_box = widgets.HBox([show_defects_button, show_sections_button]) 189 190 place_defect_button = widgets.Button( 191 description='Place Defect', 192 disabled=False 193 ).add_class("global_basic_button") 194 place_defect_button.on_click(self.place_defect) 195 remove_defects_button = widgets.Button( 196 description='Remove Defects', 197 disabled=False 198 ).add_class("global_basic_button") 199 remove_defects_button.on_click(self.remove_defects) 200 grow_selection_button = widgets.Button( 201 description='Grow Selection', 202 disabled=False 203 ).add_class("global_basic_button") 204 grow_selection_button.on_click(self.grow_selection) 205 shrink_selection_button = widgets.Button( 206 description='Shrink Selection', 207 disabled=False 208 ).add_class("global_basic_button") 209 shrink_selection_button.on_click(self.shrink_selection) 210 self.defect_id_widget = widgets.Dropdown( 211 options=[(defect.display_name, defect_id) for defect_id, defect in self.available_defects.items()], 212 value=min(self.available_defects.keys()), 213 description='Defect:', 214 disabled=False, 215 ).add_class("global_basic_input") 216 self.defect_id_widget.observe(self.on_defect_type_changed, names=['value']) 217 self.defect_param_output = widgets.Output().add_class("defect_param_output_widget") 218 219 self.fig = self.init_cell_layer_rendering() 220 select_displayed_layer_headline = widgets.Label(value="Display Options:").add_class("global_headline") 221 selected_cell_headline = widgets.Label(value="Cell Information:").add_class("global_headline") 222 cell_select_info_box = widgets.VBox([ 223 select_displayed_layer_headline, 224 toggle_button_box, 225 scalar_button_box, 226 selected_cell_headline, 227 self.selected_id_widget, 228 self.layer_dropdown_widget, 229 self.cell_layer_widget, 230 self.layer_material_widget, 231 self.layer_thickness_widget, 232 self.layer_angle_widget 233 ]) 234 defect_configuration_box = widgets.VBox([ 235 widgets.HBox([ 236 place_defect_button, 237 remove_defects_button 238 ]), 239 self.defect_id_widget, 240 self.defect_param_output 241 ]) 242 defect_definition_box = widgets.VBox([ 243 widgets.Label(value="Cell selection:").add_class("global_headline"), 244 widgets.HBox([ 245 grow_selection_button, 246 shrink_selection_button 247 ]), 248 widgets.Label(value="Defect configuration:").add_class("global_headline"), 249 defect_configuration_box 250 ]) 251 layup_headline = widgets.Label(value="Cell Material Layup:").add_class("global_headline").add_class("defect_place_layup_headline") 252 layer_box = widgets.VBox([ 253 layup_headline, 254 self.fig.canvas 255 ]).add_class("defect_place_layer_render_container") 256 257 input_set_selection_label = widgets.Label(value="Select Input Set:").add_class("global_headline") 258 self.input_set_selection = widgets.Select( 259 options=['No data loaded'], 260 value='No data loaded', 261 rows=10, 262 description='Input sets:', 263 disabled=False 264 ).add_class("global_input_set_selection").add_class("global_basic_input") 265 display_input_set_button = widgets.Button( 266 description='Load Input Set', 267 disabled=False, 268 tooltip='Load Input Set', 269 icon='play', 270 ).add_class("global_basic_button") 271 display_input_set_button.on_click(self.display_input_set) 272 input_set_box = widgets.VBox([ 273 input_set_selection_label, 274 self.input_set_selection, 275 display_input_set_button 276 ]) 277 placed_defects_list_label = widgets.Label(value="Defects in cell:").add_class("global_headline") 278 self.defect_in_cell_selection = widgets.Select( 279 options=['Select cell first'], 280 value='Select cell first', 281 rows=10, 282 description='', 283 disabled=False 284 ).add_class("global_input_set_selection").add_class("global_basic_input") 285 defects_in_cell_box = widgets.VBox([ 286 placed_defects_list_label, 287 self.defect_in_cell_selection 288 ]) 289 save_input_set_button = widgets.Button( 290 description='Save configured defects as new input set', 291 disabled=False, 292 tooltip='Save configured defects as new input set', 293 icon='save', 294 ).add_class("global_save_input_set_button").add_class("global_basic_button") 295 save_input_set_button.on_click(self.save_new_input_set) 296 297 simulation_display_box = widgets.HBox([ 298 self.init_pyvista_render(), 299 layer_box 300 ]).add_class("defect_place_simulation_row_container") 301 simulation_input_box = widgets.HBox([ 302 input_set_box, 303 cell_select_info_box, 304 defects_in_cell_box, 305 defect_definition_box 306 ]).add_class("defect_place_simulation_row_container") 307 simulation_box = widgets.VBox([ 308 simulation_display_box, 309 simulation_input_box, 310 save_input_set_button 311 ]) 312 return simulation_box 313 314 def cell_selected(self, cell: pv.UnstructuredGrid): 315 """Update material layup render and layer display with new information. 316 317 Args: 318 cell: Single PyVista cell returned by callback 319 """ 320 self.reset_multi_select() 321 self.selected_cells["center"] = cell 322 cell_id = cell['original_id'][0] 323 available_layers = self.layer_data.get_available_layers_for_cell(cell_id) 324 self.layer_dropdown_widget.options = [("Layer {}: {}".format(x[self.LAYER_ID_KEY], x[self.LAYER_MATERIAL_KEY]), x[self.LAYER_ID_KEY]) 325 for i, x in available_layers.iterrows()] 326 327 self.update_defects_in_cell_display(cell_id) 328 self.update_layers_in_cell_display(cell_id) 329 self.render_cell_layer_structure(available_layers) 330 331 def update_defects_in_cell_display(self, cell_id: int): 332 """Update display of already placed faults in currently selected cell.""" 333 defects_in_cell = self.placed_defects.get_defects_of_cell(cell_id) 334 if defects_in_cell: 335 options = [] 336 for layer_id, defects in defects_in_cell.items(): 337 for defect_id, defect in defects.items(): 338 options.append("Layer {}: '{}'".format(layer_id, defect.generate_display_label())) 339 self.defect_in_cell_selection.options = options 340 else: 341 self.defect_in_cell_selection.options = ['No defects in selected cell'] 342 343 def update_layers_in_cell_display(self, cell_id: int): 344 """Update display of available layers in currently selected cell.""" 345 first_layer = self.layer_data.get_available_layers_for_cell(cell_id).iloc[0] 346 self.layer_dropdown_widget.value = first_layer[self.LAYER_ID_KEY] 347 self.selected_id_widget.value = cell_id 348 self.cell_layer_widget.value = first_layer[self.LAYER_ID_KEY] 349 self.layer_material_widget.value = first_layer[self.LAYER_MATERIAL_KEY] 350 self.layer_thickness_widget.value = first_layer[self.LAYER_THICKNESS_KEY] 351 self.layer_angle_widget.value = first_layer[self.LAYER_ANGLE_KEY] 352 self.layer_dropdown_widget.observe(self.layer_selected, names="value") 353 354 def show_mesh(self, _, selected_meshes: list=[]): 355 """Update visibility of all meshes in PyVista view. 356 357 Args: 358 selected_meshes: ids of meshes to display in case of partial render 359 """ 360 if len(selected_meshes) == 0: 361 selected_meshes = self.MESH_DATA.keys() 362 for id, data in self.MESH_DATA.items(): 363 current_visibility = data["mesh"].GetVisibility() 364 if current_visibility == False and id in selected_meshes: 365 data["mesh"].SetVisibility(True) 366 elif current_visibility == True and id not in selected_meshes: 367 data["mesh"].SetVisibility(False) 368 self.plotter.update() 369 370 def create_display_mesh_data(self) -> dict: 371 """Read 3D mesh data from file and analyse for display in UI. 372 373 Returns: 374 dictionary of mesh data divided into different shells according to SHELL_DATA_KEY 375 """ 376 initial_mesh = pv.read(self.MESH_FILE_PATH).cast_to_unstructured_grid() 377 available_shells = np.unique(initial_mesh[self.SHELL_DATA_KEY]) 378 # IDs in layer file start at 1 379 initial_mesh['original_id'] = [x+1 for x in range(len(initial_mesh[self.SECTION_DATA_KEY]))] 380 display_mesh_data = {} 381 for shell in available_shells: 382 cell_to_remove = np.argwhere(initial_mesh[self.SHELL_DATA_KEY] != shell) 383 extracted_shell = initial_mesh.remove_cells(cell_to_remove) 384 extracted_scalars = extracted_shell[self.SECTION_DATA_KEY] 385 extracted_shell['subshell_id'] = [x for x in range(len(extracted_scalars))] 386 extracted_shell['color'] = [0 for x in range(len(extracted_scalars))] 387 extracted_shell['section'] = extracted_shell[self.SECTION_DATA_KEY] 388 if shell == 0: 389 display_name = "Outer Shell" 390 elif shell == 1: 391 display_name = "Webs" 392 else: 393 display_name = "Layer '{}'".format(shell) 394 display_mesh_data[shell] = { 395 "input": extracted_shell, 396 "name": display_name, 397 "mesh": None, 398 "scalar_arrays": { 399 "section": extracted_scalars, 400 "state": [0 for x in range(len(extracted_scalars))], 401 "selection": [0 for x in range(len(extracted_scalars))] 402 }, 403 "value_ranges": { 404 "section": [0, 68], 405 "state": [0, self.SELECTION_COLOR_ID], 406 "selection": [0, self.SELECTION_COLOR_ID] 407 } 408 } 409 return display_mesh_data 410 411 def layer_selected(self, change: Bunch): 412 """Change displayed layer information on layer dropdown change.""" 413 selected_layer_data = self.layer_data.get_layer_of_cell(self.selected_cells["center"]['original_id'][0], change['new']) 414 self.cell_layer_widget.value = selected_layer_data[self.LAYER_ID_KEY] 415 self.layer_material_widget.value = selected_layer_data[self.LAYER_MATERIAL_KEY] 416 self.layer_thickness_widget.value = selected_layer_data[self.LAYER_THICKNESS_KEY] 417 self.layer_angle_widget.value = selected_layer_data[self.LAYER_ANGLE_KEY] 418 419 def init_cell_layer_rendering(self) -> plt.Figure: 420 """Initialize 3D matplot for rendering material layup and fill with placeholder.""" 421 with plt.ioff(): 422 fig = plt.figure() 423 fig.set_figwidth(4) 424 self.ax = fig.add_subplot(111, projection='3d', computed_zorder=False) 425 fig.set_facecolor((0.0, 0.0, 0.0, 0.0)) 426 self.ax.set_facecolor((0.0, 0.0, 0.0, 0.0)) 427 self.ax.set_axis_off() 428 self.ax.set_title("") 429 fig.canvas.toolbar_visible = False 430 fig.canvas.header_visible = False 431 fig.canvas.footer_visible = False 432 fig.canvas.resizable = False 433 self.ax.bar3d(1, 1, 1, 1, 1, 1, label="Select cell first",color="white", shade=True) 434 handles, labels = self.ax.get_legend_handles_labels() 435 self.ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.05), prop={'size': 12}) 436 self.ax.disable_mouse_rotation() 437 return fig 438 439 def render_cell_layer_structure(self, layers: pd.DataFrame): 440 """Update rendered material layupt 3D matplot. 441 442 Args: 443 layers: Sorted layers to display with structure according to CellLayersDataSet 444 """ 445 self.ax.clear() 446 self.ax.set_axis_off() 447 self.ax.set_title("Cell {}".format(self.selected_id_widget.value), y=-0.01) 448 total_width = 0 449 total_height = 0 450 legend_colors = [] 451 graph_colors = [] 452 current_label = None 453 previous_label = None 454 graph_labels = [] 455 layer_counter = 1 456 layer_counts = [] 457 layer_angles = [] 458 group_width = 0.5 459 alignments = [[]] 460 461 # count layer and material composition 462 for i, (_, layer) in enumerate(layers.iterrows()): 463 color = configuration.get_material_color(layer[self.LAYER_MATERIAL_KEY]) 464 current_label = layer[self.LAYER_MATERIAL_KEY] 465 if current_label != previous_label: 466 graph_labels.append(current_label) 467 legend_colors.append(color) 468 if i > 0: 469 layer_counts.append(layer_counter) 470 layer_counter = 1 471 else: 472 graph_labels.append("_") 473 layer_counter += 1 474 graph_colors.append(color) 475 previous_label = layer[self.LAYER_MATERIAL_KEY] 476 total_height += layer[self.LAYER_THICKNESS_KEY] 477 layer_counts.append(layer_counter) 478 for layer in layer_counts: 479 total_width += group_width 480 481 # add data to graph 482 current_height = total_height + layers.iloc[0][self.LAYER_THICKNESS_KEY] 483 current_width = 0 484 current_layer_index = 0 485 for i, (_, layer) in enumerate(layers.iterrows()): 486 current_label = layer[self.LAYER_MATERIAL_KEY] 487 if current_label != previous_label and i > 0: 488 current_layer_index += 1 489 alignments.append([]) 490 current_height -= layer[self.LAYER_THICKNESS_KEY] 491 if layer_counts[current_layer_index] == 1: 492 width_delta = group_width * 0.66 493 else: 494 width_delta = group_width / layer_counts[current_layer_index] 495 current_width += width_delta 496 p = self.ax.bar3d(1, 1, current_height, current_width, 3, layer[self.LAYER_THICKNESS_KEY], 497 label=graph_labels[i],color=graph_colors[i],zorder=current_height, shade=True) 498 previous_label = layer[self.LAYER_MATERIAL_KEY] 499 alignments[current_layer_index].append(layer[self.LAYER_ANGLE_KEY]) 500 501 # adjust text and colors of legend 502 handles, labels = self.ax.get_legend_handles_labels() 503 for i, layer_count in enumerate(layer_counts): 504 if layer_count > 1: 505 labels[i] = "{} - {}".format(labels[i], composite_layup.generate_composite_layup_description(alignments[i])) 506 self.ax.disable_mouse_rotation() 507 self.ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.30), prop={'size': 10}) 508 for i, color in enumerate(legend_colors): 509 self.ax.get_legend().legend_handles[i].set_color(color) 510 511 plt.show() 512 # manually trigger redraw event - update cycles take several seconds to show up on frontend otherwise 513 self.fig.canvas.draw() 514 515 def create_scalar_color_definitions(self) -> dict: 516 """Create custom color maps for defect and section displays in PyVista 3D render. 517 518 Returns: 519 dict with str key and matplotlib Colormap as value 520 """ 521 color_maps = {} 522 color_maps["section"] = "prism" 523 524 default = np.array([189 / 256, 189 / 256, 189 / 256, 1.0]) 525 multiple_defects = np.array([1.0, 0.0, 0.0, 1.0]) 526 selection = np.array([255 / 256, 3 / 256, 213 / 256, 1.0]) 527 intmapping = np.linspace(0, self.SELECTION_COLOR_ID, 256) 528 intcolors = np.empty((256, 4)) 529 intcolors[intmapping <= (self.SELECTION_COLOR_ID + 0.1)] = selection 530 intcolors[intmapping <= (self.MULTIPLE_DEFECTS_COLOR_ID + 0.1)] = multiple_defects 531 for defect_id, defect in reversed(self.available_defects.items()): 532 defect_color = list(defect.display_color) 533 defect_color.append(1.0) 534 intcolors[intmapping <= (defect_id + 0.1)] = defect_color 535 intcolors[intmapping <= 0.1] = default 536 color_maps["state"] = pltcolor.ListedColormap(intcolors) 537 538 return color_maps 539 540 def change_scalar_display_mode(self, _, scalar_name: str=None): 541 """Change displayed scalar for colors on 3D mesh.""" 542 self.reset_multi_select() 543 for shell_id, data in self.MESH_DATA.items(): 544 scalars = data["scalar_arrays"][scalar_name] 545 value_range = data["value_ranges"][scalar_name] 546 data["input"].cell_data.set_scalars(scalars, "color") 547 data["mesh"].mapper.lookup_table.cmap = self.COLOR_DEFINITIONS[scalar_name] 548 data["mesh"].mapper.scalar_range = value_range[0], value_range[1] 549 self.active_scalar_display = scalar_name 550 self.plotter.update() 551 552 def place_defect(self, ui_element=None): 553 """Place new defect according to currently entered information in UI. 554 555 Args: 556 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 557 """ 558 cells = self.selected_cells["all"] if self.selected_cells["all"] else self.selected_cells["center"] 559 subshell_ids = cells['subshell_id'] 560 vtk_ids = cells['original_id'] 561 params = self.get_new_defect_configuration(self) 562 for i in range(len(subshell_ids)): 563 new_defect = self.new_defect_to_place(vtk_ids[i], self.layer_dropdown_widget.value, **params) 564 self.placed_defects.add_defect_to_cell(vtk_ids[i], self.layer_dropdown_widget.value, new_defect) 565 self.update_defects_in_cell_display(self.selected_cells["center"]['original_id'][0]) 566 self.update_displayed_cell_state(cells) 567 self.reset_multi_select() 568 569 def remove_defects(self, ui_element=None): 570 """Remove all defects from currently selected cell. 571 572 Args: 573 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 574 """ 575 cells = self.selected_cells["all"] if self.selected_cells["all"] else self.selected_cells["center"] 576 subshell_ids = cells['subshell_id'] 577 vtk_ids = cells['original_id'] 578 for i in range(len(subshell_ids)): 579 self.placed_defects.clear_defects_of_cell(vtk_ids[i]) 580 self.update_defects_in_cell_display(self.selected_cells["center"]['original_id'][0]) 581 self.update_displayed_cell_state(cells) 582 self.reset_multi_select() 583 584 def update_displayed_cell_state(self, cells: pv.UnstructuredGrid): 585 """Update colors of selected cells in 3D mesh according to current defect state. 586 587 Args: 588 cells to refresh defect display information for 589 """ 590 shell_id = cells[self.SHELL_DATA_KEY][0] 591 data = self.MESH_DATA[shell_id] 592 subshell_ids = cells['subshell_id'] 593 vtk_ids = cells['original_id'] 594 defect_display_data = self.placed_defects.generate_defect_display_summary() 595 for i, vtk_id in enumerate(vtk_ids): 596 if vtk_id not in defect_display_data["cell_defect_types"]: 597 display_value = 0 598 else: 599 cell_defects = defect_display_data["cell_defect_types"][vtk_id] 600 if len(cell_defects) > 1: 601 display_value = self.MULTIPLE_DEFECTS_COLOR_ID 602 else: 603 display_value = cell_defects[0] 604 data["scalar_arrays"]["state"][subshell_ids[i]] = display_value 605 606 def grow_selection(self, ui_element=None): 607 """Grow current selection of cells in all directions by 1 cell while respecting section borders. 608 609 Args: 610 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 611 """ 612 grow_counter = self.selected_cells["grow_counter"] + 1 613 self.perform_multiselect(grow_counter) 614 615 def shrink_selection(self, ui_element=None): 616 """Shrink current selection of cells in all directions by 1 cell. 617 618 Args: 619 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 620 """ 621 grow_counter = self.selected_cells["grow_counter"] - 1 622 self.perform_multiselect(grow_counter) 623 624 def perform_multiselect(self, grow_counter: int): 625 """Update selection area according to current multi select counter.""" 626 self.reset_multi_select(refresh=False) 627 self.selected_cells["grow_counter"] = grow_counter 628 cell = self.selected_cells["center"] 629 shell_id = cell['WebShellAssignment'][0] 630 search_id = cell['original_id'][0] 631 cell_subshell_id = cell['subshell_id'][0] 632 section_id = cell['section'][0] 633 mesh_data = self.MESH_DATA[shell_id]["input"] 634 mesh_data.cell_data.set_scalars(self.MESH_DATA[shell_id]["scalar_arrays"]["state"], "color") 635 636 cells_found = [cell_subshell_id] 637 for _ in range(grow_counter): 638 new_cells = [] 639 for cell_id in cells_found: 640 result = mesh_data.cell_neighbors(cell_id, "points") 641 extracted_cells = mesh_data.extract_cells(result) 642 section_ids = extracted_cells['section'] 643 subshell_ids = extracted_cells['subshell_id'] 644 for i, item in enumerate(section_ids): 645 if item == section_id: 646 new_cells.append(subshell_ids[i]) 647 cells_found.extend(new_cells) 648 cells_found = list(set(cells_found)) 649 650 grow_cells = mesh_data.extract_cells(cells_found) 651 self.selected_cells["all"] = grow_cells 652 for i in grow_cells['subshell_id']: 653 self.MESH_DATA[shell_id]["scalar_arrays"]["selection"][i] = self.SELECTION_COLOR_ID 654 mesh_data.cell_data.set_scalars(self.MESH_DATA[shell_id]["scalar_arrays"]["selection"], "color") 655 656 self.plotter.update() 657 658 def reset_multi_select(self, refresh: bool=True): 659 """Reset internal storage of currently selected cells and display state of 3D mesh back to baseline. 660 661 Args: 662 refresh: Trigger PyVista re-render or not 663 """ 664 self.selected_cells["all"] = None 665 self.selected_cells["grow_counter"] = 0 666 for key, data in self.MESH_DATA.items(): 667 data["scalar_arrays"]["selection"] = copy.deepcopy(data["scalar_arrays"]["state"]) 668 if self.active_scalar_display == 'state': 669 data["input"].cell_data.set_scalars(data["scalar_arrays"]["state"], "color") 670 if refresh: 671 self.plotter.update() 672 673 def on_defect_type_changed(self, ui_element=None): 674 """Update displayed UI elements for defect configuration after selected defect type has changed. 675 676 Args: 677 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 678 """ 679 selected_defect_type_id = self.defect_id_widget.value 680 self.new_defect_to_place = self.available_defects[selected_defect_type_id] 681 match selected_defect_type_id: 682 case 1: # PlyWaviness 683 self.defect_param_widget = widgets.FloatText( 684 value=0, 685 description="Orientation (°):" 686 ).add_class("defect_param_input").add_class("global_basic_input") 687 self.defect_length_widget = widgets.FloatText( 688 value=0, 689 description="Length (m):" 690 ).add_class("defect_param_input").add_class("global_basic_input") 691 self.defect_amplitude_widget = widgets.FloatText( 692 value=0, 693 description="Amplitude (m):" 694 ).add_class("defect_param_input").add_class("global_basic_input") 695 output = widgets.VBox([ 696 self.defect_param_widget, 697 self.defect_length_widget, 698 self.defect_amplitude_widget 699 ]) 700 def get_param(self): 701 return { 702 "orientation": self.defect_param_widget.value, 703 "length": self.defect_length_widget.value, 704 "amplitude": self.defect_amplitude_widget.value 705 } 706 self.get_new_defect_configuration = get_param 707 case 2: # PlyMisorientation 708 self.defect_param_widget = widgets.FloatText( 709 value=0, 710 description="Offset Angle (°):" 711 ).add_class("defect_param_input").add_class("global_basic_input") 712 output = self.defect_param_widget 713 def get_param(self): 714 return { 715 "angle": self.defect_param_widget.value 716 } 717 self.get_new_defect_configuration = get_param 718 case 3: # MissingPly 719 self.defect_param_widget = None 720 output = None 721 def get_param(self): 722 return {} 723 self.get_new_defect_configuration = get_param 724 case 4: # VaryingThickness 725 self.defect_param_widget = widgets.FloatText( 726 value=0, 727 description="FVC (%):" 728 ).add_class("defect_param_input").add_class("global_basic_input") 729 output = self.defect_param_widget 730 def get_param(self): 731 return { 732 "fvc": self.defect_param_widget.value 733 } 734 self.get_new_defect_configuration = get_param 735 case _: 736 print("Unknown type of defect selected with ID {}".format(self.defect_id_widget.value)) 737 self.defect_param_output.clear_output() 738 if output: 739 with self.defect_param_output: 740 display(output) 741 742 def get_new_defect_configuration(self) -> dict: 743 """Return all parameters of defect currently being defined. 744 745 This method is redefined everytime the selected defect type changes 746 747 Returns: 748 dictionary of all parameters currently entered in UI for defect configuration (can be empty) 749 """ 750 pass 751 752 def load_local_data(self, ui_element=None): 753 """Use locally stored data for UI. 754 755 Args: 756 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 757 """ 758 self.conn = store_connection.LocalStoreConnection("sensotwin_world") 759 self.placed_defects = configuration.DefectInputDataSet(conn=self.conn) 760 self.load_input_set_data() 761 762 def load_remote_data(self, ui_element=None): 763 """Use remotely stored data for UI. 764 765 Args: 766 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 767 """ 768 self.conn = store_connection.FusekiConnection(self.remote_database_input.value) 769 self.placed_defects = configuration.DefectInputDataSet(conn=self.conn) 770 self.load_input_set_data() 771 772 def save_new_input_set(self, ui_element=None): 773 new_id = max(self.input_set_data.keys()) + 1 if self.input_set_data else 1 774 775 self.placed_defects.save_entry_to_store(new_id) 776 self.load_input_set_data() 777 778 def load_input_set_data(self): 779 """Load data from previously set input source and populate UI.""" 780 self.input_set_data = configuration.DefectInputDataSet.get_all_entries_from_store(self.conn) 781 options = [] 782 if self.input_set_data: 783 for key, input_set in self.input_set_data.items(): 784 label = input_set.generate_input_set_display_label() 785 options.append((label, key)) 786 self.input_set_selection.options = options 787 self.input_set_selection.value = list(self.input_set_data.keys())[0] 788 789 def display_input_set(self, ui_element=None): 790 """Display data for currently selected input set in UI. 791 792 Args: 793 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 794 """ 795 self.placed_defects = self.input_set_data[self.input_set_selection.value] 796 for id, data in self.MESH_DATA.items(): 797 self.update_displayed_cell_state(data["input"]) 798 self.reset_multi_select() 799 800 def apply_styling(self): 801 """Apply CSS hack to notebook for better styling than native Jupyter Widget styles.""" 802 css = """ 803 <style> 804 {} 805 {} 806 </style> 807 """.format(global_style.global_css, style.local_css) 808 return widgets.HTML(css)
Contains all UI functionality of material curing process configuration UI (Step 2).
In order to use, show the top level widget 'self.dashboard' in one cell of a notebook and the apply css styling calling 'self.apply_styling()' an another cell
35 def __init__(self): 36 """Initialize UI objects and associated UI functionality.""" 37 self.init_global_data() 38 self.dashboard = widgets.Tab().add_class("global_tab_container") 39 self.dashboard.children = [ 40 self.init_data_source_box(), 41 self.init_placement_box() 42 ] 43 tab_titles = ['Input', 'Simulation'] 44 for i in range(len(tab_titles)): 45 self.dashboard.set_title(i, tab_titles[i]) 46 # manually trigger change event to properly render intial defect configuration display 47 self.on_defect_type_changed()
Initialize UI objects and associated UI functionality.
49 def init_global_data(self): 50 """Set necessary global parameters for UI and load static data like 3D mesh and 3D mesh layer data.""" 51 self.available_defects = {} 52 for defect in configuration.DefectInputDataSet().get_available_defect_types(): 53 self.available_defects[defect.type_id] = defect 54 self.new_defect_to_place = self.available_defects[min(self.available_defects.keys())] 55 self.MULTIPLE_DEFECTS_COLOR_ID = len(self.available_defects) + 1 56 self.SELECTION_COLOR_ID = self.MULTIPLE_DEFECTS_COLOR_ID + 1 57 self.COLOR_DEFINITIONS = self.create_scalar_color_definitions() 58 self.selected_cells = { 59 "center": None, 60 "all": None, 61 "grow_counter" : 0 62 } 63 self.active_scalar_display = 'state' 64 self.layer_data = configuration.CellLayersDataSet(self.LAYER_FILE_PATH) 65 self.MESH_DATA = self.create_display_mesh_data()
Set necessary global parameters for UI and load static data like 3D mesh and 3D mesh layer data.
67 def init_data_source_box(self) -> widgets.Box: 68 """Initialize first tab of dashboard, choosing data source.""" 69 local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline") 70 use_local_data_button = widgets.Button( 71 description='Use local database', 72 disabled=False, 73 tooltip='Use local database', 74 icon='play', 75 ).add_class("global_load_data_button").add_class("global_basic_button") 76 use_local_data_button.on_click(self.load_local_data) 77 78 remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline") 79 self.remote_database_input = widgets.Text( 80 value=None, 81 placeholder="insert SPARQL url here", 82 description="SPARQL Endpoint:", 83 disabled=False 84 ).add_class("global_url_input").add_class("global_basic_input") 85 use_remote_data_button = widgets.Button( 86 description='Use remote database', 87 disabled=False, 88 tooltip='Use remote database', 89 icon='play', 90 ).add_class("global_load_data_button").add_class("global_basic_button") 91 use_remote_data_button.on_click(self.load_remote_data) 92 local_data_box = widgets.VBox([ 93 local_database_label, 94 use_local_data_button 95 ]) 96 97 remote_data_box = widgets.VBox([ 98 remote_database_label, 99 self.remote_database_input, 100 use_remote_data_button 101 ]) 102 data_source_box = widgets.VBox([ 103 local_data_box, 104 remote_data_box 105 ]).add_class("global_data_tab_container") 106 return data_source_box
Initialize first tab of dashboard, choosing data source.
108 def init_pyvista_render(self) -> widgets.Output: 109 """Initialize Jupyter Output widget for rendering 3D mesh via PyVista.""" 110 OUTPUT_RENDER_HEIGHT = 500 111 OUTPUT_RENDER_WIDTH= 1000 112 self.plotter = pv.Plotter() 113 for id, data in self.MESH_DATA.items(): 114 data["mesh"] = self.plotter.add_mesh(data["input"], show_edges=True, show_scalar_bar=False, clim=data["value_ranges"]['state'], cmap=self.COLOR_DEFINITIONS['state'], scalars="color") 115 self.plotter.camera_position = [(-30.765771120379302, -28.608772602676154, 39.46235706090557), 116 (0.9572034500000001, 0.0005481500000000805, -30.4), 117 (-0.16976192468426815, -0.8825527410211623, -0.43849919982085056)] 118 self.plotter.window_size = [OUTPUT_RENDER_WIDTH, OUTPUT_RENDER_HEIGHT] 119 self.plotter.enable_element_picking(callback=self.cell_selected, show_message=False) 120 legend_labels = [(defect.display_name, defect.display_color) for _, defect in self.available_defects.items()] 121 legend_labels.append(["Multiple Defects", (1.0, 0.0, 0.0)]) 122 self.plotter.add_legend(labels=legend_labels, bcolor=None, size=(0.2,0.2), loc="upper right", face=None) 123 render_widget = widgets.Output(layout={'height': '{}px'.format(OUTPUT_RENDER_HEIGHT+15), 124 'width': '{}px'.format(OUTPUT_RENDER_WIDTH+10)}) 125 with render_widget: 126 self.plotter.show(jupyter_backend='trame') 127 return render_widget
Initialize Jupyter Output widget for rendering 3D mesh via PyVista.
129 def init_placement_box(self) -> widgets.Box: 130 """Initialize second tab of dashboard, configuring construction defects.""" 131 self.selected_id_widget = widgets.IntText( 132 value=None, 133 description="Selected Cell", 134 disabled=True 135 ).add_class("global_basic_input").add_class("defect_place_layer_input") 136 self.layer_dropdown_widget = widgets.Dropdown( 137 options=['Select cell first'], 138 value='Select cell first', 139 description='Show Layer', 140 disabled=False, 141 ).add_class("global_basic_input").add_class("defect_place_layer_input") 142 self.cell_layer_widget = widgets.FloatText( 143 placeholder='select cell', 144 description="Layer ID", 145 disabled=True 146 ).add_class("global_basic_input").add_class("defect_place_layer_input") 147 self.layer_material_widget = widgets.Text( 148 placeholder='select cell', 149 description="Material", 150 disabled=True 151 ).add_class("global_basic_input").add_class("defect_place_layer_input") 152 self.layer_thickness_widget = widgets.FloatText( 153 placeholder='select cell', 154 description="Thickness (mm)", 155 disabled=True 156 ).add_class("global_basic_input").add_class("defect_place_layer_input") 157 self.layer_angle_widget = widgets.FloatText( 158 placeholder='select cell', 159 description="Angle (°)", 160 disabled=True 161 ).add_class("global_basic_input").add_class("defect_place_layer_input") 162 163 rendering_toggle_buttons = [] 164 show_all_button = widgets.Button( 165 description='Show all', 166 disabled=False 167 ).add_class("global_basic_button") 168 show_all_button.on_click(self.show_mesh) 169 rendering_toggle_buttons.append(show_all_button) 170 for id, data in self.MESH_DATA.items(): 171 current_button = widgets.Button( 172 description='Show ' + data["name"], 173 disabled=False 174 ).add_class("global_basic_button") 175 current_button.on_click(functools.partial(self.show_mesh, selected_meshes=tuple([id]))) 176 rendering_toggle_buttons.append(current_button) 177 toggle_button_box = widgets.HBox([x for x in rendering_toggle_buttons]) 178 show_defects_button = widgets.Button( 179 description='Show Defects', 180 disabled=False 181 ).add_class("global_basic_button") 182 show_defects_button.on_click(functools.partial(self.change_scalar_display_mode, scalar_name="state")) 183 show_sections_button = widgets.Button( 184 description='Show Sections', 185 disabled=False 186 ).add_class("global_basic_button") 187 show_sections_button.on_click(functools.partial(self.change_scalar_display_mode, scalar_name="section")) 188 scalar_button_box = widgets.HBox([show_defects_button, show_sections_button]) 189 190 place_defect_button = widgets.Button( 191 description='Place Defect', 192 disabled=False 193 ).add_class("global_basic_button") 194 place_defect_button.on_click(self.place_defect) 195 remove_defects_button = widgets.Button( 196 description='Remove Defects', 197 disabled=False 198 ).add_class("global_basic_button") 199 remove_defects_button.on_click(self.remove_defects) 200 grow_selection_button = widgets.Button( 201 description='Grow Selection', 202 disabled=False 203 ).add_class("global_basic_button") 204 grow_selection_button.on_click(self.grow_selection) 205 shrink_selection_button = widgets.Button( 206 description='Shrink Selection', 207 disabled=False 208 ).add_class("global_basic_button") 209 shrink_selection_button.on_click(self.shrink_selection) 210 self.defect_id_widget = widgets.Dropdown( 211 options=[(defect.display_name, defect_id) for defect_id, defect in self.available_defects.items()], 212 value=min(self.available_defects.keys()), 213 description='Defect:', 214 disabled=False, 215 ).add_class("global_basic_input") 216 self.defect_id_widget.observe(self.on_defect_type_changed, names=['value']) 217 self.defect_param_output = widgets.Output().add_class("defect_param_output_widget") 218 219 self.fig = self.init_cell_layer_rendering() 220 select_displayed_layer_headline = widgets.Label(value="Display Options:").add_class("global_headline") 221 selected_cell_headline = widgets.Label(value="Cell Information:").add_class("global_headline") 222 cell_select_info_box = widgets.VBox([ 223 select_displayed_layer_headline, 224 toggle_button_box, 225 scalar_button_box, 226 selected_cell_headline, 227 self.selected_id_widget, 228 self.layer_dropdown_widget, 229 self.cell_layer_widget, 230 self.layer_material_widget, 231 self.layer_thickness_widget, 232 self.layer_angle_widget 233 ]) 234 defect_configuration_box = widgets.VBox([ 235 widgets.HBox([ 236 place_defect_button, 237 remove_defects_button 238 ]), 239 self.defect_id_widget, 240 self.defect_param_output 241 ]) 242 defect_definition_box = widgets.VBox([ 243 widgets.Label(value="Cell selection:").add_class("global_headline"), 244 widgets.HBox([ 245 grow_selection_button, 246 shrink_selection_button 247 ]), 248 widgets.Label(value="Defect configuration:").add_class("global_headline"), 249 defect_configuration_box 250 ]) 251 layup_headline = widgets.Label(value="Cell Material Layup:").add_class("global_headline").add_class("defect_place_layup_headline") 252 layer_box = widgets.VBox([ 253 layup_headline, 254 self.fig.canvas 255 ]).add_class("defect_place_layer_render_container") 256 257 input_set_selection_label = widgets.Label(value="Select Input Set:").add_class("global_headline") 258 self.input_set_selection = widgets.Select( 259 options=['No data loaded'], 260 value='No data loaded', 261 rows=10, 262 description='Input sets:', 263 disabled=False 264 ).add_class("global_input_set_selection").add_class("global_basic_input") 265 display_input_set_button = widgets.Button( 266 description='Load Input Set', 267 disabled=False, 268 tooltip='Load Input Set', 269 icon='play', 270 ).add_class("global_basic_button") 271 display_input_set_button.on_click(self.display_input_set) 272 input_set_box = widgets.VBox([ 273 input_set_selection_label, 274 self.input_set_selection, 275 display_input_set_button 276 ]) 277 placed_defects_list_label = widgets.Label(value="Defects in cell:").add_class("global_headline") 278 self.defect_in_cell_selection = widgets.Select( 279 options=['Select cell first'], 280 value='Select cell first', 281 rows=10, 282 description='', 283 disabled=False 284 ).add_class("global_input_set_selection").add_class("global_basic_input") 285 defects_in_cell_box = widgets.VBox([ 286 placed_defects_list_label, 287 self.defect_in_cell_selection 288 ]) 289 save_input_set_button = widgets.Button( 290 description='Save configured defects as new input set', 291 disabled=False, 292 tooltip='Save configured defects as new input set', 293 icon='save', 294 ).add_class("global_save_input_set_button").add_class("global_basic_button") 295 save_input_set_button.on_click(self.save_new_input_set) 296 297 simulation_display_box = widgets.HBox([ 298 self.init_pyvista_render(), 299 layer_box 300 ]).add_class("defect_place_simulation_row_container") 301 simulation_input_box = widgets.HBox([ 302 input_set_box, 303 cell_select_info_box, 304 defects_in_cell_box, 305 defect_definition_box 306 ]).add_class("defect_place_simulation_row_container") 307 simulation_box = widgets.VBox([ 308 simulation_display_box, 309 simulation_input_box, 310 save_input_set_button 311 ]) 312 return simulation_box
Initialize second tab of dashboard, configuring construction defects.
314 def cell_selected(self, cell: pv.UnstructuredGrid): 315 """Update material layup render and layer display with new information. 316 317 Args: 318 cell: Single PyVista cell returned by callback 319 """ 320 self.reset_multi_select() 321 self.selected_cells["center"] = cell 322 cell_id = cell['original_id'][0] 323 available_layers = self.layer_data.get_available_layers_for_cell(cell_id) 324 self.layer_dropdown_widget.options = [("Layer {}: {}".format(x[self.LAYER_ID_KEY], x[self.LAYER_MATERIAL_KEY]), x[self.LAYER_ID_KEY]) 325 for i, x in available_layers.iterrows()] 326 327 self.update_defects_in_cell_display(cell_id) 328 self.update_layers_in_cell_display(cell_id) 329 self.render_cell_layer_structure(available_layers)
Update material layup render and layer display with new information.
Args: cell: Single PyVista cell returned by callback
331 def update_defects_in_cell_display(self, cell_id: int): 332 """Update display of already placed faults in currently selected cell.""" 333 defects_in_cell = self.placed_defects.get_defects_of_cell(cell_id) 334 if defects_in_cell: 335 options = [] 336 for layer_id, defects in defects_in_cell.items(): 337 for defect_id, defect in defects.items(): 338 options.append("Layer {}: '{}'".format(layer_id, defect.generate_display_label())) 339 self.defect_in_cell_selection.options = options 340 else: 341 self.defect_in_cell_selection.options = ['No defects in selected cell']
Update display of already placed faults in currently selected cell.
343 def update_layers_in_cell_display(self, cell_id: int): 344 """Update display of available layers in currently selected cell.""" 345 first_layer = self.layer_data.get_available_layers_for_cell(cell_id).iloc[0] 346 self.layer_dropdown_widget.value = first_layer[self.LAYER_ID_KEY] 347 self.selected_id_widget.value = cell_id 348 self.cell_layer_widget.value = first_layer[self.LAYER_ID_KEY] 349 self.layer_material_widget.value = first_layer[self.LAYER_MATERIAL_KEY] 350 self.layer_thickness_widget.value = first_layer[self.LAYER_THICKNESS_KEY] 351 self.layer_angle_widget.value = first_layer[self.LAYER_ANGLE_KEY] 352 self.layer_dropdown_widget.observe(self.layer_selected, names="value")
Update display of available layers in currently selected cell.
354 def show_mesh(self, _, selected_meshes: list=[]): 355 """Update visibility of all meshes in PyVista view. 356 357 Args: 358 selected_meshes: ids of meshes to display in case of partial render 359 """ 360 if len(selected_meshes) == 0: 361 selected_meshes = self.MESH_DATA.keys() 362 for id, data in self.MESH_DATA.items(): 363 current_visibility = data["mesh"].GetVisibility() 364 if current_visibility == False and id in selected_meshes: 365 data["mesh"].SetVisibility(True) 366 elif current_visibility == True and id not in selected_meshes: 367 data["mesh"].SetVisibility(False) 368 self.plotter.update()
Update visibility of all meshes in PyVista view.
Args: selected_meshes: ids of meshes to display in case of partial render
370 def create_display_mesh_data(self) -> dict: 371 """Read 3D mesh data from file and analyse for display in UI. 372 373 Returns: 374 dictionary of mesh data divided into different shells according to SHELL_DATA_KEY 375 """ 376 initial_mesh = pv.read(self.MESH_FILE_PATH).cast_to_unstructured_grid() 377 available_shells = np.unique(initial_mesh[self.SHELL_DATA_KEY]) 378 # IDs in layer file start at 1 379 initial_mesh['original_id'] = [x+1 for x in range(len(initial_mesh[self.SECTION_DATA_KEY]))] 380 display_mesh_data = {} 381 for shell in available_shells: 382 cell_to_remove = np.argwhere(initial_mesh[self.SHELL_DATA_KEY] != shell) 383 extracted_shell = initial_mesh.remove_cells(cell_to_remove) 384 extracted_scalars = extracted_shell[self.SECTION_DATA_KEY] 385 extracted_shell['subshell_id'] = [x for x in range(len(extracted_scalars))] 386 extracted_shell['color'] = [0 for x in range(len(extracted_scalars))] 387 extracted_shell['section'] = extracted_shell[self.SECTION_DATA_KEY] 388 if shell == 0: 389 display_name = "Outer Shell" 390 elif shell == 1: 391 display_name = "Webs" 392 else: 393 display_name = "Layer '{}'".format(shell) 394 display_mesh_data[shell] = { 395 "input": extracted_shell, 396 "name": display_name, 397 "mesh": None, 398 "scalar_arrays": { 399 "section": extracted_scalars, 400 "state": [0 for x in range(len(extracted_scalars))], 401 "selection": [0 for x in range(len(extracted_scalars))] 402 }, 403 "value_ranges": { 404 "section": [0, 68], 405 "state": [0, self.SELECTION_COLOR_ID], 406 "selection": [0, self.SELECTION_COLOR_ID] 407 } 408 } 409 return display_mesh_data
Read 3D mesh data from file and analyse for display in UI.
Returns: dictionary of mesh data divided into different shells according to SHELL_DATA_KEY
411 def layer_selected(self, change: Bunch): 412 """Change displayed layer information on layer dropdown change.""" 413 selected_layer_data = self.layer_data.get_layer_of_cell(self.selected_cells["center"]['original_id'][0], change['new']) 414 self.cell_layer_widget.value = selected_layer_data[self.LAYER_ID_KEY] 415 self.layer_material_widget.value = selected_layer_data[self.LAYER_MATERIAL_KEY] 416 self.layer_thickness_widget.value = selected_layer_data[self.LAYER_THICKNESS_KEY] 417 self.layer_angle_widget.value = selected_layer_data[self.LAYER_ANGLE_KEY]
Change displayed layer information on layer dropdown change.
419 def init_cell_layer_rendering(self) -> plt.Figure: 420 """Initialize 3D matplot for rendering material layup and fill with placeholder.""" 421 with plt.ioff(): 422 fig = plt.figure() 423 fig.set_figwidth(4) 424 self.ax = fig.add_subplot(111, projection='3d', computed_zorder=False) 425 fig.set_facecolor((0.0, 0.0, 0.0, 0.0)) 426 self.ax.set_facecolor((0.0, 0.0, 0.0, 0.0)) 427 self.ax.set_axis_off() 428 self.ax.set_title("") 429 fig.canvas.toolbar_visible = False 430 fig.canvas.header_visible = False 431 fig.canvas.footer_visible = False 432 fig.canvas.resizable = False 433 self.ax.bar3d(1, 1, 1, 1, 1, 1, label="Select cell first",color="white", shade=True) 434 handles, labels = self.ax.get_legend_handles_labels() 435 self.ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.05), prop={'size': 12}) 436 self.ax.disable_mouse_rotation() 437 return fig
Initialize 3D matplot for rendering material layup and fill with placeholder.
439 def render_cell_layer_structure(self, layers: pd.DataFrame): 440 """Update rendered material layupt 3D matplot. 441 442 Args: 443 layers: Sorted layers to display with structure according to CellLayersDataSet 444 """ 445 self.ax.clear() 446 self.ax.set_axis_off() 447 self.ax.set_title("Cell {}".format(self.selected_id_widget.value), y=-0.01) 448 total_width = 0 449 total_height = 0 450 legend_colors = [] 451 graph_colors = [] 452 current_label = None 453 previous_label = None 454 graph_labels = [] 455 layer_counter = 1 456 layer_counts = [] 457 layer_angles = [] 458 group_width = 0.5 459 alignments = [[]] 460 461 # count layer and material composition 462 for i, (_, layer) in enumerate(layers.iterrows()): 463 color = configuration.get_material_color(layer[self.LAYER_MATERIAL_KEY]) 464 current_label = layer[self.LAYER_MATERIAL_KEY] 465 if current_label != previous_label: 466 graph_labels.append(current_label) 467 legend_colors.append(color) 468 if i > 0: 469 layer_counts.append(layer_counter) 470 layer_counter = 1 471 else: 472 graph_labels.append("_") 473 layer_counter += 1 474 graph_colors.append(color) 475 previous_label = layer[self.LAYER_MATERIAL_KEY] 476 total_height += layer[self.LAYER_THICKNESS_KEY] 477 layer_counts.append(layer_counter) 478 for layer in layer_counts: 479 total_width += group_width 480 481 # add data to graph 482 current_height = total_height + layers.iloc[0][self.LAYER_THICKNESS_KEY] 483 current_width = 0 484 current_layer_index = 0 485 for i, (_, layer) in enumerate(layers.iterrows()): 486 current_label = layer[self.LAYER_MATERIAL_KEY] 487 if current_label != previous_label and i > 0: 488 current_layer_index += 1 489 alignments.append([]) 490 current_height -= layer[self.LAYER_THICKNESS_KEY] 491 if layer_counts[current_layer_index] == 1: 492 width_delta = group_width * 0.66 493 else: 494 width_delta = group_width / layer_counts[current_layer_index] 495 current_width += width_delta 496 p = self.ax.bar3d(1, 1, current_height, current_width, 3, layer[self.LAYER_THICKNESS_KEY], 497 label=graph_labels[i],color=graph_colors[i],zorder=current_height, shade=True) 498 previous_label = layer[self.LAYER_MATERIAL_KEY] 499 alignments[current_layer_index].append(layer[self.LAYER_ANGLE_KEY]) 500 501 # adjust text and colors of legend 502 handles, labels = self.ax.get_legend_handles_labels() 503 for i, layer_count in enumerate(layer_counts): 504 if layer_count > 1: 505 labels[i] = "{} - {}".format(labels[i], composite_layup.generate_composite_layup_description(alignments[i])) 506 self.ax.disable_mouse_rotation() 507 self.ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.30), prop={'size': 10}) 508 for i, color in enumerate(legend_colors): 509 self.ax.get_legend().legend_handles[i].set_color(color) 510 511 plt.show() 512 # manually trigger redraw event - update cycles take several seconds to show up on frontend otherwise 513 self.fig.canvas.draw()
Update rendered material layupt 3D matplot.
Args: layers: Sorted layers to display with structure according to CellLayersDataSet
515 def create_scalar_color_definitions(self) -> dict: 516 """Create custom color maps for defect and section displays in PyVista 3D render. 517 518 Returns: 519 dict with str key and matplotlib Colormap as value 520 """ 521 color_maps = {} 522 color_maps["section"] = "prism" 523 524 default = np.array([189 / 256, 189 / 256, 189 / 256, 1.0]) 525 multiple_defects = np.array([1.0, 0.0, 0.0, 1.0]) 526 selection = np.array([255 / 256, 3 / 256, 213 / 256, 1.0]) 527 intmapping = np.linspace(0, self.SELECTION_COLOR_ID, 256) 528 intcolors = np.empty((256, 4)) 529 intcolors[intmapping <= (self.SELECTION_COLOR_ID + 0.1)] = selection 530 intcolors[intmapping <= (self.MULTIPLE_DEFECTS_COLOR_ID + 0.1)] = multiple_defects 531 for defect_id, defect in reversed(self.available_defects.items()): 532 defect_color = list(defect.display_color) 533 defect_color.append(1.0) 534 intcolors[intmapping <= (defect_id + 0.1)] = defect_color 535 intcolors[intmapping <= 0.1] = default 536 color_maps["state"] = pltcolor.ListedColormap(intcolors) 537 538 return color_maps
Create custom color maps for defect and section displays in PyVista 3D render.
Returns: dict with str key and matplotlib Colormap as value
540 def change_scalar_display_mode(self, _, scalar_name: str=None): 541 """Change displayed scalar for colors on 3D mesh.""" 542 self.reset_multi_select() 543 for shell_id, data in self.MESH_DATA.items(): 544 scalars = data["scalar_arrays"][scalar_name] 545 value_range = data["value_ranges"][scalar_name] 546 data["input"].cell_data.set_scalars(scalars, "color") 547 data["mesh"].mapper.lookup_table.cmap = self.COLOR_DEFINITIONS[scalar_name] 548 data["mesh"].mapper.scalar_range = value_range[0], value_range[1] 549 self.active_scalar_display = scalar_name 550 self.plotter.update()
Change displayed scalar for colors on 3D mesh.
552 def place_defect(self, ui_element=None): 553 """Place new defect according to currently entered information in UI. 554 555 Args: 556 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 557 """ 558 cells = self.selected_cells["all"] if self.selected_cells["all"] else self.selected_cells["center"] 559 subshell_ids = cells['subshell_id'] 560 vtk_ids = cells['original_id'] 561 params = self.get_new_defect_configuration(self) 562 for i in range(len(subshell_ids)): 563 new_defect = self.new_defect_to_place(vtk_ids[i], self.layer_dropdown_widget.value, **params) 564 self.placed_defects.add_defect_to_cell(vtk_ids[i], self.layer_dropdown_widget.value, new_defect) 565 self.update_defects_in_cell_display(self.selected_cells["center"]['original_id'][0]) 566 self.update_displayed_cell_state(cells) 567 self.reset_multi_select()
Place new defect according to currently entered information in UI.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
569 def remove_defects(self, ui_element=None): 570 """Remove all defects from currently selected cell. 571 572 Args: 573 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 574 """ 575 cells = self.selected_cells["all"] if self.selected_cells["all"] else self.selected_cells["center"] 576 subshell_ids = cells['subshell_id'] 577 vtk_ids = cells['original_id'] 578 for i in range(len(subshell_ids)): 579 self.placed_defects.clear_defects_of_cell(vtk_ids[i]) 580 self.update_defects_in_cell_display(self.selected_cells["center"]['original_id'][0]) 581 self.update_displayed_cell_state(cells) 582 self.reset_multi_select()
Remove all defects from currently selected cell.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
584 def update_displayed_cell_state(self, cells: pv.UnstructuredGrid): 585 """Update colors of selected cells in 3D mesh according to current defect state. 586 587 Args: 588 cells to refresh defect display information for 589 """ 590 shell_id = cells[self.SHELL_DATA_KEY][0] 591 data = self.MESH_DATA[shell_id] 592 subshell_ids = cells['subshell_id'] 593 vtk_ids = cells['original_id'] 594 defect_display_data = self.placed_defects.generate_defect_display_summary() 595 for i, vtk_id in enumerate(vtk_ids): 596 if vtk_id not in defect_display_data["cell_defect_types"]: 597 display_value = 0 598 else: 599 cell_defects = defect_display_data["cell_defect_types"][vtk_id] 600 if len(cell_defects) > 1: 601 display_value = self.MULTIPLE_DEFECTS_COLOR_ID 602 else: 603 display_value = cell_defects[0] 604 data["scalar_arrays"]["state"][subshell_ids[i]] = display_value
Update colors of selected cells in 3D mesh according to current defect state.
Args: cells to refresh defect display information for
606 def grow_selection(self, ui_element=None): 607 """Grow current selection of cells in all directions by 1 cell while respecting section borders. 608 609 Args: 610 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 611 """ 612 grow_counter = self.selected_cells["grow_counter"] + 1 613 self.perform_multiselect(grow_counter)
Grow current selection of cells in all directions by 1 cell while respecting section borders.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
615 def shrink_selection(self, ui_element=None): 616 """Shrink current selection of cells in all directions by 1 cell. 617 618 Args: 619 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 620 """ 621 grow_counter = self.selected_cells["grow_counter"] - 1 622 self.perform_multiselect(grow_counter)
Shrink current selection of cells in all directions by 1 cell.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
624 def perform_multiselect(self, grow_counter: int): 625 """Update selection area according to current multi select counter.""" 626 self.reset_multi_select(refresh=False) 627 self.selected_cells["grow_counter"] = grow_counter 628 cell = self.selected_cells["center"] 629 shell_id = cell['WebShellAssignment'][0] 630 search_id = cell['original_id'][0] 631 cell_subshell_id = cell['subshell_id'][0] 632 section_id = cell['section'][0] 633 mesh_data = self.MESH_DATA[shell_id]["input"] 634 mesh_data.cell_data.set_scalars(self.MESH_DATA[shell_id]["scalar_arrays"]["state"], "color") 635 636 cells_found = [cell_subshell_id] 637 for _ in range(grow_counter): 638 new_cells = [] 639 for cell_id in cells_found: 640 result = mesh_data.cell_neighbors(cell_id, "points") 641 extracted_cells = mesh_data.extract_cells(result) 642 section_ids = extracted_cells['section'] 643 subshell_ids = extracted_cells['subshell_id'] 644 for i, item in enumerate(section_ids): 645 if item == section_id: 646 new_cells.append(subshell_ids[i]) 647 cells_found.extend(new_cells) 648 cells_found = list(set(cells_found)) 649 650 grow_cells = mesh_data.extract_cells(cells_found) 651 self.selected_cells["all"] = grow_cells 652 for i in grow_cells['subshell_id']: 653 self.MESH_DATA[shell_id]["scalar_arrays"]["selection"][i] = self.SELECTION_COLOR_ID 654 mesh_data.cell_data.set_scalars(self.MESH_DATA[shell_id]["scalar_arrays"]["selection"], "color") 655 656 self.plotter.update()
Update selection area according to current multi select counter.
658 def reset_multi_select(self, refresh: bool=True): 659 """Reset internal storage of currently selected cells and display state of 3D mesh back to baseline. 660 661 Args: 662 refresh: Trigger PyVista re-render or not 663 """ 664 self.selected_cells["all"] = None 665 self.selected_cells["grow_counter"] = 0 666 for key, data in self.MESH_DATA.items(): 667 data["scalar_arrays"]["selection"] = copy.deepcopy(data["scalar_arrays"]["state"]) 668 if self.active_scalar_display == 'state': 669 data["input"].cell_data.set_scalars(data["scalar_arrays"]["state"], "color") 670 if refresh: 671 self.plotter.update()
Reset internal storage of currently selected cells and display state of 3D mesh back to baseline.
Args: refresh: Trigger PyVista re-render or not
673 def on_defect_type_changed(self, ui_element=None): 674 """Update displayed UI elements for defect configuration after selected defect type has changed. 675 676 Args: 677 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 678 """ 679 selected_defect_type_id = self.defect_id_widget.value 680 self.new_defect_to_place = self.available_defects[selected_defect_type_id] 681 match selected_defect_type_id: 682 case 1: # PlyWaviness 683 self.defect_param_widget = widgets.FloatText( 684 value=0, 685 description="Orientation (°):" 686 ).add_class("defect_param_input").add_class("global_basic_input") 687 self.defect_length_widget = widgets.FloatText( 688 value=0, 689 description="Length (m):" 690 ).add_class("defect_param_input").add_class("global_basic_input") 691 self.defect_amplitude_widget = widgets.FloatText( 692 value=0, 693 description="Amplitude (m):" 694 ).add_class("defect_param_input").add_class("global_basic_input") 695 output = widgets.VBox([ 696 self.defect_param_widget, 697 self.defect_length_widget, 698 self.defect_amplitude_widget 699 ]) 700 def get_param(self): 701 return { 702 "orientation": self.defect_param_widget.value, 703 "length": self.defect_length_widget.value, 704 "amplitude": self.defect_amplitude_widget.value 705 } 706 self.get_new_defect_configuration = get_param 707 case 2: # PlyMisorientation 708 self.defect_param_widget = widgets.FloatText( 709 value=0, 710 description="Offset Angle (°):" 711 ).add_class("defect_param_input").add_class("global_basic_input") 712 output = self.defect_param_widget 713 def get_param(self): 714 return { 715 "angle": self.defect_param_widget.value 716 } 717 self.get_new_defect_configuration = get_param 718 case 3: # MissingPly 719 self.defect_param_widget = None 720 output = None 721 def get_param(self): 722 return {} 723 self.get_new_defect_configuration = get_param 724 case 4: # VaryingThickness 725 self.defect_param_widget = widgets.FloatText( 726 value=0, 727 description="FVC (%):" 728 ).add_class("defect_param_input").add_class("global_basic_input") 729 output = self.defect_param_widget 730 def get_param(self): 731 return { 732 "fvc": self.defect_param_widget.value 733 } 734 self.get_new_defect_configuration = get_param 735 case _: 736 print("Unknown type of defect selected with ID {}".format(self.defect_id_widget.value)) 737 self.defect_param_output.clear_output() 738 if output: 739 with self.defect_param_output: 740 display(output)
Update displayed UI elements for defect configuration after selected defect type has changed.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
742 def get_new_defect_configuration(self) -> dict: 743 """Return all parameters of defect currently being defined. 744 745 This method is redefined everytime the selected defect type changes 746 747 Returns: 748 dictionary of all parameters currently entered in UI for defect configuration (can be empty) 749 """ 750 pass
Return all parameters of defect currently being defined.
This method is redefined everytime the selected defect type changes
Returns: dictionary of all parameters currently entered in UI for defect configuration (can be empty)
752 def load_local_data(self, ui_element=None): 753 """Use locally stored data for UI. 754 755 Args: 756 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 757 """ 758 self.conn = store_connection.LocalStoreConnection("sensotwin_world") 759 self.placed_defects = configuration.DefectInputDataSet(conn=self.conn) 760 self.load_input_set_data()
Use locally stored data for UI.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
762 def load_remote_data(self, ui_element=None): 763 """Use remotely stored data for UI. 764 765 Args: 766 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 767 """ 768 self.conn = store_connection.FusekiConnection(self.remote_database_input.value) 769 self.placed_defects = configuration.DefectInputDataSet(conn=self.conn) 770 self.load_input_set_data()
Use remotely stored data for UI.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
778 def load_input_set_data(self): 779 """Load data from previously set input source and populate UI.""" 780 self.input_set_data = configuration.DefectInputDataSet.get_all_entries_from_store(self.conn) 781 options = [] 782 if self.input_set_data: 783 for key, input_set in self.input_set_data.items(): 784 label = input_set.generate_input_set_display_label() 785 options.append((label, key)) 786 self.input_set_selection.options = options 787 self.input_set_selection.value = list(self.input_set_data.keys())[0]
Load data from previously set input source and populate UI.
789 def display_input_set(self, ui_element=None): 790 """Display data for currently selected input set in UI. 791 792 Args: 793 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 794 """ 795 self.placed_defects = self.input_set_data[self.input_set_selection.value] 796 for id, data in self.MESH_DATA.items(): 797 self.update_displayed_cell_state(data["input"]) 798 self.reset_multi_select()
Display data for currently selected input set in UI.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
800 def apply_styling(self): 801 """Apply CSS hack to notebook for better styling than native Jupyter Widget styles.""" 802 css = """ 803 <style> 804 {} 805 {} 806 </style> 807 """.format(global_style.global_css, style.local_css) 808 return widgets.HTML(css)
Apply CSS hack to notebook for better styling than native Jupyter Widget styles.