diff --git a/docs/source/user_guide_model_creation.rst b/docs/source/user_guide_model_creation.rst index 56212d91..16dffde9 100644 --- a/docs/source/user_guide_model_creation.rst +++ b/docs/source/user_guide_model_creation.rst @@ -389,7 +389,9 @@ Alignment pertains to how the set of conductors is positioned within the winding Placement Strategies ^^^^^^^^^^^^^^^^^^^^ -The strategy for placing conductors is named based on the initial direction and subsequent movement. Examples include: +The strategy for placing conductors is named based on the initial direction and subsequent movement. It is only applied if the winding type is ``Single``. + +For ``RoundSolid`` and ``RoundLitz`` conductors, the placement strategies are as follows: - **VerticalUpward_HorizontalRightward**: Placement starts at the bottom, moving upward vertically, then shifts rightward horizontally for the next column. @@ -407,6 +409,20 @@ The strategy for placing conductors is named based on the initial direction and - **HorizontalLeftward_VerticalDownward**: Starts on the right side, moving leftward, then downward for each new row. +For ``RectangularSolid`` conductors, where the winding scheme is ``FoilVertical`` or ``FoilHorizontal``, the placement strategies are as follows: + +- **FoilVerticalDistribution**: These strategies are used when distributing rectangular foil conductors vertically. + + - **HorizontalRightward**: Begins placement from the left of the winding window, moving horizontally rightward for each conductor. + + - **HorizontalLeftward**: Begins placement from the right of the winding window, moving horizontally leftward for each conductor. + +- **FoilHorizontalDistribution**: These strategies are used when distributing rectangular foil conductors horizontally. + + - **VerticalUpward**: Begins placement from the bottom of the winding window, moving upward for each conductor. + + - **VerticalDownward**: Begins placement from the top of the winding window, moving downward for each conductor. + Zigzag Condition ^^^^^^^^^^^^^^^^ @@ -415,6 +431,7 @@ Zigzag placement introduces an alternating pattern in the layout: - After completing a row or column, the direction alternates (e.g., if moving upward initially, the next is downward). - The ``zigzag`` parameter is optional and defaults to ``False``. It can be omitted if a zigzag movement is not needed. +It is only can be used for ``RoundSolid`` and ``RoundLitz`` conductors when the winding type is ``Single``. Now before simulating the winding window needs to be added to the model as well: diff --git a/femmt/functions.py b/femmt/functions.py index ccb31af6..568cf8d7 100644 --- a/femmt/functions.py +++ b/femmt/functions.py @@ -254,7 +254,7 @@ def litz_database() -> Dict: litz_dict["1.5x105x0.1"] = {"strands_numbers": 105, "strand_radii": 0.1e-3 / 2, "conductor_radii": 1.5e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "", "litz": "RUPALIT V155", @@ -262,7 +262,7 @@ def litz_database() -> Dict: litz_dict["1.4x200x0.071"] = {"strands_numbers": 200, "strand_radii": 0.071e-3 / 2, "conductor_radii": 1.4e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "", "litz": "RUPALIT V155", @@ -270,7 +270,7 @@ def litz_database() -> Dict: litz_dict["2.0x405x0.071"] = {"strands_numbers": 405, "strand_radii": 0.071e-3 / 2, "conductor_radii": 2.0e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "", "material_number": "", "litz": "", @@ -278,7 +278,7 @@ def litz_database() -> Dict: litz_dict["2.0x800x0.05"] = {"strands_numbers": 800, "strand_radii": 0.05e-3 / 2, "conductor_radii": 2e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "Elektrisola", "material_number": "12104184", "litz": "", @@ -287,7 +287,7 @@ def litz_database() -> Dict: litz_dict["1.1x60x0.1"] = {"strands_numbers": 60, "strand_radii": 0.1e-3 / 2, "conductor_radii": 1.1e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "", "litz": "RUPALIT V155", @@ -296,7 +296,7 @@ def litz_database() -> Dict: litz_dict["1.35x200x0.071"] = {"strands_numbers": 200, "strand_radii": 0.071e-3 / 2, "conductor_radii": 1.35e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "", "litz": "RUPALIT V155", @@ -305,7 +305,7 @@ def litz_database() -> Dict: litz_dict["3.2x2100x0.05"] = {"strands_numbers": 2100, "strand_radii": 0.05e-3 / 2, "conductor_radii": 3.2e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "AB21220373", "litz": "RUPALIT V155", @@ -315,7 +315,7 @@ def litz_database() -> Dict: litz_dict["4.6x2160x0.071"] = {"strands_numbers": 2160, "strand_radii": 0.071e-3 / 2, "conductor_radii": 4.6e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "AB21225497", "litz": "RUPALIT V155", @@ -325,7 +325,7 @@ def litz_database() -> Dict: litz_dict["2.9x1200x0.06"] = {"strands_numbers": 1200, "strand_radii": 0.06e-3 / 2, "conductor_radii": 2.9e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "Elektrisola", "material_number": "", "litz": "", @@ -334,7 +334,7 @@ def litz_database() -> Dict: litz_dict["2.6x1000x0.06"] = {"strands_numbers": 1000, "strand_radii": 0.06e-3 / 2, "conductor_radii": 2.6e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "Elektrisola", "material_number": "", "litz": "", @@ -343,7 +343,7 @@ def litz_database() -> Dict: litz_dict["1.8x512x0.05"] = {"strands_numbers": 512, "strand_radii": 0.05e-3 / 2, "conductor_radii": 1.8e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "AB21217207", "litz": "RUPALIT Safety VB155", @@ -352,7 +352,7 @@ def litz_database() -> Dict: litz_dict["2.3x600x0.071"] = {"strands_numbers": 600, "strand_radii": 0.071e-3 / 2, "conductor_radii": 2.3e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "AB21220522", "litz": "RUPALIT Safety Profil V155", @@ -361,7 +361,7 @@ def litz_database() -> Dict: litz_dict["2.8x400x0.1"] = {"strands_numbers": 400, "strand_radii": 0.1e-3 / 2, "conductor_radii": 2.8e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "PACK", "material_number": "AB21222210", "litz": "RUPALIT Safety V155", @@ -370,7 +370,7 @@ def litz_database() -> Dict: litz_dict["1.71x140x0.1"] = {"strands_numbers": 140, "strand_radii": 0.1e-3 / 2, "conductor_radii": 1.71e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "", "material_number": "", "litz": "", @@ -379,7 +379,7 @@ def litz_database() -> Dict: litz_dict["1.7x500x0.06"] = {"strands_numbers": 500, "strand_radii": 0.06e-3 / 2, "conductor_radii": 1.7e-3 / 2, - "ff": "", + "ff": None, "manufacturer": "", "material_number": "", "litz": "", @@ -1018,8 +1018,9 @@ def visualize_simulation_results(simulation_result_file_path: str, store_figure_ cumulative_core_hysteresis = 0 cumulative_core_eddy = 0 cumulative_losses = [] - cumulative_inductances = [] windings_labels = [] + # Determine if this is a single simulation or a sweep + is_single_simulation = len(loaded_results_dict["single_sweeps"]) == 1 for index, sweep in enumerate(loaded_results_dict["single_sweeps"]): freq = sweep['f'] @@ -1043,11 +1044,9 @@ def visualize_simulation_results(simulation_result_file_path: str, store_figure_ if len(cumulative_losses) < i: cumulative_losses.append(loss) - cumulative_inductances.append(inductance) windings_labels.append(f"Winding {i}") else: cumulative_losses[i - 1] += loss - cumulative_inductances[i - 1] += inductance # Plot for current frequency ax.bar(i, loss, width=0.35, label=f'{windings_labels[i - 1]} Loss at {freq} Hz') @@ -1065,8 +1064,11 @@ def visualize_simulation_results(simulation_result_file_path: str, store_figure_ # Plot cumulative results for core and windings fig, ax = plt.subplots() - ax.bar(0, cumulative_core_hysteresis, width=0.35, label='Cumulative Core Hysteresis Loss') - ax.bar(0, cumulative_core_eddy, bottom=cumulative_core_hysteresis, width=0.35, label='Cumulative Core Eddy Current Loss') + if is_single_simulation: + ax.bar(0, cumulative_core_hysteresis, width=0.35, label='Cumulative Core Hysteresis Loss') + ax.bar(0, cumulative_core_eddy, bottom=cumulative_core_hysteresis, width=0.35, label='Cumulative Core Eddy Current Loss') + else: + ax.bar(0, cumulative_core_eddy, width=0.35, label='Cumulative Core Eddy Current Loss') for index, loss in enumerate(cumulative_losses): ax.bar(index + 1, loss, width=0.35, label=f'{windings_labels[index]} Cumulative Loss') diff --git a/gui/femmt_gui.py b/gui/femmt_gui.py index 6a4e7d9c..541bcfd8 100644 --- a/gui/femmt_gui.py +++ b/gui/femmt_gui.py @@ -5,7 +5,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT from mpl_toolkits.axes_grid1 import make_axes_locatable from matplotlib import cm -from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QMessageBox +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QMessageBox, QFileDialog, QInputDialog from PyQt5 import QtCore, uic, QtWidgets from PyQt5.QtGui import QPixmap, QDoubleValidator, QIntValidator import femmt as fmt @@ -14,6 +14,7 @@ from typing import List import PIL import webbrowser +import shutil # new import for threads from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QCoreApplication, QMutex @@ -257,6 +258,10 @@ def __init__(self, parent=None): self.action_documentation.triggered.connect(self.webbrowser_documentation) self.action_report_bug.triggered.connect(self.webbrowser_bugreport) + # ## Save electro-magnetic and thermal simulation results + self.md_pushButton_simulation_result.clicked.connect(self.save_results_to_directory) + self.md_pushButton_thermal_simulation_result.clicked.connect(self.save_results_to_directory) + "******* Manual Design *********" "Signals in Definition Tab" @@ -736,6 +741,70 @@ def webbrowser_documentation(self): """Open the web browser to the FEMMT documentation.""" webbrowser.open('https://upb-lea.github.io/FEM_Magnetics_Toolbox/') + def save_results_to_directory(self): + """Open a dialog for the user to select a directory and saves the results from the default GUI working directory to the selected directory.""" + options = QFileDialog.Options() + selected_directory = QFileDialog.getExistingDirectory(self, "Select Directory to Save Results", options=options) + + if selected_directory: + # Define the source directory (where the results are stored by default) + source_directory = os.path.join(self.default_gui_working_directory, "results") + + # Check if the source directory exists + if not os.path.exists(source_directory): + QMessageBox.warning(self, "No Results Found", "The results directory does not exist. There are no results to save.") + return + + # Check if the source directory contains any .json files + json_files = [f for f in os.listdir(source_directory) if f.endswith('.json')] + + if not json_files: + QMessageBox.warning(self, "No Results Found", "There are no .json result files in the current working directory.") + return + + # Copy files + try: + self.copy_results_to_directory(source_directory, selected_directory) + self.statusBar().showMessage(f"Results saved to: {selected_directory}", 5000) + except Exception as e: + self.statusBar().showMessage(f"Error saving results: {str(e)}", 5000) + + def copy_results_to_directory(self, source_directory, target_directory): + """ + Copy JSON files from the source directory to the target directory. + + :param source_directory: The path to the directory where the results are stored. + :type source_directory: str + :param target_directory: The path to the directory where the results should be copied to. + :type target_directory: str + """ + # Create the target directory if it doesn't exist + if not os.path.exists(target_directory): + os.makedirs(target_directory) + + # Copy only .json files from the source to the target + for item in os.listdir(source_directory): + if item.endswith('.json'): # Check if the file is a .json file + source_item = os.path.join(source_directory, item) + target_item = os.path.join(target_directory, item) + + # Check if the file already exists in the target directory + if os.path.exists(target_item): + # Prompt user to rename the file + new_name, ok = QInputDialog.getText(self, "File Exists", + f"The file '{item}' already exists. Please enter a new name:") + if ok and new_name: + # Ensure the new name ends with .json + if not new_name.endswith('.json'): + new_name += '.json' + target_item = os.path.join(target_directory, new_name) + else: + # If the user cancels the dialog or does not provide a new name, skip copying this file + continue + + # Copy the JSON file + shutil.copy2(source_item, target_item) + # **************************** Automated design tab ************************************************************ # def plot_volume_loss(self, data_matrix, matplotlib_widget: MatplotlibWidget): @@ -3478,11 +3547,9 @@ def md_action_run_simulation(self, *args, **kwargs) -> None: hysteresis_label = getattr(self, f'md_loss_core_hysteresis_label{index + 1}') eddy_current_label = getattr(self, f'md_loss_core_eddy_current_label{index + 1}') winding1_loss_label = getattr(self, f'md_loss_winding1_label{index + 1}') - inductance1_label = getattr(self, f'md_inductance1_label{index + 1}') if self.md_simulation_type_comboBox.currentText() == self.translation_dict['transformer']: winding2_loss_label = getattr(self, f'md_loss_winding2_label{index + 1}') - inductance2_label = getattr(self, f'md_inductance2_label{index + 1}') # Update frequency label freq_label.setText(f"Frequency: {sweep['f']} Hz") @@ -3497,30 +3564,13 @@ def md_action_run_simulation(self, *args, **kwargs) -> None: # just for shown one figure: self.md_loss_plot_label1.setPixmap(pixmap) self.md_loss_plot_label1.show() - # # Show the losses with round and approximation. - # hysteresis_label.setText(f"Core Hysteresis loss: {sweep.get('core_hyst_losses', 0):.5f} W") - # eddy_current_label.setText(f"Core Eddy Current loss: {sweep.get('core_eddy_losses', 0):.5f} W") - # winding1_loss_label.setText(f"Winding 1 loss: {sweep['winding1'].get('winding_losses', 0):.5f} W") - # - # primary_inductance_nh = sweep['winding1'].get('flux_over_current', [0])[0] * 1e9 - # inductance1_label.setText(f"Primary Inductance: {primary_inductance_nh:.0f} nH") - # # Transformer case. - # if self.md_simulation_type_comboBox.currentText() == self.translation_dict['transformer']: - # secondary_inductance_nh = sweep['winding2'].get('flux_over_current', [0])[0] * 1e9 - # winding2_loss_label.setText(f"Winding 2 loss: {sweep['winding2'].get('winding_losses', 0):.0f} W") - # inductance2_label.setText(f"Secondary Inductance: {secondary_inductance_nh:.5f} nH") # Show the losses with the new format_number_with_units function. hysteresis_label.setText(f"Core Hysteresis loss: {format_number_with_units(sweep.get('core_hyst_losses', 0))} W") eddy_current_label.setText(f"Core Eddy Current loss: {format_number_with_units(sweep.get('core_eddy_losses', 0))} W") winding1_loss_label.setText(f"Winding 1 loss: {format_number_with_units(sweep['winding1'].get('winding_losses', 0))} W") - - primary_inductance_nh = sweep['winding1'].get('flux_over_current', [0])[0] * 1e9 - inductance1_label.setText(f"Primary Inductance: {primary_inductance_nh:.0f} nH") # Transformer case. if self.md_simulation_type_comboBox.currentText() == self.translation_dict['transformer']: - secondary_inductance_nh = sweep['winding2'].get('flux_over_current', [0])[0] winding2_loss_label.setText(f"Winding 2 loss: {format_number_with_units(sweep['winding2'].get('winding_losses', 0))} W") - inductance2_label.setText(f"Secondary Inductance: {format_number_with_units(secondary_inductance_nh, decimals=4)} H") finally: # Unlock the mutex to allow other operations to proceed. diff --git a/gui/femmt_gui.ui b/gui/femmt_gui.ui index 23d7f08d..89fc4664 100644 --- a/gui/femmt_gui.ui +++ b/gui/femmt_gui.ui @@ -61,7 +61,7 @@ - 0 + 3 @@ -2022,6 +2022,13 @@ Simulation Text Output + + + + Save electrical simulation result (JSON) + + + @@ -2077,10 +2084,6 @@ - - - - @@ -2088,6 +2091,10 @@ + + + + @@ -2151,10 +2158,6 @@ - - - - @@ -2162,6 +2165,10 @@ + + + + @@ -2219,23 +2226,23 @@ - + - - - - - + + + + + @@ -2299,10 +2306,6 @@ - - - - @@ -2310,6 +2313,10 @@ + + + + @@ -2367,23 +2374,23 @@ - + - - - - - + + + + + @@ -2447,10 +2454,6 @@ - - - - @@ -2458,6 +2461,10 @@ + + + + @@ -2521,10 +2528,6 @@ - - - - @@ -2532,6 +2535,10 @@ + + + + @@ -2595,10 +2602,6 @@ - - - - @@ -2606,6 +2609,10 @@ + + + + @@ -2669,10 +2676,6 @@ - - - - @@ -2680,6 +2683,10 @@ + + + + @@ -2775,6 +2782,13 @@ + + + + Save thermal simulation result (JSON) + + + @@ -4470,8 +4484,8 @@ 0 0 - 694 - 301 + 1440 + 1172 @@ -4707,8 +4721,8 @@ 0 0 - 98 - 518 + 1398 + 755 @@ -4759,8 +4773,8 @@ 0 0 - 547 - 221 + 1440 + 1172 @@ -4879,8 +4893,8 @@ 0 0 - 98 - 518 + 1378 + 1020 @@ -9728,7 +9742,7 @@ 0 0 - 98 + 43 1024