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