diff --git a/doc/changelog.d/6495.added.md b/doc/changelog.d/6495.added.md new file mode 100644 index 00000000000..ad021c6047e --- /dev/null +++ b/doc/changelog.d/6495.added.md @@ -0,0 +1 @@ +Icepak model reviewer extension \ No newline at end of file diff --git a/doc/source/User_guide/pyaedt_extensions_doc/icepak/icepak_model_reviewer.rst b/doc/source/User_guide/pyaedt_extensions_doc/icepak/icepak_model_reviewer.rst new file mode 100644 index 00000000000..5b6e192ccda --- /dev/null +++ b/doc/source/User_guide/pyaedt_extensions_doc/icepak/icepak_model_reviewer.rst @@ -0,0 +1,111 @@ +Icepak Model Reviewer +===================== + +The Icepak Model Reviewer extension allows users to inspect and modify details of an Icepak project. It enables +reviewing critical data such as boundary conditions, materials, and model-specific properties. + +The extension provides a graphical user interface (GUI) where the user can load a project, review its components, +and make necessary updates. + +The following image shows the extension GUI: + +.. image:: ../../../_static/extensions/icepak_model_reviewer_initial_panel.png + :width: 800 + :alt: Icepak Model Reviewer UI + +After the project is loaded by clicking, the GUI show tables +.. image:: ../../../_static/extensions/icepak_model_reviewer_ui_with_table.png + :width: 800 + :alt: Icepak Model Reviewer UI After loading Project + +Features +-------- + +- Load and inspect an Icepak project. +- Display editable tables showing: + - Boundary conditions + - Material properties + - Model details +- Update project parameters. + +Using the Extension +------------------- + +1. Open the **Automation** tab in the Icepak interface. +2. Click on the **Icepak Model Reviewer** icon under the Extension Manager. +3. In the GUI, interact with the following elements: + - **Load Project**: Load the Icepak project into the GUI. + - **Update Project**: Push any modifications made in the tables back to the project. +4. After the project is loaded: + - The GUI displays three main tables: + - **Boundary Conditions**: Lists all the boundary condition settings applied in the model. + - **Materials**: Shows the materials used and their key properties. + - **Models**: Displays model-specific data and configurations. + +Each table allows inline editing of values. To commit changes, click the **Update Project** button. This will apply all modifications to the currently loaded project. + +Tables Overview +--------------- + +**Boundary Conditions Table** + +This table displays the thermal boundary conditions defined in the Icepak project. +The data is organized with both read-only and editable fields to help users review and make updates. ++--------------------------+----------------------------------------------------------------------------------+ +| **Property** | **Description** | ++==========================+==================================================================================+ +| **Name** | Name of the boundary. This field is **read-only** and cannot be modified. | ++--------------------------+----------------------------------------------------------------------------------+ +| **Boundary Type** | Indicates whether the boundary is a *Block* or a *Source*. This is | +| | also a **read-only** field. | ++--------------------------+----------------------------------------------------------------------------------+ +| **BC Name 1 / BC Name 2**| Names of the boundary conditions applied (*Total Power*, *Power Density*, | +| | *Heat Transfer Coefficient*, *Temperature*). These fields are **read-only** | ++--------------------------+----------------------------------------------------------------------------------+ +| **Value 1 / Value 2** | Corresponding values for the selected boundary conditions. These fields | +| | are **editable**. | ++--------------------------+----------------------------------------------------------------------------------+ + + +**Materials Table** + + +This table displays material properties for all objects used in the Icepak project. +Some fields are read-only and provided for reference, while others can be modified depending on the simulation setup. + ++----------------------------------+--------------------------------------------------------------+ +| **Property** | **Description** | ++==================================+==============================================================+ +| **Name** | Material name (**read-only**) | ++----------------------------------+--------------------------------------------------------------+ +| **Material Type** | Indicates if the material is *Solid* or *Fluid* | +| | (**read-only**) | ++----------------------------------+--------------------------------------------------------------+ +| **Thermal Conductivity** | Thermal conductivity in **W/m·K** | ++----------------------------------+--------------------------------------------------------------+ +| **Mass Density** | Density in **kg/m³** | ++----------------------------------+--------------------------------------------------------------+ +| **Specific Heat** | Specific heat capacity in **J/kg·K** | ++----------------------------------+--------------------------------------------------------------+ +| **Thermal Expansion Coefficient**| Coefficient of thermal expansion in **1/K** | ++----------------------------------+--------------------------------------------------------------+ +| **Thermal Diffusivity** | Thermal diffusivity in **m²/s** | ++----------------------------------+--------------------------------------------------------------+ +| **Viscosity** | Dynamic viscosity in **Kg/m-s** | ++----------------------------------+--------------------------------------------------------------+ + + +**Models Table** + +This table lists the object-level settings related to modeling in the Icepak project. +It shows the assigned bulk and surface materials. Information about whether an object is model/ non-model object. + + + +Notes +----- + +- Ensure the CSV file for power map creation is formatted correctly before importing. +- Changes made in the tables are not applied to the project until the **Update Project** button is clicked. +- This extension is intended for reviewing model data efficiently without the need to navigate complex menus. + diff --git a/doc/source/_static/extensions/icepak_model_reviewer_initial_panel.png b/doc/source/_static/extensions/icepak_model_reviewer_initial_panel.png new file mode 100644 index 00000000000..1f44e1b785a Binary files /dev/null and b/doc/source/_static/extensions/icepak_model_reviewer_initial_panel.png differ diff --git a/doc/source/_static/extensions/icepak_model_reviewer_ui_with_table.png b/doc/source/_static/extensions/icepak_model_reviewer_ui_with_table.png new file mode 100644 index 00000000000..41690962900 Binary files /dev/null and b/doc/source/_static/extensions/icepak_model_reviewer_ui_with_table.png differ diff --git a/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer.py b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer.py new file mode 100644 index 00000000000..37d002fc7b8 --- /dev/null +++ b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import tkinter as tk +from tkinter import ttk + +from icepak_model_reviewer.backend import export_config_file +from icepak_model_reviewer.backend import get_object_id_mapping +from icepak_model_reviewer.backend import import_config_file +from icepak_model_reviewer.configuration_data_processing import compare_and_update_boundary_data +from icepak_model_reviewer.configuration_data_processing import compare_and_update_material_data +from icepak_model_reviewer.configuration_data_processing import compare_and_update_model_data +from icepak_model_reviewer.configuration_data_processing import extract_boundary_data +from icepak_model_reviewer.configuration_data_processing import extract_material_data +from icepak_model_reviewer.configuration_data_processing import extract_model_data + +from ansys.aedt.core import * +from ansys.aedt.core.extensions.misc import ExtensionCommon +from ansys.aedt.core.extensions.misc import ExtensionTheme +from ansys.aedt.core.extensions.misc import get_aedt_version +from ansys.aedt.core.extensions.misc import get_port +from ansys.aedt.core.extensions.misc import get_process_id +from ansys.aedt.core.extensions.misc import is_student + +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() +theme = ExtensionTheme() +EXTENSION_TITLE = "Icepak Model Reviewer" + + +def flatten_list(mixed_list): + flat_list = [] + for row in mixed_list: + result = [] + for item in row: + if isinstance(item, list): + result.append(",".join(str(sub) for sub in item)) + else: + result.append(str(item)) + flat_list.append(result) + return flat_list + + +def expand_list(flat_list): + mixed_list = [] + for row in flat_list: + result = [] + for item in row: + if isinstance(item, str) and "," in item: + parts = [s.strip() for s in item.split(",")] + result.append(parts) + else: + result.append(str(item)) + mixed_list.append(result) + return mixed_list + + +def add_icon_to_cells(data, icon_indices, icon="🔒"): + """ + Adds an icon to specified cells in the data. + Parameters: + - data: list of list of strings (table rows) + - icon_indices: list of list of ints, where each inner list contains read only column indices + - icon: string icon to prepend (default '🔒') + + Returns: + - new_data: deep copy of data with icons added + """ + from copy import deepcopy + + new_data = deepcopy(data) + + for row_idx, cols in enumerate(icon_indices): + for col_idx in cols: + cell = new_data[row_idx][col_idx - 1] + if isinstance(cell, str): + new_data[row_idx][col_idx - 1] = f"{cell}{icon}" + elif isinstance(cell, list): + cell[-1] += icon + new_data[row_idx][col_idx - 1] = cell + return new_data + + +def remove_icon_from_cells(data, icon="🔒"): + """ + Removes the icon from all cells that start with it. + + Parameters: + - data: list of list of strings + - icon: the icon to remove (default '🔒') + + Returns: + - new_data: deep copy of data with icons removed + """ + from copy import deepcopy + + new_data = deepcopy(data) + + for i, row in enumerate(new_data): + for j, cell in enumerate(row): + if isinstance(cell, str): + if cell.endswith(f"{icon}"): # cells can also be list in case of multiple selection + new_data[i][j] = cell[: -(len(icon))] + if isinstance(cell, list): + if cell[-1].endswith(icon): + cell[-1] = cell[-1][: -(len(icon))] + return new_data + + +def add_table_to_tab(tab, table_data): + for child in tab.winfo_children(): + child.destroy() + headings, type_list, selection_dict, row_data, read_only_cols = table_data + table = Table(tab, headings, type_list, read_only_cols) + table.pack(fill=tk.BOTH, expand=True) + for column_name, options in selection_dict.items(): + table.set_multi_select_options(column_name, options) + row_data_with_icon = add_icon_to_cells(flatten_list(row_data), read_only_cols) + for row in row_data_with_icon: + table.add_row(row) + return table + + +class Table(tk.Frame): + def __init__(self, parent, headers, types, read_only_data): + super().__init__(parent) + self.headers = ["✔"] + headers + print("read only data") + print(read_only_data) + self.types = ["checkbox"] + types + self.read_only_data = [set(r) for r in read_only_data] + # style.configure("Treeview", font=theme.default_font) + self.tree = ttk.Treeview(self, columns=self.headers, show="headings", selectmode="browse") + for i, header in enumerate(self.headers): + self.tree.heading(header, text=header) + self.tree.column(header, width=50 if i == 0 else 140, anchor=tk.CENTER) + + self.tree.pack(fill=tk.BOTH, expand=True) + self.tree.bind("", self.edit_cell) + + self.rows_data = [] + self.multi_select_options = {} + self.selected_rows = set() + + def set_multi_select_options(self, header, options): + self.multi_select_options[header] = options + + def add_row(self, row_data): + print("adding row") + print(row_data) + if len(row_data) != len(self.headers) - 1: + raise ValueError("Row data must match the number of non-checkbox columns") + + full_data = ["⬜"] + row_data + self.rows_data.append(full_data) + row_id = self.tree.insert("", "end", values=full_data) + + def toggle_row(self, row_id, item_index): + if row_id in self.selected_rows: + self.selected_rows.remove(row_id) + self.tree.set(row_id, 0, "⬜") + else: + self.selected_rows.add(row_id) + self.tree.set(row_id, 0, "✅") + + def get_modified_data(self): + return [row[1:] for row in self.rows_data] # exclude checkbox column + + def edit_cell(self, event): + region = self.tree.identify("region", event.x, event.y) + if region != "cell": + return + + row_id = self.tree.identify_row(event.y) + col = int(self.tree.identify_column(event.x)[1:]) - 1 + item_index = self.tree.index(row_id) + + if col == 0: + self.toggle_row(row_id, item_index) + return + + if col in self.read_only_data[item_index]: + return + + bbox = self.tree.bbox(row_id, f"#{col + 1}") + if not bbox: + return + + x, y, width, height = bbox + abs_x = self.tree.winfo_rootx() + x + abs_y = self.tree.winfo_rooty() + y + + item = self.tree.item(row_id) + value = item["values"][col] + type_ = self.types[col] + + def apply_to_selected(new_value): + for rid in self.tree.get_children(): + if row_id in self.selected_rows: + targets = self.selected_rows + else: + targets = {row_id} + if rid in targets: + idx = self.tree.index(rid) + if col not in self.read_only_data[idx]: + self.tree.set(rid, col, new_value) + self.rows_data[idx][col] = new_value + + if type_ == "text": + entry = tk.Entry(self.tree) + entry.insert(0, value) + entry.place(x=x, y=y, width=width, height=height) + entry.focus_set() + + def on_return(event): + new_value = entry.get() + apply_to_selected(new_value) + entry.destroy() + + entry.bind("", on_return) + entry.bind("", lambda e: entry.destroy()) + + elif type_ == "combo": + options = self.multi_select_options.get(self.headers[col], []) + combo = ttk.Combobox( + self.tree, values=options, state="readonly", style="PyAEDT.TCombobox", font=theme.default_font + ) + combo.place(x=x, y=y, width=width, height=height) + combo.set(value) + + def on_select(event=None): + new_value = combo.get() + apply_to_selected(new_value) + combo.destroy() + + combo.bind("<>", on_select) + combo.bind("", lambda e: combo.destroy()) + combo.focus_set() + + elif type_ == "multiple_text": + options = self.multi_select_options.get(self.headers[col], []) + top = tk.Toplevel(self) + top.title("Select Multiple") + top.geometry(f"200x300+{abs_x}+{abs_y}") + + listbox = tk.Listbox(top, selectmode="multiple", font=theme.default_font) + for opt in options: + listbox.insert(tk.END, opt) + listbox.pack(fill="both", expand=True) + + current_vals = [val.strip() for val in value.split(",")] + for idx, opt in enumerate(options): + if opt in current_vals: + listbox.selection_set(idx) + + def confirm(): + selected = [options[i] for i in listbox.curselection()] + value_str = ", ".join(selected) + apply_to_selected(value_str) + top.destroy() + + ttk.Button(top, text="OK", command=confirm).pack() + top.transient(self) + top.grab_set() + self.wait_window(top) + + +class IcepakModelReviewer(ExtensionCommon): + def __init__(self, withdraw: bool = False): + # Initialize the common extension class with the title and theme color + super().__init__( + EXTENSION_TITLE, + theme_color="light", + withdraw=withdraw, + add_custom_content=False, + ) + + self.add_extension_content() + + def add_extension_content(self): + # --- Top Button Panel --- + button_frame = ttk.Frame(self.root, style="PyAEDT.TFrame") + button_frame.pack(fill=tk.X, padx=10, pady=5) + notebook = ttk.Notebook(self.root, style="TNotebook") + notebook.pack(fill=tk.BOTH, expand=True) + self.root.boundary_tab = ttk.Frame(notebook, style="PyAEDT.TFrame") + notebook.add(self.root.boundary_tab, text="Boundary") + self.root.materials_tab = ttk.Frame(notebook, style="PyAEDT.TFrame") + notebook.add(self.root.materials_tab, text="Material") + self.root.models_tab = ttk.Frame(notebook, style="PyAEDT.TFrame") + notebook.add(self.root.models_tab, text="Models") + ttk.Button(button_frame, text="Load Project", command=self.load_project, style="PyAEDT.TButton").pack( + side=tk.LEFT, padx=5 + ) + ttk.Button(button_frame, text="Update Project", command=self.update_project, style="PyAEDT.TButton").pack( + side=tk.LEFT, padx=5 + ) + + def load_project(self): + print("Loading project...") + + desktop = Desktop( + new_desktop=False, + version=version, + port=port, + aedt_process_id=aedt_process_id, + student_version=is_student(), + ) + + ipk = Icepak() + data = export_config_file(ipk) + print("config_file_exported") + # print(data) + self.root.json_data = data + desktop.release_desktop(close_projects=False, close_on_exit=False) + # --- Tabbed Interface --- + table_data = extract_boundary_data(data) + self.root.bc_table = add_table_to_tab(self.root.boundary_tab, table_data) + table_data = extract_material_data(data) + self.root.mat_table = add_table_to_tab(self.root.materials_tab, table_data) + table_data = extract_model_data(data) + self.root.model_table = add_table_to_tab(self.root.models_tab, table_data) + + def update_project(self): + print("Updating project...") + desktop = Desktop( + new_desktop=False, + version=version, + port=port, + aedt_process_id=aedt_process_id, + student_version=is_student(), + ) + + ipk = Icepak() + obj_mapping = get_object_id_mapping(ipk) + bc_data = self.root.bc_table.get_modified_data() + bc_data = expand_list(remove_icon_from_cells(bc_data)) + differences, new_bc_data = compare_and_update_boundary_data(self.root.json_data, bc_data, obj_mapping) + print(differences) + mat_data = self.root.mat_table.get_modified_data() + mat_data = expand_list(remove_icon_from_cells(mat_data)) + differences, new_mat_data = compare_and_update_material_data(self.root.json_data, mat_data) + print(differences) + model_data = self.root.model_table.get_modified_data() + model_data = expand_list(remove_icon_from_cells(model_data)) + differences, new_model_data = compare_and_update_model_data(self.root.json_data, model_data) + combined_data = {**new_model_data, **new_mat_data, **new_bc_data} + print(differences) + import_config_file(ipk, combined_data) + desktop.release_desktop(False, False) + + +# === Main Application === +if __name__ == "__main__": + # main() + extension = IcepakModelReviewer(withdraw=False) + tk.mainloop() diff --git a/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/__init__.py b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/__init__.py new file mode 100644 index 00000000000..b78d8fed76c --- /dev/null +++ b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/backend.py b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/backend.py new file mode 100644 index 00000000000..4691d40e58c --- /dev/null +++ b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/backend.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json +import os + + +def export_config_file(aedtapp): + aedtapp.configurations.options.export_monitor = False + aedtapp.configurations.options.export_native_components = False + aedtapp.configurations.options.export_datasets = False + aedtapp.configurations.options.export_parametrics = False + aedtapp.configurations.options.export_variables = False + aedtapp.configurations.options.export_mesh_operations = False + aedtapp.configurations.options.export_optimizations = False + config_file = aedtapp.configurations.export_config() + with open(config_file, "r") as file: + data = json.load(file) + return data + + +def import_config_file(aedtapp, json_data): + full_path = os.path.abspath("load.json") + with open(full_path, "w") as file: + json.dump(json_data, file) + print(f"json file path is {full_path}") + out = aedtapp.configurations.import_config(full_path) + result = aedtapp.configurations.validate(out) + if result: + print("sucessfully imported configuration") + else: + print("import has issues") + return None + + +def get_object_id_mapping(aedtapp): + object_id_map = {name: aedtapp.modeler.get_obj_id(name) for name in aedtapp.modeler.object_names} + return object_id_map diff --git a/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/configuration_data_processing.py b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/configuration_data_processing.py new file mode 100644 index 00000000000..d34cb2ba34d --- /dev/null +++ b/src/ansys/aedt/core/extensions/icepak/icepak_model_reviewer/configuration_data_processing.py @@ -0,0 +1,452 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import copy + + +def extract_boundary_data(data): + boundary_data = [] + object_mapping = data.get("general", {}).get("object_mapping", {}) + read_only_cols = [] + selection_dict = {} + headings = ["Boundary Name", "Boundary Type", "Selected Objects", "BC Name 1", "Value 1", "BC Name 2", "Value 2"] + type_list = ["text", "text", "multiple_text", "text", "text", "text", "text"] + # Convert mapping to ensure all values are names (some are lists) + id_to_name = {} + for obj_id, value in object_mapping.items(): + if isinstance(value, list): # Some entries have extra data in a list + id_to_name[obj_id] = value[0] # Extract the name + else: + id_to_name[obj_id] = value # Direct mapping + + all_objects = [obj_name for obj_name, obj_details in data.get("objects", {}).items()] + selection_dict["Selected Objects"] = all_objects + + for boundary_name, boundary_details in data.get("boundaries", {}).items(): + cols = [1, 2, 4, 6] + boundary_type = boundary_details.get("BoundType", "") + # block_type = boundary_details.get("Block Type", "") # Solid or Fluid + if boundary_type == "Block": + use_total_power = boundary_details.get("Use Total Power", "") + use_external_conditions = boundary_details.get("Use External Conditions", "") + if use_total_power: + bc_name1 = "Total Power" + bc_name2 = "N/A" + value2 = "0.0" + value1 = boundary_details.get("Total Power", "0W") # Default to "0W" if missing + object_ids = boundary_details.get("Objects", []) # Default to empty list if missing + selected_objects = [id_to_name[str(obj_id)] for obj_id in object_ids] + boundary_data.append( + [boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2] + ) + cols.append(7) + read_only_cols.append(cols) + elif use_external_conditions: + bc_name1 = "Heat Transfer Coefficient" + bc_name2 = "Temperature" + value1 = boundary_details.get("Heat Transfer Coefficient", "0w_per_m2k") + value2 = boundary_details.get("Temperature", "0cel") + object_ids = boundary_details.get("Objects", []) # Default to empty list if missing + selected_objects = [id_to_name[str(obj_id)] for obj_id in object_ids] + boundary_data.append( + [boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2] + ) + read_only_cols.append(cols) + else: + bc_name1 = "Power Density" + bc_name2 = "N/A" + value2 = "0.0" + value1 = boundary_details.get("Power Density", "0w_per_m3") + object_ids = boundary_details.get("Objects", []) # Default to empty list if missing + selected_objects = [id_to_name[str(obj_id)] for obj_id in object_ids] + boundary_data.append( + [boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2] + ) + cols.append(7) + read_only_cols.append(cols) + + if boundary_type == "Source": + thermal_condition = boundary_details.get("Thermal Condition", "") + if thermal_condition == "Surface Flux": + bc_name1 = "Surface Heat" + bc_name2 = "N/A" + value1 = boundary_details.get("Surface Heat", "0kW_per_m2") + value2 = "0.0" + object_ids = boundary_details.get("Faces", []) # Default to empty list if missing + print(f"Faces id {object_ids}") + # selected_objects = [id_to_name[str(obj_id)] for obj_id in object_ids] + # print(f"converted faces id {selected_objects}") + selected_objects = [str(ids) for ids in object_ids] + boundary_data.append( + [boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2] + ) + cols.append(3) + cols.append(7) + read_only_cols.append(cols) + if thermal_condition == "Total Power": + bc_name1 = "Total Power" + bc_name2 = "N/A" + value1 = boundary_details.get("Total Power", "0W") + value2 = "0.0" + object_ids = boundary_details.get("Faces", []) # Default to empty list if missing + # selected_objects = [id_to_name[str(obj_id)] for obj_id in object_ids] + selected_objects = [str(ids) for ids in object_ids] + boundary_data.append( + [boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2] + ) + cols.append(3) + cols.append(7) + read_only_cols.append(cols) + if thermal_condition == "Fixed Temperature": + bc_name1 = "Fixed Temperature" + bc_name2 = "N/A" + value1 = boundary_details.get("Temperature", "0cel") + value2 = "0.0" + object_ids = boundary_details.get("Faces", []) # Default to empty list if missing + # selected_objects = [id_to_name[str(obj_id)] for obj_id in object_ids] + selected_objects = [str(ids) for ids in object_ids] + boundary_data.append( + [boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2] + ) + cols.append(3) + cols.append(7) + read_only_cols.append(cols) + return headings, type_list, selection_dict, boundary_data, read_only_cols + + +def extract_model_data(data): + object_data = [] + bulk_materials = set() + surface_materials = set() + headings = ["Object Name", "Bulk Material", "Surface Material", "Solve Inside", "Modeling"] + type_list = ["text", "combo", "combo", "combo", "combo"] + selection_dict = {"Solve Inside": ["True", "False"], "Modeling": ["Model", "Non-Model"]} + read_only_cols = [] + + for obj_name, obj_details in data.get("objects", {}).items(): + cols = [1] + component_name = obj_name + bulk_material = obj_details.get("Material", "") + surface_material = obj_details.get("SurfaceMaterial", "") + solve = obj_details.get("SolveInside", "") + if solve: + solve_inside = "True" + else: + solve_inside = "False" + + model = obj_details.get("Model", "") + + if model: + modelling = "Model" + else: + modelling = "Non-Model" + + if bulk_material == '""' or bulk_material == "": + bulk_material = "Not Specified" + bulk_materials.add(bulk_material) + else: + bulk_materials.add(bulk_material) + + if surface_material == '""': + surface_material = surface_material.strip('"') + data["objects"][obj_name]["SurfaceMaterial"] = surface_material + surface_material = "Not Specified" + surface_materials.add(surface_material) + elif surface_material == "": + surface_material = "Not Specified" + surface_materials.add(surface_material) + elif surface_material == None: + surface_material = "Not Specified" + surface_materials.add(surface_material) + else: + surface_material = surface_material.strip('"') + data["objects"][obj_name]["SurfaceMaterial"] = surface_material # removing "" in the source + surface_materials.add(surface_material) + + object_data.append([component_name, bulk_material, surface_material, solve_inside, modelling]) + read_only_cols.append(cols) + selection_dict["Bulk Material"] = list(bulk_materials) + selection_dict["Surface Material"] = list(surface_materials) + return headings, type_list, selection_dict, object_data, read_only_cols + + +def extract_material_data(data): + materials = data.get("materials", {}) + extracted_data = [] + read_only_cols = [] + headings = [ + "Material Name", + "Material Type", + "Thermal Conductivity", + "Mass Density", + "Specific Heat", + "Thermal Expansion Coefficient", + "Diffusivity", + "Viscosity", + ] + type_list = ["text", "text", "text", "text", "text", "text", "text", "text"] + selection_dict = {} + + for mat_name, mat_details in materials.items(): + cols = [1] + material_type = mat_details.get("thermal_material_type", {}).get("Choice", "N/A") + # make material type read only + cols.append(2) + if material_type == "Solid" or material_type == "N/A": + cols.append(8) + thermal_conductivity = mat_details.get("thermal_conductivity", "N/A") + # Handle anisotropic thermal conductivity case + if isinstance(thermal_conductivity, dict): + thermal_conductivity = thermal_conductivity["property_type"] + cols.append(3) + mass_density = mat_details.get("mass_density", "N/A") + specific_heat = mat_details.get("specific_heat", "N/A") + thermal_expansion_coefficient = mat_details.get("thermal_expansion_coefficient", "N/A") + + diffusivity = mat_details.get("diffusivity", "N/A") + viscosity = mat_details.get("viscosity", "N/A") + + extracted_data.append( + [ + mat_name, + material_type, + thermal_conductivity, + mass_density, + specific_heat, + thermal_expansion_coefficient, + diffusivity, + viscosity, + ] + ) + read_only_cols.append(cols) + return headings, type_list, selection_dict, extracted_data, read_only_cols + + +def compare_and_update_boundary_data(original_data, modified_object_data, object_mapping): + """ + Compares modified object data with the original data and identifies differences. + Also returns the updated original data with the modifications applied. + """ + # updated_data = copy.deepcopy(original_data) # Make a copy to avoid modifying original + differences = [] + inverse_mapping = {str(v): k for k, v in object_mapping.items()} + + updated_data = {"boundaries": {}, "general": {}} + updated_data["general"]["object_mapping"] = {**original_data["general"]["object_mapping"], **inverse_mapping} + modified_boundary = set() + for row in modified_object_data: + (boundary_name, boundary_type, selected_objects, bc_name1, value1, bc_name2, value2) = row + + original_obj = original_data["boundaries"][boundary_name] + updated_data["boundaries"][boundary_name] = copy.deepcopy(original_data["boundaries"][boundary_name]) + + # Convert string values back to boolean where necessary + print("printing selected objects") + print(selected_objects) + print(object_mapping) + + # Check for differences and update + if isinstance(selected_objects, list): + selected_object_ids = [int(i) if str(i).isdigit() else object_mapping[i] for i in selected_objects] + selected_objects_list = [i for i in selected_objects] + if isinstance(selected_objects, str): + selected_object_ids = [ + int(i.strip()) if str(i.strip()).isdigit() else object_mapping[i.strip()] + for i in selected_objects.split(",") + ] + selected_objects_list = [i.strip() for i in selected_objects.split(",")] + + original_obj_ids = original_obj.get("Objects", []) + if original_obj_ids: + if set(original_obj_ids) != set(selected_object_ids): + differences.append(f"{boundary_name}: {bc_name1} selected objects changed to '{selected_objects}'") + # updated_data["boundaries"][boundary_name]["Objects"] = selected_object_ids + updated_data["boundaries"][boundary_name]["Objects"] = selected_objects_list + modified_boundary.add(boundary_name) + + if original_obj.get(bc_name1, "") != value1: + differences.append( + f"{boundary_name}: {bc_name1} changed from '{original_obj.get(bc_name1, '')}' to '{value1}'" + ) + updated_data["boundaries"][boundary_name][bc_name1] = value1 + modified_boundary.add(boundary_name) + + if bc_name2 != "N/A": + if original_obj.get(bc_name2, "") != value2: + differences.append( + f"{boundary_name}: {bc_name1} changed from '{original_obj.get(bc_name1, '')}' to '{value1}'" + ) + updated_data["boundaries"][boundary_name][bc_name2] = value2 + modified_boundary.add(boundary_name) + + modified_data = {"boundaries": {}, "general": {}} + modified_data["general"]["object_mapping"] = {**original_data["general"]["object_mapping"], **inverse_mapping} + for boundary_name in modified_boundary: + modified_data["boundaries"][boundary_name] = copy.deepcopy(updated_data["boundaries"][boundary_name]) + + return differences, modified_data + + +def compare_and_update_model_data(original_data, modified_object_data): + """ + Compares modified object data with the original data and identifies differences. + Also returns the updated original data with the modifications applied. + """ + # updated_data = copy.deepcopy(original_data) # Make a copy to avoid modifying original + updated_data = {"objects": {}} + differences = [] + modified_objects = set() + # Convert object data into a lookup dictionary for easy comparison + for row in modified_object_data: + obj_name, bulk_material, surface_material, solve_inside, modelling = row + + original_obj = original_data["objects"][obj_name] + updated_data["objects"][obj_name] = copy.deepcopy(original_data["objects"][obj_name]) + + # Convert string values back to boolean where necessary + solve_inside_bool = solve_inside == "True" + modelling_bool = modelling == "Model" + + # Check for differences and update + if not bulk_material == "Not Specified": + if original_obj.get("Material", "") != bulk_material: + differences.append( + f"{obj_name}: Bulk Material changed from '{original_obj.get('Material', '')}' to '{bulk_material}'" + ) + updated_data["objects"][obj_name]["Material"] = bulk_material + modified_objects.add(obj_name) + + if not surface_material == "Not Specified": + if original_obj.get("SurfaceMaterial", "") != surface_material: + differences.append( + f"{obj_name}: Surface Material changed from '{original_obj.get('SurfaceMaterial', '')}' to '{surface_material}'" + ) + updated_data["objects"][obj_name]["SurfaceMaterial"] = surface_material + modified_objects.add(obj_name) + + if original_obj.get("SolveInside", "") != solve_inside_bool: + differences.append( + f"{obj_name}: Solve Inside changed from '{original_obj.get('SolveInside', '')}' to '{solve_inside_bool}'" + ) + updated_data["objects"][obj_name]["SolveInside"] = solve_inside_bool + modified_objects.add(obj_name) + + if original_obj.get("Model", "") != modelling_bool: + differences.append( + f"{obj_name}: Modeling changed from '{original_obj.get('Model', '')}' to '{modelling_bool}'" + ) + updated_data["objects"][obj_name]["Model"] = modelling_bool + updated_data["objects"][obj_name]["SurfaceMaterial"] = None + modified_objects.add(obj_name) + + modified_data = {"objects": {}} + for obj_name in modified_objects: + modified_data["objects"][obj_name] = copy.deepcopy(updated_data["objects"][obj_name]) + + return differences, modified_data + + +def compare_and_update_material_data(original_data, modified_object_data): + """ + Compares modified object data with the original data and identifies differences. + Also returns the updated original data with the modifications applied. + """ + # updated_data = copy.deepcopy(original_data) # Make a copy to avoid modifying original + differences = [] + updated_data = {"materials": {}} + # Convert object data into a lookup dictionary for easy comparison + modified_materials = set() + for row in modified_object_data: + ( + mat_name, + material_type, + thermal_conductivity, + mass_density, + specific_heat, + thermal_expansion_coefficient, + diffusivity, + viscosity, + ) = row + + original_obj = original_data["materials"][mat_name] + updated_data["materials"][mat_name] = copy.deepcopy(original_data["materials"][mat_name]) + + # Check for differences and update + if not isinstance(original_obj.get("thermal_conductivity"), dict): + if original_obj.get("thermal_conductivity", "") != thermal_conductivity: + differences.append( + f"{mat_name}: Thermal Conductivity changed from '{original_obj.get('thermal_conductivity', '')}' to '{thermal_conductivity}'" + ) + updated_data["materials"][mat_name]["thermal_conductivity"] = thermal_conductivity + modified_materials.add(mat_name) + + if original_obj.get("mass_density", "") != mass_density: + differences.append( + f"{mat_name}: Mass density changed from '{original_obj.get('mass_density', '')}' to '{mass_density}'" + ) + updated_data["materials"][mat_name]["mass_density"] = mass_density + modified_materials.add(mat_name) + + if original_obj.get("specific_heat", "") != specific_heat: + differences.append( + f"{mat_name}: specific heat changed from '{original_obj.get('specific_heat', '')}' to '{specific_heat}'" + ) + updated_data["materials"][mat_name]["specific_heat"] = specific_heat + modified_materials.add(mat_name) + + if original_obj.get("thermal_expansion_coefficient", "") != thermal_expansion_coefficient: + differences.append( + f"{mat_name}: Thermal expansion coefficient changed " + f"from '{original_obj.get('thermal_expansion_coefficient', '')}' to '{thermal_expansion_coefficient}'" + ) + updated_data["materials"][mat_name]["thermal_expansion_coefficient"] = thermal_expansion_coefficient + modified_materials.add(mat_name) + + if original_obj.get("diffusivity", "") != diffusivity: + differences.append( + f"{mat_name}: diffusivity changed from '{original_obj.get('diffusivity', '')}' to '{diffusivity}'" + ) + updated_data["materials"][mat_name]["diffusivity"] = diffusivity + modified_materials.add(mat_name) + + if original_obj.get("viscosity", "") != viscosity: + differences.append( + f"{mat_name}: diffusivity changed from '{original_obj.get('diffusivity', '')}' to '{diffusivity}'" + ) + updated_data["materials"][mat_name]["viscosity"] = diffusivity + modified_materials.add(mat_name) + + modified_data = {"materials": {}} + for mat_name in modified_materials: + modified_data["materials"][mat_name] = copy.deepcopy(updated_data["materials"][mat_name]) + return differences, modified_data + + +if __name__ == "__main__": + import json + + sample_json = "test1.json" + with open(sample_json, "r") as file: + data = json.load(file) + extract_boundary_data(data)