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        
class DefectPlacementUIElements:
 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

DefectPlacementUIElements()
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.

MESH_FILE_PATH = 'Inputs/input_mesh_17052024.vtu'
SHELL_DATA_KEY = 'WebShellAssignment'
SECTION_DATA_KEY = 'SectionAssignment'
MESH_DATA = {}
LAYER_FILE_PATH = 'Inputs/composite_layup_db.vtt'
LAYER_ID_KEY = 'layer_id'
LAYER_MATERIAL_KEY = 'layer_material'
LAYER_THICKNESS_KEY = 'layer_thickness_mm'
LAYER_ANGLE_KEY = 'layer_angle_deg'
dashboard
def init_global_data(self):
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.

def init_data_source_box(self) -> ipywidgets.widgets.widget_box.Box:
 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.

def init_pyvista_render(self) -> ipywidgets.widgets.widget_output.Output:
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.

def init_placement_box(self) -> ipywidgets.widgets.widget_box.Box:
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.

def cell_selected(self, cell: pyvista.core.pointset.UnstructuredGrid):
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

def update_defects_in_cell_display(self, cell_id: int):
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.

def update_layers_in_cell_display(self, cell_id: int):
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.

def show_mesh(self, _, selected_meshes: list = []):
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

def create_display_mesh_data(self) -> dict:
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

def layer_selected(self, change: traitlets.utils.bunch.Bunch):
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.

def init_cell_layer_rendering(self) -> matplotlib.figure.Figure:
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.

def render_cell_layer_structure(self, layers: pandas.core.frame.DataFrame):
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

def create_scalar_color_definitions(self) -> dict:
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

def change_scalar_display_mode(self, _, scalar_name: str = None):
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.

def place_defect(self, ui_element=None):
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

def remove_defects(self, ui_element=None):
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

def update_displayed_cell_state(self, cells: pyvista.core.pointset.UnstructuredGrid):
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

def grow_selection(self, ui_element=None):
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

def shrink_selection(self, ui_element=None):
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

def perform_multiselect(self, grow_counter: int):
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.

def reset_multi_select(self, refresh: bool = True):
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

def on_defect_type_changed(self, ui_element=None):
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

def get_new_defect_configuration(self) -> dict:
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)

def load_local_data(self, ui_element=None):
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

def load_remote_data(self, ui_element=None):
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

def save_new_input_set(self, ui_element=None):
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()
def load_input_set_data(self):
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.

def display_input_set(self, ui_element=None):
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

def apply_styling(self):
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.