sensotwin.lifetime_extrapolation.ui
1import ipywidgets as widgets 2from . import configuration 3from . import style 4from .. import global_style 5from .. import store_connection 6from ..defect_placement import configuration as defect_config 7from ..stress_selection import configuration as stress_config 8from ..temperature_selection import configuration as temperature_config 9import pyvista as pv 10import numpy as np 11import matplotlib.pyplot as plt 12import matplotlib.colors as pltcolor 13from IPython.display import display 14import copy 15from traitlets.utils.bunch import Bunch 16from vtkmodules.vtkRenderingCore import vtkActor2D 17import math 18 19 20def create_d_cmap(): 21 """Create matplotlib color map for d plot in result. 22 23 Returns: 24 matplotlib Colormap 25 """ 26 base_cmap = plt.get_cmap('jet') 27 damage_cmap = pltcolor.LinearSegmentedColormap.from_list( 28 'trunc({n},{a:.2f},{b:.2f})'.format(n=base_cmap.name, a=0.30, b=1.0), 29 base_cmap(np.linspace(0.30, 1.0, 100))) 30 return damage_cmap 31 32def create_t_cmap(): 33 """Create matplotlib color map for t plot in result. 34 35 Returns: 36 matplotlib Colormap 37 """ 38 base_cmap = plt.get_cmap('jet') 39 damage_cmap = pltcolor.LinearSegmentedColormap.from_list( 40 'trunc({n},{a:.2f},{b:.2f})'.format(n=base_cmap.name, a=0.30, b=1.0), 41 base_cmap(np.linspace(0.30, 1.0, 100))) 42 return "jet" 43 44 45class LifetimeExtrapolationUIElements: 46 """Contains all UI functionality for executing simulation and viewing results (Step 4).""" 47 LAYER_ID_KEY = "LAYID" 48 LAYER_THICKNESS_KEY = 'THICKMM' 49 POINT_WARP_KEY = "U_AEROCENT" 50 CURING_MESH_ID = 0 51 STRUCTURE_MESH_ID = 1 52 53 STRUCTURE_DISPLAY_DEFINITIONS = { 54 "DFAT": { 55 "clim": [0.000000001, 1], 56 "cmap": create_d_cmap(), 57 "log_scale": True 58 }, 59 "TFAT": { 60 "clim": [0.001, 100], 61 "cmap": create_t_cmap(), 62 "log_scale": True 63 }, 64 "THICKNESS": { 65 "cmap": "terrain", 66 "log_scale": False 67 } 68 } 69 70 structure_limit_actor = None 71 curing_limit_actor = None 72 73 def __init__(self): 74 """Initialize UI objects and associated UI functionality.""" 75 self.init_input_set_selection_UI() 76 self.init_job_overview_UI() 77 self.init_result_display_UI() 78 79 def init_input_set_selection_UI(self): 80 """Initialize first UI of notebook, choosing data source and selecting input data sets.""" 81 temperature_input_selection_label = widgets.Label(value="Material Hardening Input:").add_class("global_headline") 82 self.temperature_input_selection = widgets.Select( 83 options=['No data in source'], 84 value='No data in source', 85 rows=10, 86 description='Input sets:', 87 disabled=False 88 ).add_class("global_input_set_selection").add_class("global_basic_input") 89 defect_input_selection_label = widgets.Label(value="Defect Placement Input Set:").add_class("global_headline") 90 self.defect_input_selection = widgets.Select( 91 options=['No data in source'], 92 value='No data in source', 93 rows=10, 94 description='Input sets:', 95 disabled=False 96 ).add_class("global_input_set_selection").add_class("global_basic_input") 97 stress_input_selection_label = widgets.Label(value="Stress/Strain Input Set:").add_class("global_headline") 98 self.stress_input_selection = widgets.Select( 99 options=['No data in source'], 100 value='No data in source', 101 rows=10, 102 description='Input sets:', 103 disabled=False 104 ).add_class("global_input_set_selection").add_class("global_basic_input") 105 start_simulation_button = widgets.Button( 106 description='Start simulation with selected input data sets', 107 disabled=False, 108 tooltip='Start simulation with selected input data sets', 109 icon='play', 110 ).add_class("global_save_input_set_button").add_class("global_basic_button") 111 start_simulation_button.on_click(self.start_simulation) 112 113 simulation_temperature_selection = widgets.VBox([ 114 temperature_input_selection_label, 115 self.temperature_input_selection 116 ]).add_class("life_extra_input_column_container") 117 simulation_defect_placement = widgets.VBox([ 118 defect_input_selection_label, 119 self.defect_input_selection 120 ]).add_class("life_extra_input_column_container") 121 simulation_stress_selection = widgets.VBox([ 122 stress_input_selection_label, 123 self.stress_input_selection 124 ]).add_class("life_extra_input_column_container") 125 simulation_box = widgets.VBox([ 126 widgets.HBox([ 127 simulation_temperature_selection, 128 simulation_defect_placement, 129 simulation_stress_selection 130 ]).add_class("life_extra_input_row_container"), 131 start_simulation_button 132 ]) 133 134 local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline") 135 use_local_data_button = widgets.Button( 136 description='Use local database', 137 disabled=False, 138 tooltip='Use local database', 139 icon='play', 140 ).add_class("global_load_data_button").add_class("global_basic_button") 141 use_local_data_button.on_click(self.load_local_data) 142 remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline") 143 self.remote_database_input = widgets.Text( 144 value=None, 145 placeholder="insert SPARQL url here", 146 description="SPARQL Endpoint:", 147 disabled=False 148 ).add_class("global_url_input").add_class("global_basic_input") 149 use_remote_data_button = widgets.Button( 150 description='Use remote database', 151 disabled=False, 152 tooltip='Use remote database', 153 icon='play', 154 ).add_class("global_load_data_button").add_class("global_basic_button") 155 use_remote_data_button.on_click(self.load_remote_data) 156 local_data_box = widgets.VBox([ 157 local_database_label, 158 use_local_data_button 159 ]) 160 remote_data_box = widgets.VBox([ 161 remote_database_label, 162 self.remote_database_input, 163 use_remote_data_button 164 ]) 165 data_source_box = widgets.VBox([ 166 local_data_box, 167 remote_data_box 168 ]).add_class("global_data_tab_container") 169 170 self.input_dashboard = widgets.Tab().add_class("global_tab_container") 171 self.input_dashboard.children = [ 172 data_source_box, 173 simulation_box 174 ] 175 tab_titles = ['Input', 'Simulation'] 176 for i in range(len(tab_titles)): 177 self.input_dashboard.set_title(i, tab_titles[i]) 178 179 def start_simulation(self, ui_element=None): 180 """Start simulation with the 3 currently selected input sets. 181 182 Args: 183 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 184 """ 185 configuration.execute_job( 186 self.pyiron_project, 187 self.temperature_input_set_data[self.temperature_input_selection.value], 188 self.defect_input_set_data[self.defect_input_selection.value], 189 self.stress_input_set_data[self.stress_input_selection.value], 190 self.conn 191 ) 192 193 def init_job_overview_UI(self): 194 """Initialize second UI of notebook, viewing the pyiron job table.""" 195 self.pyiron_project = configuration.get_pyiron_project("jobs_test") 196 self.job_dashboard = widgets.Output(layout={'border': '1px solid black'}) 197 self.update_job_list_display() 198 199 def update_job_list_display(self): 200 """Reload job table from pyiron project.""" 201 if self.job_dashboard: 202 self.job_dashboard.clear_output() 203 columns_to_hide = ["projectpath","chemicalformula","parentid","masterid","hamversion"] 204 with self.job_dashboard: 205 display(configuration.get_job_table(self.pyiron_project).drop(columns=columns_to_hide)) 206 207 def init_result_display_UI(self): 208 """Initialize third UI of notebook, displaying result of a simulation.""" 209 result_selection_label = widgets.Label(value="Completed simulation:").add_class("global_headline") 210 self.result_selection = widgets.Select( 211 options=['Load data first'], 212 value='Load data first', 213 rows=10, 214 description='Input sets:', 215 disabled=False 216 ).add_class("global_input_set_selection").add_class("global_basic_input") 217 display_result_button = widgets.Button( 218 description='Display Result', 219 disabled=False, 220 tooltip='Display Result', 221 icon='play', 222 ).add_class("global_basic_button") 223 display_result_button.on_click(self.display_result) 224 result_display_selection_box = widgets.VBox([ 225 result_selection_label, 226 self.result_selection, 227 display_result_button 228 ]) 229 230 structure_render_label = widgets.Label(value="Structure simulation:").add_class("global_headline") 231 self.structure_deformation_toggle = widgets.ToggleButton( 232 value=False, 233 description='Toggle Deformation', 234 tooltip='Toggle Deformation', 235 ).add_class("global_basic_button") 236 self.structure_deformation_toggle.observe(self.on_structure_deformation_changed, names=['value']) 237 self.structure_layer_select = widgets.Dropdown( 238 options=['Load Result'], 239 value='Load Result', 240 description='Layer:', 241 disabled=False, 242 ).add_class("global_basic_input") 243 self.structure_layer_select.observe(self.on_structure_layer_changed, names=['value']) 244 self.structure_scalar_select = widgets.Dropdown( 245 options=['Select Layer'], 246 value='Select Layer', 247 description='Value:', 248 disabled=False, 249 ).add_class("global_basic_input") 250 self.structure_scalar_select.observe(self.on_structure_scalar_changed, names=['value']) 251 self.structure_cell_info = widgets.HTML().add_class("life_extra_cell_data_column_container") 252 253 curing_render_label = widgets.Label(value="Curing simulation:").add_class("global_headline") 254 self.curing_deformation_toggle = widgets.ToggleButton( 255 value=False, 256 description='Toggle Deformation', 257 tooltip='Toggle Deformation', 258 ).add_class("global_basic_button") 259 self.curing_deformation_toggle.observe(self.on_curing_deformation_changed, names=['value']) 260 self.curing_layer_select = widgets.Dropdown( 261 options=['Load Result'], 262 value='Load Result', 263 description='Layer:', 264 disabled=False, 265 ).add_class("global_basic_input") 266 self.curing_layer_select.observe(self.on_curing_layer_changed, names=['value']) 267 self.curing_scalar_select = widgets.Dropdown( 268 options=['Select Layer'], 269 value='Select Layer', 270 description='Value:', 271 disabled=False, 272 ).add_class("global_basic_input") 273 self.curing_scalar_select.observe(self.on_curing_scalar_changed, names=['value']) 274 self.curing_cell_info = widgets.HTML().add_class("life_extra_cell_data_column_container") 275 276 result_display_controls = widgets.VBox([ 277 widgets.VBox([ 278 curing_render_label, 279 self.curing_deformation_toggle, 280 self.curing_layer_select, 281 self.curing_scalar_select 282 ]).add_class("life_extra_output_column_container"), 283 widgets.VBox([ 284 structure_render_label, 285 self.structure_deformation_toggle, 286 self.structure_layer_select, 287 self.structure_scalar_select 288 ]).add_class("life_extra_output_column_container") 289 ]).add_class("life_extra_output_column_container") 290 291 PYVISTA_OUTPUT_RENDER_HEIGHT = 1100 292 PYVISTA_OUTPUT_RENDER_WIDTH = 900 293 pyvista_render_widget = widgets.Output(layout={'height': '{}px'.format(PYVISTA_OUTPUT_RENDER_HEIGHT+15), 294 'width': '{}px'.format(PYVISTA_OUTPUT_RENDER_WIDTH+10)}) 295 296 result_cell_info_box = widgets.VBox([ 297 self.curing_cell_info, 298 self.structure_cell_info 299 ]).add_class("life_extra_output_column_container") 300 result_display_box = widgets.HBox([ 301 widgets.VBox([ 302 pyvista_render_widget 303 ]), 304 result_display_controls, 305 result_cell_info_box 306 ]) 307 308 self.plotter = pv.Plotter(shape=(2,1)) 309 self.plotter.window_size = [PYVISTA_OUTPUT_RENDER_WIDTH, PYVISTA_OUTPUT_RENDER_HEIGHT] 310 self.plotter.enable_element_picking(callback=self.selection_callback, show_message=False) 311 with pyvista_render_widget: 312 self.plotter.show(jupyter_backend='trame') 313 314 self.result_dashboard = widgets.Tab().add_class("global_tab_container") 315 self.result_dashboard.children = [ 316 result_display_selection_box, 317 result_display_box 318 ] 319 tab_titles = ['Select Result', 'Result Display'] 320 for i in range(len(tab_titles)): 321 self.result_dashboard.set_title(i, tab_titles[i]) 322 323 def update_result_list_display(self): 324 """Update list of completed jobs in result display selection list.""" 325 if self.result_dashboard: 326 options = [] 327 for _, row in configuration.get_job_table(self.pyiron_project).iterrows(): 328 if row["hamilton"] == "StructureSimulation" and row["status"] == "finished": 329 options.append((row["job"], row["id"])) 330 if options: 331 self.result_selection.options = options 332 self.result_selection.value = options[0][1] 333 334 def display_result(self, ui_element=None): 335 """Load currently selected result from list of finished jobs. 336 337 Args: 338 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 339 """ 340 all_jobs = configuration.get_job_table(self.pyiron_project) 341 result = all_jobs.loc[all_jobs.id == self.result_selection.value].iloc[0] 342 result_job = self.pyiron_project.load(result["job"]) 343 self.init_pyvista_render(result_job.output.curing_mesh,result_job.output.mesh) 344 345 def init_pyvista_render(self, curing_mesh, structure_mesh): 346 """Load result mesh from job into PyVista display. 347 348 Args: 349 structure_mesh: Relative file path to result mesh 350 """ 351 initial_camera_position = [(-30.765771120379302, -28.608772602676154, 39.46235706090557), 352 (0.9572034500000001, 0.0005481500000000805, -30.4), 353 (-0.16976192468426815, -0.8825527410211623, -0.43849919982085056)] 354 355 # render will be triggered by the scalar widget change event when setting new options 356 self.set_plotter_to_curing() 357 self.curing_mesh_data = self.create_curing_mesh_data(curing_mesh) 358 self.plotter.camera_position = initial_camera_position 359 self.relevant_curing_cell_fields = self.get_relevant_curing_point_fields() 360 self.update_available_curing_layers() 361 362 # render will be triggered by the scalar widget change event when setting new options 363 self.set_plotter_to_structure() 364 self.structure_mesh_data = self.create_structure_mesh_data(structure_mesh) 365 self.plotter.camera_position = initial_camera_position 366 self.relevant_structure_cell_fields = self.get_relevant_structure_cell_fields() 367 self.update_available_structure_layers() 368 369 self.plotter.update() 370 371 def create_structure_mesh_data(self, mesh_path: str) -> dict: 372 """Read structure 3D mesh and split into information necessary for display purposes. 373 374 Args: 375 mesh_path: Path to vtu file on file system 376 377 Returns: 378 dictionary containing parsed mesh data for given file with placeholders for display PyVista actors 379 """ 380 mesh = self.reduce_cell_points(pv.read(mesh_path).cast_to_unstructured_grid(), self.STRUCTURE_MESH_ID) 381 layers = np.unique(mesh[self.LAYER_ID_KEY]) 382 mesh_data = {} 383 for layer_id in layers: 384 input_mesh = mesh.remove_cells(np.argwhere(mesh[self.LAYER_ID_KEY] != layer_id)) 385 input_mesh['color'] = [0 for x in range(len(input_mesh[self.LAYER_THICKNESS_KEY]))] 386 mesh_data[layer_id] = { 387 "input": input_mesh, 388 "warped": input_mesh.warp_by_vector(vectors=self.POINT_WARP_KEY), 389 "scalar_bar_name": None, 390 "output": None 391 } 392 return mesh_data 393 394 def create_curing_mesh_data(self, mesh_path: str) -> dict: 395 """Read structure 3D mesh and split into information necessary for display purposes. 396 397 Args: 398 mesh_path: Path to vtu file on file system 399 400 Returns: 401 dictionary containing parsed mesh data for given file with placeholders for display PyVista actors 402 """ 403 mesh = self.reduce_cell_points(pv.read(mesh_path).cast_to_unstructured_grid(), self.CURING_MESH_ID) 404 layers = np.unique(mesh[self.LAYER_ID_KEY]) 405 mesh_data = {} 406 for layer_id in layers: 407 input_mesh = mesh.remove_cells(np.argwhere(mesh[self.LAYER_ID_KEY] != layer_id)) 408 input_mesh['color'] = [0 for x in range(len(input_mesh[self.LAYER_THICKNESS_KEY]))] 409 mesh_data[layer_id] = { 410 "input": input_mesh, 411 "warped": input_mesh.warp_by_vector(vectors=self.POINT_WARP_KEY), 412 "scalar_bar_name": None, 413 "output": None 414 } 415 return mesh_data 416 417 def reduce_cell_points(self, mesh: pv.UnstructuredGrid, key: int) -> pv.UnstructuredGrid: 418 """Reduces 3D cells of structure simulation result to 2D polygons for display purposes. 419 420 Args: 421 mesh: PyVista mesh read directly from simulation result file 422 key: key to add to cells for later identification of meshes 423 424 Returns: 425 3D mesh with same cell layout and cell data content as input, but reduced in dimension (20 to 4 points per cell) 426 """ 427 full_cell_data = mesh.cell_data 428 full_point_data = mesh.point_data 429 cell_ids = mesh.cells 430 cell_ids = np.split(cell_ids, len(cell_ids) / 21) 431 432 new_points = [] 433 new_ids = [] 434 point_scalar_slice = [] 435 for cell_id in range(mesh.n_cells): 436 cell = mesh.get_cell(cell_id) 437 new_ids.append(4) 438 for i, point in enumerate(cell.points[:4]): 439 new_points.append(point) 440 point_scalar_slice.append(cell_ids[cell_id][i+1]) 441 new_ids.append(len(new_points) - 1) 442 443 point_scalars = full_point_data[self.POINT_WARP_KEY][point_scalar_slice] 444 445 reduced_mesh = pv.UnstructuredGrid(new_ids, [pv.CellType.POLYGON for _ in range(mesh.n_cells)], new_points) 446 for prop in full_cell_data: 447 reduced_mesh[prop] = full_cell_data[prop] 448 reduced_mesh["mesh_id"] = [key] * len(cell_ids) 449 reduced_mesh.point_data[self.POINT_WARP_KEY] = point_scalars 450 reduced_mesh.point_data["{}_total".format(self.POINT_WARP_KEY)] = [math.sqrt(x*x+y*y+z*z) for [x,y,z] in point_scalars] 451 for i in range(len(point_scalars[0])): 452 reduced_mesh.point_data["{}_{}".format(self.POINT_WARP_KEY, i)] = [x[i] for x in point_scalars] 453 454 return reduced_mesh 455 456 def get_relevant_structure_cell_fields(self) -> list: 457 """Get all fields that should ne filter-able for structure mesh. 458 459 Returns: 460 list of string names of scalar fields that should be toggleable for display 461 """ 462 relevant_cells = ["THICKMM"] 463 for x in self.structure_mesh_data[list(self.structure_mesh_data.keys())[0]]["input"].cell_data: 464 if x.startswith("DFAT_") or x.startswith("TFAT_"): 465 relevant_cells.append(x) 466 return relevant_cells 467 468 def get_relevant_curing_point_fields(self) -> list: 469 """Get all fields that should ne filter-able for curing mesh. 470 471 Returns: 472 list of string names of scalar fields that should be toggleable for display 473 """ 474 relevant_cells = [] 475 for x in self.curing_mesh_data[list(self.curing_mesh_data.keys())[0]]["input"].point_data: 476 if x.startswith("{}_".format(self.POINT_WARP_KEY)): 477 relevant_cells.append(x) 478 return relevant_cells 479 480 def update_available_structure_layers(self): 481 """Update available layers for display in structure dropdown. 482 483 This causes an update in PyVista rendering by triggering the onchange event of 484 the layer select widget. 485 """ 486 self.structure_layer_select.options = self.structure_mesh_data.keys() 487 self.structure_layer_select.value = list(self.structure_mesh_data.keys())[0] 488 489 def update_available_curing_layers(self): 490 """Update available layers for display in curing dropdown. 491 492 This causes an update in PyVista rendering by triggering the onchange event of 493 the layer select widget. 494 """ 495 self.curing_layer_select.options = self.curing_mesh_data.keys() 496 self.curing_layer_select.value = list(self.curing_mesh_data.keys())[0] 497 498 def update_available_structure_layer_scalars(self, layer_id: int): 499 """Update scalar select with available scalars for currently displayed layer.""" 500 options = [] 501 for field in self.relevant_structure_cell_fields: 502 if not np.isnan(self.structure_mesh_data[layer_id]["input"][field]).all(): 503 options.append(field) 504 if len(options) == 0: 505 options.append("No layers with valid values") 506 self.structure_scalar_select.options = options 507 self.structure_scalar_select.value = options[0] 508 509 def update_available_curing_layer_scalars(self, layer_id: int): 510 """Update scalar select with available scalars for currently displayed layer.""" 511 options = [] 512 for field in self.relevant_curing_cell_fields: 513 if not np.isnan(self.curing_mesh_data[layer_id]["input"][field]).all(): 514 options.append(field) 515 if len(options) == 0: 516 options.append("No layers with valid values") 517 self.curing_scalar_select.options = options 518 self.curing_scalar_select.value = options[0] 519 520 def on_structure_layer_changed(self, value: Bunch): 521 """Event to update UI after selected structure layer to display has changed.""" 522 self.update_available_structure_layer_scalars(value["new"]) 523 self.update_displayed_structure_scalar(self.structure_scalar_select.value) 524 525 def on_curing_layer_changed(self, value: Bunch): 526 """Event to update UI after selected curing layer to display has changed.""" 527 self.update_available_curing_layer_scalars(value["new"]) 528 self.update_displayed_curing_scalar(self.curing_scalar_select.value) 529 530 def on_structure_scalar_changed(self, value: Bunch): 531 """Event to update UI after selected structure scalar to display has changed.""" 532 self.update_displayed_structure_scalar(value["new"]) 533 534 def on_curing_scalar_changed(self, value: Bunch): 535 """Event to update UI after selected curing scalar to display has changed.""" 536 self.update_displayed_curing_scalar(value["new"]) 537 538 def on_structure_deformation_changed(self, value: Bunch): 539 """Event to update UI after value of deformation display has changed.""" 540 self.update_displayed_structure_scalar(self.structure_scalar_select.value) 541 542 def on_curing_deformation_changed(self, value: Bunch): 543 """Event to update UI after value of deformation display has changed.""" 544 self.update_displayed_curing_scalar(self.curing_scalar_select.value) 545 546 def update_displayed_structure_scalar(self, value: str): 547 """Update PyVista structure render. 548 549 Args: 550 value: Scalar name to display on 3D mesh 551 """ 552 self.set_plotter_to_structure() 553 if self.structure_deformation_toggle.value == True: 554 mesh_name = "warped" 555 else: 556 mesh_name = "input" 557 for layer_id, structure_layer in self.structure_mesh_data.items(): 558 if structure_layer["output"] != None: 559 self.plotter.remove_actor(structure_layer["output"]) 560 # scalars bars with multiplots have unpredictable behaviour, do not this check 561 if structure_layer["scalar_bar_name"] in self.plotter.scalar_bars: 562 self.plotter.remove_scalar_bar(structure_layer["scalar_bar_name"]) 563 structure_layer["scalar_bar_name"] = None 564 structure_layer["output"] = None 565 if self.structure_limit_actor: 566 self.plotter.remove_actor(self.structure_limit_actor) 567 self.structure_limit_actor = None 568 self.structure_cell_info.value = "" 569 570 data = self.structure_mesh_data[self.structure_layer_select.value] 571 data[mesh_name].cell_data.set_scalars(data[mesh_name][value], "color") 572 if value.startswith("DFAT_"): 573 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["cmap"] 574 clim = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["clim"] 575 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["log_scale"] 576 title = "DFAT Layer {}".format(self.structure_layer_select.value) 577 self.structure_limit_actor = self.show_limit_value(data[mesh_name], value, limit="max", geometry_type="cell") 578 elif value.startswith("TFAT"): 579 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["cmap"] 580 clim = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["clim"] 581 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["log_scale"] 582 title = "TFAT Layer {}".format(self.structure_layer_select.value) 583 self.structure_limit_actor = self.show_limit_value(data[mesh_name], value, limit="min", geometry_type="cell") 584 elif value.startswith(self.LAYER_THICKNESS_KEY): 585 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["THICKNESS"]["cmap"] 586 clim = np.nanmin(data[mesh_name][self.LAYER_THICKNESS_KEY]), np.max(data[mesh_name][self.LAYER_THICKNESS_KEY]) 587 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["THICKNESS"]["log_scale"] 588 title = "Thickness(mm) Layer {}".format(self.structure_layer_select.value) 589 else: 590 raise ValueError("No colorscheme for chosen scalar") 591 592 data["output"] = self.plotter.add_mesh(data[mesh_name], show_edges=False, lighting=False, show_scalar_bar=True, scalars="color", 593 cmap=cmap, clim=clim, log_scale=log_scale, scalar_bar_args={'title': title}) 594 data["scalar_bar_name"] = title 595 self.plotter.update() 596 597 def update_displayed_curing_scalar(self, value: str): 598 """Update PyVista curing render. 599 600 Args: 601 value: Scalar name to display on 3D mesh 602 """ 603 self.set_plotter_to_curing() 604 if self.curing_deformation_toggle.value == True: 605 mesh_name = "warped" 606 else: 607 mesh_name = "input" 608 for layer_id, curing_layer in self.curing_mesh_data.items(): 609 if curing_layer["output"] != None: 610 self.plotter.remove_actor(curing_layer["output"]) 611 # scalars bars with multiplots have unpredictable behaviour, do not this check 612 if curing_layer["scalar_bar_name"] in self.plotter.scalar_bars: 613 self.plotter.remove_scalar_bar(curing_layer["scalar_bar_name"]) 614 curing_layer["scalar_bar_name"] = None 615 curing_layer["output"] = None 616 if self.curing_limit_actor: 617 self.plotter.remove_actor(self.curing_limit_actor) 618 self.curing_limit_actor = None 619 self.curing_cell_info.value = "" 620 621 data = self.curing_mesh_data[self.curing_layer_select.value] 622 if value.startswith(self.POINT_WARP_KEY): 623 cmap = 'jet' 624 title = value 625 self.curing_limit_actor = self.show_limit_value(data[mesh_name], value, limit="max", geometry_type="point") 626 else: 627 raise ValueError("No colorscheme for chosen scalar") 628 data["output"] = self.plotter.add_mesh(data[mesh_name], show_edges=False, lighting=False, show_scalar_bar=True, scalars=value, 629 cmap=cmap, scalar_bar_args={'title': title}) 630 data["scalar_bar_name"] = title 631 self.plotter.update() 632 633 def show_limit_value(self, mesh: pv.UnstructuredGrid, scalar: str, limit: str='max', geometry_type: str='cell') -> vtkActor2D: 634 """Render point with tooltip at place of maximum value in PyVista. 635 636 Args: 637 mesh: 3D mesh with cells to check for values 638 scalar: Name of scalar to check for values 639 limit: 'max' or 'min' 640 641 Returns: 642 vtk actor of added max value point 643 """ 644 if limit == 'max': 645 limit_value = np.nanmax(mesh[scalar]) 646 fmt = "Max: %.5f" 647 else: 648 limit_value = np.nanmin(mesh[scalar]) 649 fmt = "Min: %.5f" 650 651 if geometry_type == "cell": 652 limit_cell = mesh.remove_cells(np.argwhere(mesh[scalar] != limit_value)) 653 point = limit_cell.points[0] 654 scalar_value = [limit_cell[scalar]] 655 elif geometry_type == "point": 656 limit_cells = mesh.extract_points(np.argwhere(mesh[scalar] == limit_value)) 657 point = limit_cells.points[0] 658 scalar_value = [limit_cells[scalar][0]] 659 660 return self.plotter.add_point_scalar_labels(point, scalar_value, name="{}_limit_value_point".format(geometry_type), fmt=fmt, point_size=20, shape="rounded_rect", 661 point_color="red", render_points_as_spheres=True, fill_shape=True, shape_color="pink", text_color="red",shadow=True, tolerance=1) 662 663 def selection_callback(self, cell: pv.Cell): 664 """Event to display detail information for selected cell.""" 665 display_html = '<p class="global_headline"> Selected cell: </p>' 666 if cell["mesh_id"] == self.CURING_MESH_ID: 667 for prop in cell.point_data: 668 if not np.isnan(cell[prop].any()): 669 display_html += "<b>{property_name}</b>: {property_value} \n".format(property_name=prop, property_value=cell[prop][0]) 670 self.curing_cell_info.value = display_html 671 else: 672 for prop in cell.cell_data: 673 if not np.isnan(cell[prop]): 674 display_html += "<b>{property_name}</b>: {property_value} \n".format(property_name=prop, property_value=cell[prop][0]) 675 self.structure_cell_info.value = display_html 676 677 def set_plotter_to_curing(self): 678 """Set active plot to curing simulation, redirecting call to plotter.""" 679 self.plotter.subplot(0,0) 680 681 def set_plotter_to_structure(self): 682 """Set active plot to structure simulation, redirecting call to plotter.""" 683 self.plotter.subplot(1,0) 684 685 def load_local_data(self, ui_element=None): 686 """Use locally stored data for UI. 687 688 Args: 689 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 690 """ 691 self.conn = store_connection.LocalStoreConnection("sensotwin_world") 692 self.load_input_set_data() 693 694 def load_remote_data(self, ui_element=None): 695 """Use remotely stored data for UI. 696 697 Args: 698 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 699 """ 700 self.conn = store_connection.FusekiConnection(self.remote_database_input.value) 701 self.load_input_set_data() 702 703 def load_input_set_data(self): 704 """Load data from previously set input source and populate UI.""" 705 self.temperature_input_set_data = temperature_config.MaterialHardeningInputDataSet.get_all_entries_from_store(self.conn) 706 if self.temperature_input_set_data: 707 options = [] 708 for key, input_set in self.temperature_input_set_data.items(): 709 label = input_set.generate_input_set_display_label() 710 options.append((label, key)) 711 self.temperature_input_selection.options = options 712 self.temperature_input_selection.value = list(self.temperature_input_set_data.keys())[0] 713 714 self.defect_input_set_data = defect_config.DefectInputDataSet.get_all_entries_from_store(self.conn) 715 if self.defect_input_set_data: 716 options = [] 717 for key, input_set in self.defect_input_set_data.items(): 718 label = input_set.generate_input_set_display_label() 719 options.append((label, key)) 720 self.defect_input_selection.options = options 721 self.defect_input_selection.value = list(self.defect_input_set_data.keys())[0] 722 723 self.stress_input_set_data = stress_config.StressStrainInputDataSet.get_all_entries_from_store(self.conn) 724 if self.stress_input_set_data: 725 options = [] 726 for key, input_set in self.stress_input_set_data.items(): 727 label = input_set.generate_input_set_display_label() 728 options.append((label, key)) 729 self.stress_input_selection.options = options 730 self.stress_input_selection.value = list(self.stress_input_set_data.keys())[0] 731 732 def apply_styling(self): 733 css = """ 734 <style> 735 {} 736 {} 737 </style> 738 """.format(global_style.global_css, style.local_css) 739 return widgets.HTML(css)
21def create_d_cmap(): 22 """Create matplotlib color map for d plot in result. 23 24 Returns: 25 matplotlib Colormap 26 """ 27 base_cmap = plt.get_cmap('jet') 28 damage_cmap = pltcolor.LinearSegmentedColormap.from_list( 29 'trunc({n},{a:.2f},{b:.2f})'.format(n=base_cmap.name, a=0.30, b=1.0), 30 base_cmap(np.linspace(0.30, 1.0, 100))) 31 return damage_cmap
Create matplotlib color map for d plot in result.
Returns: matplotlib Colormap
33def create_t_cmap(): 34 """Create matplotlib color map for t plot in result. 35 36 Returns: 37 matplotlib Colormap 38 """ 39 base_cmap = plt.get_cmap('jet') 40 damage_cmap = pltcolor.LinearSegmentedColormap.from_list( 41 'trunc({n},{a:.2f},{b:.2f})'.format(n=base_cmap.name, a=0.30, b=1.0), 42 base_cmap(np.linspace(0.30, 1.0, 100))) 43 return "jet"
Create matplotlib color map for t plot in result.
Returns: matplotlib Colormap
46class LifetimeExtrapolationUIElements: 47 """Contains all UI functionality for executing simulation and viewing results (Step 4).""" 48 LAYER_ID_KEY = "LAYID" 49 LAYER_THICKNESS_KEY = 'THICKMM' 50 POINT_WARP_KEY = "U_AEROCENT" 51 CURING_MESH_ID = 0 52 STRUCTURE_MESH_ID = 1 53 54 STRUCTURE_DISPLAY_DEFINITIONS = { 55 "DFAT": { 56 "clim": [0.000000001, 1], 57 "cmap": create_d_cmap(), 58 "log_scale": True 59 }, 60 "TFAT": { 61 "clim": [0.001, 100], 62 "cmap": create_t_cmap(), 63 "log_scale": True 64 }, 65 "THICKNESS": { 66 "cmap": "terrain", 67 "log_scale": False 68 } 69 } 70 71 structure_limit_actor = None 72 curing_limit_actor = None 73 74 def __init__(self): 75 """Initialize UI objects and associated UI functionality.""" 76 self.init_input_set_selection_UI() 77 self.init_job_overview_UI() 78 self.init_result_display_UI() 79 80 def init_input_set_selection_UI(self): 81 """Initialize first UI of notebook, choosing data source and selecting input data sets.""" 82 temperature_input_selection_label = widgets.Label(value="Material Hardening Input:").add_class("global_headline") 83 self.temperature_input_selection = widgets.Select( 84 options=['No data in source'], 85 value='No data in source', 86 rows=10, 87 description='Input sets:', 88 disabled=False 89 ).add_class("global_input_set_selection").add_class("global_basic_input") 90 defect_input_selection_label = widgets.Label(value="Defect Placement Input Set:").add_class("global_headline") 91 self.defect_input_selection = widgets.Select( 92 options=['No data in source'], 93 value='No data in source', 94 rows=10, 95 description='Input sets:', 96 disabled=False 97 ).add_class("global_input_set_selection").add_class("global_basic_input") 98 stress_input_selection_label = widgets.Label(value="Stress/Strain Input Set:").add_class("global_headline") 99 self.stress_input_selection = widgets.Select( 100 options=['No data in source'], 101 value='No data in source', 102 rows=10, 103 description='Input sets:', 104 disabled=False 105 ).add_class("global_input_set_selection").add_class("global_basic_input") 106 start_simulation_button = widgets.Button( 107 description='Start simulation with selected input data sets', 108 disabled=False, 109 tooltip='Start simulation with selected input data sets', 110 icon='play', 111 ).add_class("global_save_input_set_button").add_class("global_basic_button") 112 start_simulation_button.on_click(self.start_simulation) 113 114 simulation_temperature_selection = widgets.VBox([ 115 temperature_input_selection_label, 116 self.temperature_input_selection 117 ]).add_class("life_extra_input_column_container") 118 simulation_defect_placement = widgets.VBox([ 119 defect_input_selection_label, 120 self.defect_input_selection 121 ]).add_class("life_extra_input_column_container") 122 simulation_stress_selection = widgets.VBox([ 123 stress_input_selection_label, 124 self.stress_input_selection 125 ]).add_class("life_extra_input_column_container") 126 simulation_box = widgets.VBox([ 127 widgets.HBox([ 128 simulation_temperature_selection, 129 simulation_defect_placement, 130 simulation_stress_selection 131 ]).add_class("life_extra_input_row_container"), 132 start_simulation_button 133 ]) 134 135 local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline") 136 use_local_data_button = widgets.Button( 137 description='Use local database', 138 disabled=False, 139 tooltip='Use local database', 140 icon='play', 141 ).add_class("global_load_data_button").add_class("global_basic_button") 142 use_local_data_button.on_click(self.load_local_data) 143 remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline") 144 self.remote_database_input = widgets.Text( 145 value=None, 146 placeholder="insert SPARQL url here", 147 description="SPARQL Endpoint:", 148 disabled=False 149 ).add_class("global_url_input").add_class("global_basic_input") 150 use_remote_data_button = widgets.Button( 151 description='Use remote database', 152 disabled=False, 153 tooltip='Use remote database', 154 icon='play', 155 ).add_class("global_load_data_button").add_class("global_basic_button") 156 use_remote_data_button.on_click(self.load_remote_data) 157 local_data_box = widgets.VBox([ 158 local_database_label, 159 use_local_data_button 160 ]) 161 remote_data_box = widgets.VBox([ 162 remote_database_label, 163 self.remote_database_input, 164 use_remote_data_button 165 ]) 166 data_source_box = widgets.VBox([ 167 local_data_box, 168 remote_data_box 169 ]).add_class("global_data_tab_container") 170 171 self.input_dashboard = widgets.Tab().add_class("global_tab_container") 172 self.input_dashboard.children = [ 173 data_source_box, 174 simulation_box 175 ] 176 tab_titles = ['Input', 'Simulation'] 177 for i in range(len(tab_titles)): 178 self.input_dashboard.set_title(i, tab_titles[i]) 179 180 def start_simulation(self, ui_element=None): 181 """Start simulation with the 3 currently selected input sets. 182 183 Args: 184 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 185 """ 186 configuration.execute_job( 187 self.pyiron_project, 188 self.temperature_input_set_data[self.temperature_input_selection.value], 189 self.defect_input_set_data[self.defect_input_selection.value], 190 self.stress_input_set_data[self.stress_input_selection.value], 191 self.conn 192 ) 193 194 def init_job_overview_UI(self): 195 """Initialize second UI of notebook, viewing the pyiron job table.""" 196 self.pyiron_project = configuration.get_pyiron_project("jobs_test") 197 self.job_dashboard = widgets.Output(layout={'border': '1px solid black'}) 198 self.update_job_list_display() 199 200 def update_job_list_display(self): 201 """Reload job table from pyiron project.""" 202 if self.job_dashboard: 203 self.job_dashboard.clear_output() 204 columns_to_hide = ["projectpath","chemicalformula","parentid","masterid","hamversion"] 205 with self.job_dashboard: 206 display(configuration.get_job_table(self.pyiron_project).drop(columns=columns_to_hide)) 207 208 def init_result_display_UI(self): 209 """Initialize third UI of notebook, displaying result of a simulation.""" 210 result_selection_label = widgets.Label(value="Completed simulation:").add_class("global_headline") 211 self.result_selection = widgets.Select( 212 options=['Load data first'], 213 value='Load data first', 214 rows=10, 215 description='Input sets:', 216 disabled=False 217 ).add_class("global_input_set_selection").add_class("global_basic_input") 218 display_result_button = widgets.Button( 219 description='Display Result', 220 disabled=False, 221 tooltip='Display Result', 222 icon='play', 223 ).add_class("global_basic_button") 224 display_result_button.on_click(self.display_result) 225 result_display_selection_box = widgets.VBox([ 226 result_selection_label, 227 self.result_selection, 228 display_result_button 229 ]) 230 231 structure_render_label = widgets.Label(value="Structure simulation:").add_class("global_headline") 232 self.structure_deformation_toggle = widgets.ToggleButton( 233 value=False, 234 description='Toggle Deformation', 235 tooltip='Toggle Deformation', 236 ).add_class("global_basic_button") 237 self.structure_deformation_toggle.observe(self.on_structure_deformation_changed, names=['value']) 238 self.structure_layer_select = widgets.Dropdown( 239 options=['Load Result'], 240 value='Load Result', 241 description='Layer:', 242 disabled=False, 243 ).add_class("global_basic_input") 244 self.structure_layer_select.observe(self.on_structure_layer_changed, names=['value']) 245 self.structure_scalar_select = widgets.Dropdown( 246 options=['Select Layer'], 247 value='Select Layer', 248 description='Value:', 249 disabled=False, 250 ).add_class("global_basic_input") 251 self.structure_scalar_select.observe(self.on_structure_scalar_changed, names=['value']) 252 self.structure_cell_info = widgets.HTML().add_class("life_extra_cell_data_column_container") 253 254 curing_render_label = widgets.Label(value="Curing simulation:").add_class("global_headline") 255 self.curing_deformation_toggle = widgets.ToggleButton( 256 value=False, 257 description='Toggle Deformation', 258 tooltip='Toggle Deformation', 259 ).add_class("global_basic_button") 260 self.curing_deformation_toggle.observe(self.on_curing_deformation_changed, names=['value']) 261 self.curing_layer_select = widgets.Dropdown( 262 options=['Load Result'], 263 value='Load Result', 264 description='Layer:', 265 disabled=False, 266 ).add_class("global_basic_input") 267 self.curing_layer_select.observe(self.on_curing_layer_changed, names=['value']) 268 self.curing_scalar_select = widgets.Dropdown( 269 options=['Select Layer'], 270 value='Select Layer', 271 description='Value:', 272 disabled=False, 273 ).add_class("global_basic_input") 274 self.curing_scalar_select.observe(self.on_curing_scalar_changed, names=['value']) 275 self.curing_cell_info = widgets.HTML().add_class("life_extra_cell_data_column_container") 276 277 result_display_controls = widgets.VBox([ 278 widgets.VBox([ 279 curing_render_label, 280 self.curing_deformation_toggle, 281 self.curing_layer_select, 282 self.curing_scalar_select 283 ]).add_class("life_extra_output_column_container"), 284 widgets.VBox([ 285 structure_render_label, 286 self.structure_deformation_toggle, 287 self.structure_layer_select, 288 self.structure_scalar_select 289 ]).add_class("life_extra_output_column_container") 290 ]).add_class("life_extra_output_column_container") 291 292 PYVISTA_OUTPUT_RENDER_HEIGHT = 1100 293 PYVISTA_OUTPUT_RENDER_WIDTH = 900 294 pyvista_render_widget = widgets.Output(layout={'height': '{}px'.format(PYVISTA_OUTPUT_RENDER_HEIGHT+15), 295 'width': '{}px'.format(PYVISTA_OUTPUT_RENDER_WIDTH+10)}) 296 297 result_cell_info_box = widgets.VBox([ 298 self.curing_cell_info, 299 self.structure_cell_info 300 ]).add_class("life_extra_output_column_container") 301 result_display_box = widgets.HBox([ 302 widgets.VBox([ 303 pyvista_render_widget 304 ]), 305 result_display_controls, 306 result_cell_info_box 307 ]) 308 309 self.plotter = pv.Plotter(shape=(2,1)) 310 self.plotter.window_size = [PYVISTA_OUTPUT_RENDER_WIDTH, PYVISTA_OUTPUT_RENDER_HEIGHT] 311 self.plotter.enable_element_picking(callback=self.selection_callback, show_message=False) 312 with pyvista_render_widget: 313 self.plotter.show(jupyter_backend='trame') 314 315 self.result_dashboard = widgets.Tab().add_class("global_tab_container") 316 self.result_dashboard.children = [ 317 result_display_selection_box, 318 result_display_box 319 ] 320 tab_titles = ['Select Result', 'Result Display'] 321 for i in range(len(tab_titles)): 322 self.result_dashboard.set_title(i, tab_titles[i]) 323 324 def update_result_list_display(self): 325 """Update list of completed jobs in result display selection list.""" 326 if self.result_dashboard: 327 options = [] 328 for _, row in configuration.get_job_table(self.pyiron_project).iterrows(): 329 if row["hamilton"] == "StructureSimulation" and row["status"] == "finished": 330 options.append((row["job"], row["id"])) 331 if options: 332 self.result_selection.options = options 333 self.result_selection.value = options[0][1] 334 335 def display_result(self, ui_element=None): 336 """Load currently selected result from list of finished jobs. 337 338 Args: 339 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 340 """ 341 all_jobs = configuration.get_job_table(self.pyiron_project) 342 result = all_jobs.loc[all_jobs.id == self.result_selection.value].iloc[0] 343 result_job = self.pyiron_project.load(result["job"]) 344 self.init_pyvista_render(result_job.output.curing_mesh,result_job.output.mesh) 345 346 def init_pyvista_render(self, curing_mesh, structure_mesh): 347 """Load result mesh from job into PyVista display. 348 349 Args: 350 structure_mesh: Relative file path to result mesh 351 """ 352 initial_camera_position = [(-30.765771120379302, -28.608772602676154, 39.46235706090557), 353 (0.9572034500000001, 0.0005481500000000805, -30.4), 354 (-0.16976192468426815, -0.8825527410211623, -0.43849919982085056)] 355 356 # render will be triggered by the scalar widget change event when setting new options 357 self.set_plotter_to_curing() 358 self.curing_mesh_data = self.create_curing_mesh_data(curing_mesh) 359 self.plotter.camera_position = initial_camera_position 360 self.relevant_curing_cell_fields = self.get_relevant_curing_point_fields() 361 self.update_available_curing_layers() 362 363 # render will be triggered by the scalar widget change event when setting new options 364 self.set_plotter_to_structure() 365 self.structure_mesh_data = self.create_structure_mesh_data(structure_mesh) 366 self.plotter.camera_position = initial_camera_position 367 self.relevant_structure_cell_fields = self.get_relevant_structure_cell_fields() 368 self.update_available_structure_layers() 369 370 self.plotter.update() 371 372 def create_structure_mesh_data(self, mesh_path: str) -> dict: 373 """Read structure 3D mesh and split into information necessary for display purposes. 374 375 Args: 376 mesh_path: Path to vtu file on file system 377 378 Returns: 379 dictionary containing parsed mesh data for given file with placeholders for display PyVista actors 380 """ 381 mesh = self.reduce_cell_points(pv.read(mesh_path).cast_to_unstructured_grid(), self.STRUCTURE_MESH_ID) 382 layers = np.unique(mesh[self.LAYER_ID_KEY]) 383 mesh_data = {} 384 for layer_id in layers: 385 input_mesh = mesh.remove_cells(np.argwhere(mesh[self.LAYER_ID_KEY] != layer_id)) 386 input_mesh['color'] = [0 for x in range(len(input_mesh[self.LAYER_THICKNESS_KEY]))] 387 mesh_data[layer_id] = { 388 "input": input_mesh, 389 "warped": input_mesh.warp_by_vector(vectors=self.POINT_WARP_KEY), 390 "scalar_bar_name": None, 391 "output": None 392 } 393 return mesh_data 394 395 def create_curing_mesh_data(self, mesh_path: str) -> dict: 396 """Read structure 3D mesh and split into information necessary for display purposes. 397 398 Args: 399 mesh_path: Path to vtu file on file system 400 401 Returns: 402 dictionary containing parsed mesh data for given file with placeholders for display PyVista actors 403 """ 404 mesh = self.reduce_cell_points(pv.read(mesh_path).cast_to_unstructured_grid(), self.CURING_MESH_ID) 405 layers = np.unique(mesh[self.LAYER_ID_KEY]) 406 mesh_data = {} 407 for layer_id in layers: 408 input_mesh = mesh.remove_cells(np.argwhere(mesh[self.LAYER_ID_KEY] != layer_id)) 409 input_mesh['color'] = [0 for x in range(len(input_mesh[self.LAYER_THICKNESS_KEY]))] 410 mesh_data[layer_id] = { 411 "input": input_mesh, 412 "warped": input_mesh.warp_by_vector(vectors=self.POINT_WARP_KEY), 413 "scalar_bar_name": None, 414 "output": None 415 } 416 return mesh_data 417 418 def reduce_cell_points(self, mesh: pv.UnstructuredGrid, key: int) -> pv.UnstructuredGrid: 419 """Reduces 3D cells of structure simulation result to 2D polygons for display purposes. 420 421 Args: 422 mesh: PyVista mesh read directly from simulation result file 423 key: key to add to cells for later identification of meshes 424 425 Returns: 426 3D mesh with same cell layout and cell data content as input, but reduced in dimension (20 to 4 points per cell) 427 """ 428 full_cell_data = mesh.cell_data 429 full_point_data = mesh.point_data 430 cell_ids = mesh.cells 431 cell_ids = np.split(cell_ids, len(cell_ids) / 21) 432 433 new_points = [] 434 new_ids = [] 435 point_scalar_slice = [] 436 for cell_id in range(mesh.n_cells): 437 cell = mesh.get_cell(cell_id) 438 new_ids.append(4) 439 for i, point in enumerate(cell.points[:4]): 440 new_points.append(point) 441 point_scalar_slice.append(cell_ids[cell_id][i+1]) 442 new_ids.append(len(new_points) - 1) 443 444 point_scalars = full_point_data[self.POINT_WARP_KEY][point_scalar_slice] 445 446 reduced_mesh = pv.UnstructuredGrid(new_ids, [pv.CellType.POLYGON for _ in range(mesh.n_cells)], new_points) 447 for prop in full_cell_data: 448 reduced_mesh[prop] = full_cell_data[prop] 449 reduced_mesh["mesh_id"] = [key] * len(cell_ids) 450 reduced_mesh.point_data[self.POINT_WARP_KEY] = point_scalars 451 reduced_mesh.point_data["{}_total".format(self.POINT_WARP_KEY)] = [math.sqrt(x*x+y*y+z*z) for [x,y,z] in point_scalars] 452 for i in range(len(point_scalars[0])): 453 reduced_mesh.point_data["{}_{}".format(self.POINT_WARP_KEY, i)] = [x[i] for x in point_scalars] 454 455 return reduced_mesh 456 457 def get_relevant_structure_cell_fields(self) -> list: 458 """Get all fields that should ne filter-able for structure mesh. 459 460 Returns: 461 list of string names of scalar fields that should be toggleable for display 462 """ 463 relevant_cells = ["THICKMM"] 464 for x in self.structure_mesh_data[list(self.structure_mesh_data.keys())[0]]["input"].cell_data: 465 if x.startswith("DFAT_") or x.startswith("TFAT_"): 466 relevant_cells.append(x) 467 return relevant_cells 468 469 def get_relevant_curing_point_fields(self) -> list: 470 """Get all fields that should ne filter-able for curing mesh. 471 472 Returns: 473 list of string names of scalar fields that should be toggleable for display 474 """ 475 relevant_cells = [] 476 for x in self.curing_mesh_data[list(self.curing_mesh_data.keys())[0]]["input"].point_data: 477 if x.startswith("{}_".format(self.POINT_WARP_KEY)): 478 relevant_cells.append(x) 479 return relevant_cells 480 481 def update_available_structure_layers(self): 482 """Update available layers for display in structure dropdown. 483 484 This causes an update in PyVista rendering by triggering the onchange event of 485 the layer select widget. 486 """ 487 self.structure_layer_select.options = self.structure_mesh_data.keys() 488 self.structure_layer_select.value = list(self.structure_mesh_data.keys())[0] 489 490 def update_available_curing_layers(self): 491 """Update available layers for display in curing dropdown. 492 493 This causes an update in PyVista rendering by triggering the onchange event of 494 the layer select widget. 495 """ 496 self.curing_layer_select.options = self.curing_mesh_data.keys() 497 self.curing_layer_select.value = list(self.curing_mesh_data.keys())[0] 498 499 def update_available_structure_layer_scalars(self, layer_id: int): 500 """Update scalar select with available scalars for currently displayed layer.""" 501 options = [] 502 for field in self.relevant_structure_cell_fields: 503 if not np.isnan(self.structure_mesh_data[layer_id]["input"][field]).all(): 504 options.append(field) 505 if len(options) == 0: 506 options.append("No layers with valid values") 507 self.structure_scalar_select.options = options 508 self.structure_scalar_select.value = options[0] 509 510 def update_available_curing_layer_scalars(self, layer_id: int): 511 """Update scalar select with available scalars for currently displayed layer.""" 512 options = [] 513 for field in self.relevant_curing_cell_fields: 514 if not np.isnan(self.curing_mesh_data[layer_id]["input"][field]).all(): 515 options.append(field) 516 if len(options) == 0: 517 options.append("No layers with valid values") 518 self.curing_scalar_select.options = options 519 self.curing_scalar_select.value = options[0] 520 521 def on_structure_layer_changed(self, value: Bunch): 522 """Event to update UI after selected structure layer to display has changed.""" 523 self.update_available_structure_layer_scalars(value["new"]) 524 self.update_displayed_structure_scalar(self.structure_scalar_select.value) 525 526 def on_curing_layer_changed(self, value: Bunch): 527 """Event to update UI after selected curing layer to display has changed.""" 528 self.update_available_curing_layer_scalars(value["new"]) 529 self.update_displayed_curing_scalar(self.curing_scalar_select.value) 530 531 def on_structure_scalar_changed(self, value: Bunch): 532 """Event to update UI after selected structure scalar to display has changed.""" 533 self.update_displayed_structure_scalar(value["new"]) 534 535 def on_curing_scalar_changed(self, value: Bunch): 536 """Event to update UI after selected curing scalar to display has changed.""" 537 self.update_displayed_curing_scalar(value["new"]) 538 539 def on_structure_deformation_changed(self, value: Bunch): 540 """Event to update UI after value of deformation display has changed.""" 541 self.update_displayed_structure_scalar(self.structure_scalar_select.value) 542 543 def on_curing_deformation_changed(self, value: Bunch): 544 """Event to update UI after value of deformation display has changed.""" 545 self.update_displayed_curing_scalar(self.curing_scalar_select.value) 546 547 def update_displayed_structure_scalar(self, value: str): 548 """Update PyVista structure render. 549 550 Args: 551 value: Scalar name to display on 3D mesh 552 """ 553 self.set_plotter_to_structure() 554 if self.structure_deformation_toggle.value == True: 555 mesh_name = "warped" 556 else: 557 mesh_name = "input" 558 for layer_id, structure_layer in self.structure_mesh_data.items(): 559 if structure_layer["output"] != None: 560 self.plotter.remove_actor(structure_layer["output"]) 561 # scalars bars with multiplots have unpredictable behaviour, do not this check 562 if structure_layer["scalar_bar_name"] in self.plotter.scalar_bars: 563 self.plotter.remove_scalar_bar(structure_layer["scalar_bar_name"]) 564 structure_layer["scalar_bar_name"] = None 565 structure_layer["output"] = None 566 if self.structure_limit_actor: 567 self.plotter.remove_actor(self.structure_limit_actor) 568 self.structure_limit_actor = None 569 self.structure_cell_info.value = "" 570 571 data = self.structure_mesh_data[self.structure_layer_select.value] 572 data[mesh_name].cell_data.set_scalars(data[mesh_name][value], "color") 573 if value.startswith("DFAT_"): 574 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["cmap"] 575 clim = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["clim"] 576 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["log_scale"] 577 title = "DFAT Layer {}".format(self.structure_layer_select.value) 578 self.structure_limit_actor = self.show_limit_value(data[mesh_name], value, limit="max", geometry_type="cell") 579 elif value.startswith("TFAT"): 580 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["cmap"] 581 clim = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["clim"] 582 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["log_scale"] 583 title = "TFAT Layer {}".format(self.structure_layer_select.value) 584 self.structure_limit_actor = self.show_limit_value(data[mesh_name], value, limit="min", geometry_type="cell") 585 elif value.startswith(self.LAYER_THICKNESS_KEY): 586 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["THICKNESS"]["cmap"] 587 clim = np.nanmin(data[mesh_name][self.LAYER_THICKNESS_KEY]), np.max(data[mesh_name][self.LAYER_THICKNESS_KEY]) 588 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["THICKNESS"]["log_scale"] 589 title = "Thickness(mm) Layer {}".format(self.structure_layer_select.value) 590 else: 591 raise ValueError("No colorscheme for chosen scalar") 592 593 data["output"] = self.plotter.add_mesh(data[mesh_name], show_edges=False, lighting=False, show_scalar_bar=True, scalars="color", 594 cmap=cmap, clim=clim, log_scale=log_scale, scalar_bar_args={'title': title}) 595 data["scalar_bar_name"] = title 596 self.plotter.update() 597 598 def update_displayed_curing_scalar(self, value: str): 599 """Update PyVista curing render. 600 601 Args: 602 value: Scalar name to display on 3D mesh 603 """ 604 self.set_plotter_to_curing() 605 if self.curing_deformation_toggle.value == True: 606 mesh_name = "warped" 607 else: 608 mesh_name = "input" 609 for layer_id, curing_layer in self.curing_mesh_data.items(): 610 if curing_layer["output"] != None: 611 self.plotter.remove_actor(curing_layer["output"]) 612 # scalars bars with multiplots have unpredictable behaviour, do not this check 613 if curing_layer["scalar_bar_name"] in self.plotter.scalar_bars: 614 self.plotter.remove_scalar_bar(curing_layer["scalar_bar_name"]) 615 curing_layer["scalar_bar_name"] = None 616 curing_layer["output"] = None 617 if self.curing_limit_actor: 618 self.plotter.remove_actor(self.curing_limit_actor) 619 self.curing_limit_actor = None 620 self.curing_cell_info.value = "" 621 622 data = self.curing_mesh_data[self.curing_layer_select.value] 623 if value.startswith(self.POINT_WARP_KEY): 624 cmap = 'jet' 625 title = value 626 self.curing_limit_actor = self.show_limit_value(data[mesh_name], value, limit="max", geometry_type="point") 627 else: 628 raise ValueError("No colorscheme for chosen scalar") 629 data["output"] = self.plotter.add_mesh(data[mesh_name], show_edges=False, lighting=False, show_scalar_bar=True, scalars=value, 630 cmap=cmap, scalar_bar_args={'title': title}) 631 data["scalar_bar_name"] = title 632 self.plotter.update() 633 634 def show_limit_value(self, mesh: pv.UnstructuredGrid, scalar: str, limit: str='max', geometry_type: str='cell') -> vtkActor2D: 635 """Render point with tooltip at place of maximum value in PyVista. 636 637 Args: 638 mesh: 3D mesh with cells to check for values 639 scalar: Name of scalar to check for values 640 limit: 'max' or 'min' 641 642 Returns: 643 vtk actor of added max value point 644 """ 645 if limit == 'max': 646 limit_value = np.nanmax(mesh[scalar]) 647 fmt = "Max: %.5f" 648 else: 649 limit_value = np.nanmin(mesh[scalar]) 650 fmt = "Min: %.5f" 651 652 if geometry_type == "cell": 653 limit_cell = mesh.remove_cells(np.argwhere(mesh[scalar] != limit_value)) 654 point = limit_cell.points[0] 655 scalar_value = [limit_cell[scalar]] 656 elif geometry_type == "point": 657 limit_cells = mesh.extract_points(np.argwhere(mesh[scalar] == limit_value)) 658 point = limit_cells.points[0] 659 scalar_value = [limit_cells[scalar][0]] 660 661 return self.plotter.add_point_scalar_labels(point, scalar_value, name="{}_limit_value_point".format(geometry_type), fmt=fmt, point_size=20, shape="rounded_rect", 662 point_color="red", render_points_as_spheres=True, fill_shape=True, shape_color="pink", text_color="red",shadow=True, tolerance=1) 663 664 def selection_callback(self, cell: pv.Cell): 665 """Event to display detail information for selected cell.""" 666 display_html = '<p class="global_headline"> Selected cell: </p>' 667 if cell["mesh_id"] == self.CURING_MESH_ID: 668 for prop in cell.point_data: 669 if not np.isnan(cell[prop].any()): 670 display_html += "<b>{property_name}</b>: {property_value} \n".format(property_name=prop, property_value=cell[prop][0]) 671 self.curing_cell_info.value = display_html 672 else: 673 for prop in cell.cell_data: 674 if not np.isnan(cell[prop]): 675 display_html += "<b>{property_name}</b>: {property_value} \n".format(property_name=prop, property_value=cell[prop][0]) 676 self.structure_cell_info.value = display_html 677 678 def set_plotter_to_curing(self): 679 """Set active plot to curing simulation, redirecting call to plotter.""" 680 self.plotter.subplot(0,0) 681 682 def set_plotter_to_structure(self): 683 """Set active plot to structure simulation, redirecting call to plotter.""" 684 self.plotter.subplot(1,0) 685 686 def load_local_data(self, ui_element=None): 687 """Use locally stored data for UI. 688 689 Args: 690 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 691 """ 692 self.conn = store_connection.LocalStoreConnection("sensotwin_world") 693 self.load_input_set_data() 694 695 def load_remote_data(self, ui_element=None): 696 """Use remotely stored data for UI. 697 698 Args: 699 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 700 """ 701 self.conn = store_connection.FusekiConnection(self.remote_database_input.value) 702 self.load_input_set_data() 703 704 def load_input_set_data(self): 705 """Load data from previously set input source and populate UI.""" 706 self.temperature_input_set_data = temperature_config.MaterialHardeningInputDataSet.get_all_entries_from_store(self.conn) 707 if self.temperature_input_set_data: 708 options = [] 709 for key, input_set in self.temperature_input_set_data.items(): 710 label = input_set.generate_input_set_display_label() 711 options.append((label, key)) 712 self.temperature_input_selection.options = options 713 self.temperature_input_selection.value = list(self.temperature_input_set_data.keys())[0] 714 715 self.defect_input_set_data = defect_config.DefectInputDataSet.get_all_entries_from_store(self.conn) 716 if self.defect_input_set_data: 717 options = [] 718 for key, input_set in self.defect_input_set_data.items(): 719 label = input_set.generate_input_set_display_label() 720 options.append((label, key)) 721 self.defect_input_selection.options = options 722 self.defect_input_selection.value = list(self.defect_input_set_data.keys())[0] 723 724 self.stress_input_set_data = stress_config.StressStrainInputDataSet.get_all_entries_from_store(self.conn) 725 if self.stress_input_set_data: 726 options = [] 727 for key, input_set in self.stress_input_set_data.items(): 728 label = input_set.generate_input_set_display_label() 729 options.append((label, key)) 730 self.stress_input_selection.options = options 731 self.stress_input_selection.value = list(self.stress_input_set_data.keys())[0] 732 733 def apply_styling(self): 734 css = """ 735 <style> 736 {} 737 {} 738 </style> 739 """.format(global_style.global_css, style.local_css) 740 return widgets.HTML(css)
Contains all UI functionality for executing simulation and viewing results (Step 4).
74 def __init__(self): 75 """Initialize UI objects and associated UI functionality.""" 76 self.init_input_set_selection_UI() 77 self.init_job_overview_UI() 78 self.init_result_display_UI()
Initialize UI objects and associated UI functionality.
80 def init_input_set_selection_UI(self): 81 """Initialize first UI of notebook, choosing data source and selecting input data sets.""" 82 temperature_input_selection_label = widgets.Label(value="Material Hardening Input:").add_class("global_headline") 83 self.temperature_input_selection = widgets.Select( 84 options=['No data in source'], 85 value='No data in source', 86 rows=10, 87 description='Input sets:', 88 disabled=False 89 ).add_class("global_input_set_selection").add_class("global_basic_input") 90 defect_input_selection_label = widgets.Label(value="Defect Placement Input Set:").add_class("global_headline") 91 self.defect_input_selection = widgets.Select( 92 options=['No data in source'], 93 value='No data in source', 94 rows=10, 95 description='Input sets:', 96 disabled=False 97 ).add_class("global_input_set_selection").add_class("global_basic_input") 98 stress_input_selection_label = widgets.Label(value="Stress/Strain Input Set:").add_class("global_headline") 99 self.stress_input_selection = widgets.Select( 100 options=['No data in source'], 101 value='No data in source', 102 rows=10, 103 description='Input sets:', 104 disabled=False 105 ).add_class("global_input_set_selection").add_class("global_basic_input") 106 start_simulation_button = widgets.Button( 107 description='Start simulation with selected input data sets', 108 disabled=False, 109 tooltip='Start simulation with selected input data sets', 110 icon='play', 111 ).add_class("global_save_input_set_button").add_class("global_basic_button") 112 start_simulation_button.on_click(self.start_simulation) 113 114 simulation_temperature_selection = widgets.VBox([ 115 temperature_input_selection_label, 116 self.temperature_input_selection 117 ]).add_class("life_extra_input_column_container") 118 simulation_defect_placement = widgets.VBox([ 119 defect_input_selection_label, 120 self.defect_input_selection 121 ]).add_class("life_extra_input_column_container") 122 simulation_stress_selection = widgets.VBox([ 123 stress_input_selection_label, 124 self.stress_input_selection 125 ]).add_class("life_extra_input_column_container") 126 simulation_box = widgets.VBox([ 127 widgets.HBox([ 128 simulation_temperature_selection, 129 simulation_defect_placement, 130 simulation_stress_selection 131 ]).add_class("life_extra_input_row_container"), 132 start_simulation_button 133 ]) 134 135 local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline") 136 use_local_data_button = widgets.Button( 137 description='Use local database', 138 disabled=False, 139 tooltip='Use local database', 140 icon='play', 141 ).add_class("global_load_data_button").add_class("global_basic_button") 142 use_local_data_button.on_click(self.load_local_data) 143 remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline") 144 self.remote_database_input = widgets.Text( 145 value=None, 146 placeholder="insert SPARQL url here", 147 description="SPARQL Endpoint:", 148 disabled=False 149 ).add_class("global_url_input").add_class("global_basic_input") 150 use_remote_data_button = widgets.Button( 151 description='Use remote database', 152 disabled=False, 153 tooltip='Use remote database', 154 icon='play', 155 ).add_class("global_load_data_button").add_class("global_basic_button") 156 use_remote_data_button.on_click(self.load_remote_data) 157 local_data_box = widgets.VBox([ 158 local_database_label, 159 use_local_data_button 160 ]) 161 remote_data_box = widgets.VBox([ 162 remote_database_label, 163 self.remote_database_input, 164 use_remote_data_button 165 ]) 166 data_source_box = widgets.VBox([ 167 local_data_box, 168 remote_data_box 169 ]).add_class("global_data_tab_container") 170 171 self.input_dashboard = widgets.Tab().add_class("global_tab_container") 172 self.input_dashboard.children = [ 173 data_source_box, 174 simulation_box 175 ] 176 tab_titles = ['Input', 'Simulation'] 177 for i in range(len(tab_titles)): 178 self.input_dashboard.set_title(i, tab_titles[i])
Initialize first UI of notebook, choosing data source and selecting input data sets.
180 def start_simulation(self, ui_element=None): 181 """Start simulation with the 3 currently selected input sets. 182 183 Args: 184 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 185 """ 186 configuration.execute_job( 187 self.pyiron_project, 188 self.temperature_input_set_data[self.temperature_input_selection.value], 189 self.defect_input_set_data[self.defect_input_selection.value], 190 self.stress_input_set_data[self.stress_input_selection.value], 191 self.conn 192 )
Start simulation with the 3 currently selected input sets.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
194 def init_job_overview_UI(self): 195 """Initialize second UI of notebook, viewing the pyiron job table.""" 196 self.pyiron_project = configuration.get_pyiron_project("jobs_test") 197 self.job_dashboard = widgets.Output(layout={'border': '1px solid black'}) 198 self.update_job_list_display()
Initialize second UI of notebook, viewing the pyiron job table.
200 def update_job_list_display(self): 201 """Reload job table from pyiron project.""" 202 if self.job_dashboard: 203 self.job_dashboard.clear_output() 204 columns_to_hide = ["projectpath","chemicalformula","parentid","masterid","hamversion"] 205 with self.job_dashboard: 206 display(configuration.get_job_table(self.pyiron_project).drop(columns=columns_to_hide))
Reload job table from pyiron project.
208 def init_result_display_UI(self): 209 """Initialize third UI of notebook, displaying result of a simulation.""" 210 result_selection_label = widgets.Label(value="Completed simulation:").add_class("global_headline") 211 self.result_selection = widgets.Select( 212 options=['Load data first'], 213 value='Load data first', 214 rows=10, 215 description='Input sets:', 216 disabled=False 217 ).add_class("global_input_set_selection").add_class("global_basic_input") 218 display_result_button = widgets.Button( 219 description='Display Result', 220 disabled=False, 221 tooltip='Display Result', 222 icon='play', 223 ).add_class("global_basic_button") 224 display_result_button.on_click(self.display_result) 225 result_display_selection_box = widgets.VBox([ 226 result_selection_label, 227 self.result_selection, 228 display_result_button 229 ]) 230 231 structure_render_label = widgets.Label(value="Structure simulation:").add_class("global_headline") 232 self.structure_deformation_toggle = widgets.ToggleButton( 233 value=False, 234 description='Toggle Deformation', 235 tooltip='Toggle Deformation', 236 ).add_class("global_basic_button") 237 self.structure_deformation_toggle.observe(self.on_structure_deformation_changed, names=['value']) 238 self.structure_layer_select = widgets.Dropdown( 239 options=['Load Result'], 240 value='Load Result', 241 description='Layer:', 242 disabled=False, 243 ).add_class("global_basic_input") 244 self.structure_layer_select.observe(self.on_structure_layer_changed, names=['value']) 245 self.structure_scalar_select = widgets.Dropdown( 246 options=['Select Layer'], 247 value='Select Layer', 248 description='Value:', 249 disabled=False, 250 ).add_class("global_basic_input") 251 self.structure_scalar_select.observe(self.on_structure_scalar_changed, names=['value']) 252 self.structure_cell_info = widgets.HTML().add_class("life_extra_cell_data_column_container") 253 254 curing_render_label = widgets.Label(value="Curing simulation:").add_class("global_headline") 255 self.curing_deformation_toggle = widgets.ToggleButton( 256 value=False, 257 description='Toggle Deformation', 258 tooltip='Toggle Deformation', 259 ).add_class("global_basic_button") 260 self.curing_deformation_toggle.observe(self.on_curing_deformation_changed, names=['value']) 261 self.curing_layer_select = widgets.Dropdown( 262 options=['Load Result'], 263 value='Load Result', 264 description='Layer:', 265 disabled=False, 266 ).add_class("global_basic_input") 267 self.curing_layer_select.observe(self.on_curing_layer_changed, names=['value']) 268 self.curing_scalar_select = widgets.Dropdown( 269 options=['Select Layer'], 270 value='Select Layer', 271 description='Value:', 272 disabled=False, 273 ).add_class("global_basic_input") 274 self.curing_scalar_select.observe(self.on_curing_scalar_changed, names=['value']) 275 self.curing_cell_info = widgets.HTML().add_class("life_extra_cell_data_column_container") 276 277 result_display_controls = widgets.VBox([ 278 widgets.VBox([ 279 curing_render_label, 280 self.curing_deformation_toggle, 281 self.curing_layer_select, 282 self.curing_scalar_select 283 ]).add_class("life_extra_output_column_container"), 284 widgets.VBox([ 285 structure_render_label, 286 self.structure_deformation_toggle, 287 self.structure_layer_select, 288 self.structure_scalar_select 289 ]).add_class("life_extra_output_column_container") 290 ]).add_class("life_extra_output_column_container") 291 292 PYVISTA_OUTPUT_RENDER_HEIGHT = 1100 293 PYVISTA_OUTPUT_RENDER_WIDTH = 900 294 pyvista_render_widget = widgets.Output(layout={'height': '{}px'.format(PYVISTA_OUTPUT_RENDER_HEIGHT+15), 295 'width': '{}px'.format(PYVISTA_OUTPUT_RENDER_WIDTH+10)}) 296 297 result_cell_info_box = widgets.VBox([ 298 self.curing_cell_info, 299 self.structure_cell_info 300 ]).add_class("life_extra_output_column_container") 301 result_display_box = widgets.HBox([ 302 widgets.VBox([ 303 pyvista_render_widget 304 ]), 305 result_display_controls, 306 result_cell_info_box 307 ]) 308 309 self.plotter = pv.Plotter(shape=(2,1)) 310 self.plotter.window_size = [PYVISTA_OUTPUT_RENDER_WIDTH, PYVISTA_OUTPUT_RENDER_HEIGHT] 311 self.plotter.enable_element_picking(callback=self.selection_callback, show_message=False) 312 with pyvista_render_widget: 313 self.plotter.show(jupyter_backend='trame') 314 315 self.result_dashboard = widgets.Tab().add_class("global_tab_container") 316 self.result_dashboard.children = [ 317 result_display_selection_box, 318 result_display_box 319 ] 320 tab_titles = ['Select Result', 'Result Display'] 321 for i in range(len(tab_titles)): 322 self.result_dashboard.set_title(i, tab_titles[i])
Initialize third UI of notebook, displaying result of a simulation.
324 def update_result_list_display(self): 325 """Update list of completed jobs in result display selection list.""" 326 if self.result_dashboard: 327 options = [] 328 for _, row in configuration.get_job_table(self.pyiron_project).iterrows(): 329 if row["hamilton"] == "StructureSimulation" and row["status"] == "finished": 330 options.append((row["job"], row["id"])) 331 if options: 332 self.result_selection.options = options 333 self.result_selection.value = options[0][1]
Update list of completed jobs in result display selection list.
335 def display_result(self, ui_element=None): 336 """Load currently selected result from list of finished jobs. 337 338 Args: 339 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 340 """ 341 all_jobs = configuration.get_job_table(self.pyiron_project) 342 result = all_jobs.loc[all_jobs.id == self.result_selection.value].iloc[0] 343 result_job = self.pyiron_project.load(result["job"]) 344 self.init_pyvista_render(result_job.output.curing_mesh,result_job.output.mesh)
Load currently selected result from list of finished jobs.
Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event
346 def init_pyvista_render(self, curing_mesh, structure_mesh): 347 """Load result mesh from job into PyVista display. 348 349 Args: 350 structure_mesh: Relative file path to result mesh 351 """ 352 initial_camera_position = [(-30.765771120379302, -28.608772602676154, 39.46235706090557), 353 (0.9572034500000001, 0.0005481500000000805, -30.4), 354 (-0.16976192468426815, -0.8825527410211623, -0.43849919982085056)] 355 356 # render will be triggered by the scalar widget change event when setting new options 357 self.set_plotter_to_curing() 358 self.curing_mesh_data = self.create_curing_mesh_data(curing_mesh) 359 self.plotter.camera_position = initial_camera_position 360 self.relevant_curing_cell_fields = self.get_relevant_curing_point_fields() 361 self.update_available_curing_layers() 362 363 # render will be triggered by the scalar widget change event when setting new options 364 self.set_plotter_to_structure() 365 self.structure_mesh_data = self.create_structure_mesh_data(structure_mesh) 366 self.plotter.camera_position = initial_camera_position 367 self.relevant_structure_cell_fields = self.get_relevant_structure_cell_fields() 368 self.update_available_structure_layers() 369 370 self.plotter.update()
Load result mesh from job into PyVista display.
Args: structure_mesh: Relative file path to result mesh
372 def create_structure_mesh_data(self, mesh_path: str) -> dict: 373 """Read structure 3D mesh and split into information necessary for display purposes. 374 375 Args: 376 mesh_path: Path to vtu file on file system 377 378 Returns: 379 dictionary containing parsed mesh data for given file with placeholders for display PyVista actors 380 """ 381 mesh = self.reduce_cell_points(pv.read(mesh_path).cast_to_unstructured_grid(), self.STRUCTURE_MESH_ID) 382 layers = np.unique(mesh[self.LAYER_ID_KEY]) 383 mesh_data = {} 384 for layer_id in layers: 385 input_mesh = mesh.remove_cells(np.argwhere(mesh[self.LAYER_ID_KEY] != layer_id)) 386 input_mesh['color'] = [0 for x in range(len(input_mesh[self.LAYER_THICKNESS_KEY]))] 387 mesh_data[layer_id] = { 388 "input": input_mesh, 389 "warped": input_mesh.warp_by_vector(vectors=self.POINT_WARP_KEY), 390 "scalar_bar_name": None, 391 "output": None 392 } 393 return mesh_data
Read structure 3D mesh and split into information necessary for display purposes.
Args: mesh_path: Path to vtu file on file system
Returns: dictionary containing parsed mesh data for given file with placeholders for display PyVista actors
395 def create_curing_mesh_data(self, mesh_path: str) -> dict: 396 """Read structure 3D mesh and split into information necessary for display purposes. 397 398 Args: 399 mesh_path: Path to vtu file on file system 400 401 Returns: 402 dictionary containing parsed mesh data for given file with placeholders for display PyVista actors 403 """ 404 mesh = self.reduce_cell_points(pv.read(mesh_path).cast_to_unstructured_grid(), self.CURING_MESH_ID) 405 layers = np.unique(mesh[self.LAYER_ID_KEY]) 406 mesh_data = {} 407 for layer_id in layers: 408 input_mesh = mesh.remove_cells(np.argwhere(mesh[self.LAYER_ID_KEY] != layer_id)) 409 input_mesh['color'] = [0 for x in range(len(input_mesh[self.LAYER_THICKNESS_KEY]))] 410 mesh_data[layer_id] = { 411 "input": input_mesh, 412 "warped": input_mesh.warp_by_vector(vectors=self.POINT_WARP_KEY), 413 "scalar_bar_name": None, 414 "output": None 415 } 416 return mesh_data
Read structure 3D mesh and split into information necessary for display purposes.
Args: mesh_path: Path to vtu file on file system
Returns: dictionary containing parsed mesh data for given file with placeholders for display PyVista actors
418 def reduce_cell_points(self, mesh: pv.UnstructuredGrid, key: int) -> pv.UnstructuredGrid: 419 """Reduces 3D cells of structure simulation result to 2D polygons for display purposes. 420 421 Args: 422 mesh: PyVista mesh read directly from simulation result file 423 key: key to add to cells for later identification of meshes 424 425 Returns: 426 3D mesh with same cell layout and cell data content as input, but reduced in dimension (20 to 4 points per cell) 427 """ 428 full_cell_data = mesh.cell_data 429 full_point_data = mesh.point_data 430 cell_ids = mesh.cells 431 cell_ids = np.split(cell_ids, len(cell_ids) / 21) 432 433 new_points = [] 434 new_ids = [] 435 point_scalar_slice = [] 436 for cell_id in range(mesh.n_cells): 437 cell = mesh.get_cell(cell_id) 438 new_ids.append(4) 439 for i, point in enumerate(cell.points[:4]): 440 new_points.append(point) 441 point_scalar_slice.append(cell_ids[cell_id][i+1]) 442 new_ids.append(len(new_points) - 1) 443 444 point_scalars = full_point_data[self.POINT_WARP_KEY][point_scalar_slice] 445 446 reduced_mesh = pv.UnstructuredGrid(new_ids, [pv.CellType.POLYGON for _ in range(mesh.n_cells)], new_points) 447 for prop in full_cell_data: 448 reduced_mesh[prop] = full_cell_data[prop] 449 reduced_mesh["mesh_id"] = [key] * len(cell_ids) 450 reduced_mesh.point_data[self.POINT_WARP_KEY] = point_scalars 451 reduced_mesh.point_data["{}_total".format(self.POINT_WARP_KEY)] = [math.sqrt(x*x+y*y+z*z) for [x,y,z] in point_scalars] 452 for i in range(len(point_scalars[0])): 453 reduced_mesh.point_data["{}_{}".format(self.POINT_WARP_KEY, i)] = [x[i] for x in point_scalars] 454 455 return reduced_mesh
Reduces 3D cells of structure simulation result to 2D polygons for display purposes.
Args: mesh: PyVista mesh read directly from simulation result file key: key to add to cells for later identification of meshes
Returns: 3D mesh with same cell layout and cell data content as input, but reduced in dimension (20 to 4 points per cell)
457 def get_relevant_structure_cell_fields(self) -> list: 458 """Get all fields that should ne filter-able for structure mesh. 459 460 Returns: 461 list of string names of scalar fields that should be toggleable for display 462 """ 463 relevant_cells = ["THICKMM"] 464 for x in self.structure_mesh_data[list(self.structure_mesh_data.keys())[0]]["input"].cell_data: 465 if x.startswith("DFAT_") or x.startswith("TFAT_"): 466 relevant_cells.append(x) 467 return relevant_cells
Get all fields that should ne filter-able for structure mesh.
Returns: list of string names of scalar fields that should be toggleable for display
469 def get_relevant_curing_point_fields(self) -> list: 470 """Get all fields that should ne filter-able for curing mesh. 471 472 Returns: 473 list of string names of scalar fields that should be toggleable for display 474 """ 475 relevant_cells = [] 476 for x in self.curing_mesh_data[list(self.curing_mesh_data.keys())[0]]["input"].point_data: 477 if x.startswith("{}_".format(self.POINT_WARP_KEY)): 478 relevant_cells.append(x) 479 return relevant_cells
Get all fields that should ne filter-able for curing mesh.
Returns: list of string names of scalar fields that should be toggleable for display
481 def update_available_structure_layers(self): 482 """Update available layers for display in structure dropdown. 483 484 This causes an update in PyVista rendering by triggering the onchange event of 485 the layer select widget. 486 """ 487 self.structure_layer_select.options = self.structure_mesh_data.keys() 488 self.structure_layer_select.value = list(self.structure_mesh_data.keys())[0]
Update available layers for display in structure dropdown.
This causes an update in PyVista rendering by triggering the onchange event of the layer select widget.
490 def update_available_curing_layers(self): 491 """Update available layers for display in curing dropdown. 492 493 This causes an update in PyVista rendering by triggering the onchange event of 494 the layer select widget. 495 """ 496 self.curing_layer_select.options = self.curing_mesh_data.keys() 497 self.curing_layer_select.value = list(self.curing_mesh_data.keys())[0]
Update available layers for display in curing dropdown.
This causes an update in PyVista rendering by triggering the onchange event of the layer select widget.
499 def update_available_structure_layer_scalars(self, layer_id: int): 500 """Update scalar select with available scalars for currently displayed layer.""" 501 options = [] 502 for field in self.relevant_structure_cell_fields: 503 if not np.isnan(self.structure_mesh_data[layer_id]["input"][field]).all(): 504 options.append(field) 505 if len(options) == 0: 506 options.append("No layers with valid values") 507 self.structure_scalar_select.options = options 508 self.structure_scalar_select.value = options[0]
Update scalar select with available scalars for currently displayed layer.
510 def update_available_curing_layer_scalars(self, layer_id: int): 511 """Update scalar select with available scalars for currently displayed layer.""" 512 options = [] 513 for field in self.relevant_curing_cell_fields: 514 if not np.isnan(self.curing_mesh_data[layer_id]["input"][field]).all(): 515 options.append(field) 516 if len(options) == 0: 517 options.append("No layers with valid values") 518 self.curing_scalar_select.options = options 519 self.curing_scalar_select.value = options[0]
Update scalar select with available scalars for currently displayed layer.
521 def on_structure_layer_changed(self, value: Bunch): 522 """Event to update UI after selected structure layer to display has changed.""" 523 self.update_available_structure_layer_scalars(value["new"]) 524 self.update_displayed_structure_scalar(self.structure_scalar_select.value)
Event to update UI after selected structure layer to display has changed.
526 def on_curing_layer_changed(self, value: Bunch): 527 """Event to update UI after selected curing layer to display has changed.""" 528 self.update_available_curing_layer_scalars(value["new"]) 529 self.update_displayed_curing_scalar(self.curing_scalar_select.value)
Event to update UI after selected curing layer to display has changed.
531 def on_structure_scalar_changed(self, value: Bunch): 532 """Event to update UI after selected structure scalar to display has changed.""" 533 self.update_displayed_structure_scalar(value["new"])
Event to update UI after selected structure scalar to display has changed.
535 def on_curing_scalar_changed(self, value: Bunch): 536 """Event to update UI after selected curing scalar to display has changed.""" 537 self.update_displayed_curing_scalar(value["new"])
Event to update UI after selected curing scalar to display has changed.
539 def on_structure_deformation_changed(self, value: Bunch): 540 """Event to update UI after value of deformation display has changed.""" 541 self.update_displayed_structure_scalar(self.structure_scalar_select.value)
Event to update UI after value of deformation display has changed.
543 def on_curing_deformation_changed(self, value: Bunch): 544 """Event to update UI after value of deformation display has changed.""" 545 self.update_displayed_curing_scalar(self.curing_scalar_select.value)
Event to update UI after value of deformation display has changed.
547 def update_displayed_structure_scalar(self, value: str): 548 """Update PyVista structure render. 549 550 Args: 551 value: Scalar name to display on 3D mesh 552 """ 553 self.set_plotter_to_structure() 554 if self.structure_deformation_toggle.value == True: 555 mesh_name = "warped" 556 else: 557 mesh_name = "input" 558 for layer_id, structure_layer in self.structure_mesh_data.items(): 559 if structure_layer["output"] != None: 560 self.plotter.remove_actor(structure_layer["output"]) 561 # scalars bars with multiplots have unpredictable behaviour, do not this check 562 if structure_layer["scalar_bar_name"] in self.plotter.scalar_bars: 563 self.plotter.remove_scalar_bar(structure_layer["scalar_bar_name"]) 564 structure_layer["scalar_bar_name"] = None 565 structure_layer["output"] = None 566 if self.structure_limit_actor: 567 self.plotter.remove_actor(self.structure_limit_actor) 568 self.structure_limit_actor = None 569 self.structure_cell_info.value = "" 570 571 data = self.structure_mesh_data[self.structure_layer_select.value] 572 data[mesh_name].cell_data.set_scalars(data[mesh_name][value], "color") 573 if value.startswith("DFAT_"): 574 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["cmap"] 575 clim = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["clim"] 576 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["DFAT"]["log_scale"] 577 title = "DFAT Layer {}".format(self.structure_layer_select.value) 578 self.structure_limit_actor = self.show_limit_value(data[mesh_name], value, limit="max", geometry_type="cell") 579 elif value.startswith("TFAT"): 580 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["cmap"] 581 clim = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["clim"] 582 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["TFAT"]["log_scale"] 583 title = "TFAT Layer {}".format(self.structure_layer_select.value) 584 self.structure_limit_actor = self.show_limit_value(data[mesh_name], value, limit="min", geometry_type="cell") 585 elif value.startswith(self.LAYER_THICKNESS_KEY): 586 cmap = self.STRUCTURE_DISPLAY_DEFINITIONS["THICKNESS"]["cmap"] 587 clim = np.nanmin(data[mesh_name][self.LAYER_THICKNESS_KEY]), np.max(data[mesh_name][self.LAYER_THICKNESS_KEY]) 588 log_scale = self.STRUCTURE_DISPLAY_DEFINITIONS["THICKNESS"]["log_scale"] 589 title = "Thickness(mm) Layer {}".format(self.structure_layer_select.value) 590 else: 591 raise ValueError("No colorscheme for chosen scalar") 592 593 data["output"] = self.plotter.add_mesh(data[mesh_name], show_edges=False, lighting=False, show_scalar_bar=True, scalars="color", 594 cmap=cmap, clim=clim, log_scale=log_scale, scalar_bar_args={'title': title}) 595 data["scalar_bar_name"] = title 596 self.plotter.update()
Update PyVista structure render.
Args: value: Scalar name to display on 3D mesh
598 def update_displayed_curing_scalar(self, value: str): 599 """Update PyVista curing render. 600 601 Args: 602 value: Scalar name to display on 3D mesh 603 """ 604 self.set_plotter_to_curing() 605 if self.curing_deformation_toggle.value == True: 606 mesh_name = "warped" 607 else: 608 mesh_name = "input" 609 for layer_id, curing_layer in self.curing_mesh_data.items(): 610 if curing_layer["output"] != None: 611 self.plotter.remove_actor(curing_layer["output"]) 612 # scalars bars with multiplots have unpredictable behaviour, do not this check 613 if curing_layer["scalar_bar_name"] in self.plotter.scalar_bars: 614 self.plotter.remove_scalar_bar(curing_layer["scalar_bar_name"]) 615 curing_layer["scalar_bar_name"] = None 616 curing_layer["output"] = None 617 if self.curing_limit_actor: 618 self.plotter.remove_actor(self.curing_limit_actor) 619 self.curing_limit_actor = None 620 self.curing_cell_info.value = "" 621 622 data = self.curing_mesh_data[self.curing_layer_select.value] 623 if value.startswith(self.POINT_WARP_KEY): 624 cmap = 'jet' 625 title = value 626 self.curing_limit_actor = self.show_limit_value(data[mesh_name], value, limit="max", geometry_type="point") 627 else: 628 raise ValueError("No colorscheme for chosen scalar") 629 data["output"] = self.plotter.add_mesh(data[mesh_name], show_edges=False, lighting=False, show_scalar_bar=True, scalars=value, 630 cmap=cmap, scalar_bar_args={'title': title}) 631 data["scalar_bar_name"] = title 632 self.plotter.update()
Update PyVista curing render.
Args: value: Scalar name to display on 3D mesh
634 def show_limit_value(self, mesh: pv.UnstructuredGrid, scalar: str, limit: str='max', geometry_type: str='cell') -> vtkActor2D: 635 """Render point with tooltip at place of maximum value in PyVista. 636 637 Args: 638 mesh: 3D mesh with cells to check for values 639 scalar: Name of scalar to check for values 640 limit: 'max' or 'min' 641 642 Returns: 643 vtk actor of added max value point 644 """ 645 if limit == 'max': 646 limit_value = np.nanmax(mesh[scalar]) 647 fmt = "Max: %.5f" 648 else: 649 limit_value = np.nanmin(mesh[scalar]) 650 fmt = "Min: %.5f" 651 652 if geometry_type == "cell": 653 limit_cell = mesh.remove_cells(np.argwhere(mesh[scalar] != limit_value)) 654 point = limit_cell.points[0] 655 scalar_value = [limit_cell[scalar]] 656 elif geometry_type == "point": 657 limit_cells = mesh.extract_points(np.argwhere(mesh[scalar] == limit_value)) 658 point = limit_cells.points[0] 659 scalar_value = [limit_cells[scalar][0]] 660 661 return self.plotter.add_point_scalar_labels(point, scalar_value, name="{}_limit_value_point".format(geometry_type), fmt=fmt, point_size=20, shape="rounded_rect", 662 point_color="red", render_points_as_spheres=True, fill_shape=True, shape_color="pink", text_color="red",shadow=True, tolerance=1)
Render point with tooltip at place of maximum value in PyVista.
Args: mesh: 3D mesh with cells to check for values scalar: Name of scalar to check for values limit: 'max' or 'min'
Returns: vtk actor of added max value point
664 def selection_callback(self, cell: pv.Cell): 665 """Event to display detail information for selected cell.""" 666 display_html = '<p class="global_headline"> Selected cell: </p>' 667 if cell["mesh_id"] == self.CURING_MESH_ID: 668 for prop in cell.point_data: 669 if not np.isnan(cell[prop].any()): 670 display_html += "<b>{property_name}</b>: {property_value} \n".format(property_name=prop, property_value=cell[prop][0]) 671 self.curing_cell_info.value = display_html 672 else: 673 for prop in cell.cell_data: 674 if not np.isnan(cell[prop]): 675 display_html += "<b>{property_name}</b>: {property_value} \n".format(property_name=prop, property_value=cell[prop][0]) 676 self.structure_cell_info.value = display_html
Event to display detail information for selected cell.
678 def set_plotter_to_curing(self): 679 """Set active plot to curing simulation, redirecting call to plotter.""" 680 self.plotter.subplot(0,0)
Set active plot to curing simulation, redirecting call to plotter.
682 def set_plotter_to_structure(self): 683 """Set active plot to structure simulation, redirecting call to plotter.""" 684 self.plotter.subplot(1,0)
Set active plot to structure simulation, redirecting call to plotter.
686 def load_local_data(self, ui_element=None): 687 """Use locally stored data for UI. 688 689 Args: 690 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 691 """ 692 self.conn = store_connection.LocalStoreConnection("sensotwin_world") 693 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
695 def load_remote_data(self, ui_element=None): 696 """Use remotely stored data for UI. 697 698 Args: 699 ui_element: Override for ipywidgets to not pass the UI element that triggered the event 700 """ 701 self.conn = store_connection.FusekiConnection(self.remote_database_input.value) 702 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
704 def load_input_set_data(self): 705 """Load data from previously set input source and populate UI.""" 706 self.temperature_input_set_data = temperature_config.MaterialHardeningInputDataSet.get_all_entries_from_store(self.conn) 707 if self.temperature_input_set_data: 708 options = [] 709 for key, input_set in self.temperature_input_set_data.items(): 710 label = input_set.generate_input_set_display_label() 711 options.append((label, key)) 712 self.temperature_input_selection.options = options 713 self.temperature_input_selection.value = list(self.temperature_input_set_data.keys())[0] 714 715 self.defect_input_set_data = defect_config.DefectInputDataSet.get_all_entries_from_store(self.conn) 716 if self.defect_input_set_data: 717 options = [] 718 for key, input_set in self.defect_input_set_data.items(): 719 label = input_set.generate_input_set_display_label() 720 options.append((label, key)) 721 self.defect_input_selection.options = options 722 self.defect_input_selection.value = list(self.defect_input_set_data.keys())[0] 723 724 self.stress_input_set_data = stress_config.StressStrainInputDataSet.get_all_entries_from_store(self.conn) 725 if self.stress_input_set_data: 726 options = [] 727 for key, input_set in self.stress_input_set_data.items(): 728 label = input_set.generate_input_set_display_label() 729 options.append((label, key)) 730 self.stress_input_selection.options = options 731 self.stress_input_selection.value = list(self.stress_input_set_data.keys())[0]
Load data from previously set input source and populate UI.