From e1e39864713b7ab89e48ade9ede634b598cbe45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 20:02:49 +0100 Subject: [PATCH 1/7] try to move lint tests into pytests instead of CI for local tests --- .github/workflows/main.yml | 39 ++++++++++++++++++---------------- setup.py | 1 + tests/integration/test_lint.py | 30 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 tests/integration/test_lint.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b452f7ba..e030ec1f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,28 +28,31 @@ jobs: - name: check linting using pycodestyle run: | pip install pycodestyle + cd tests/integration + pytest test_lint.py + cd ../.. # code - pycodestyle femmt/constants.py - pycodestyle femmt/logparser.py - pycodestyle femmt/enumerations.py - pycodestyle femmt/dtos.py + #pycodestyle femmt/constants.py + #pycodestyle femmt/logparser.py + #pycodestyle femmt/enumerations.py + #pycodestyle femmt/dtos.py # example files - pycodestyle femmt/examples/advanced_inductor_sweep.py - pycodestyle femmt/examples/advanced_sto.py - pycodestyle femmt/examples/basic_inductor.py - pycodestyle femmt/examples/basic_inductor_foil_vertical.py - pycodestyle femmt/examples/basic_transformer.py - pycodestyle femmt/examples/basic_transformer_5_windings.py - pycodestyle femmt/examples/basic_transformer_center_tapped.py - pycodestyle femmt/examples/basic_transformer_integrated.py - pycodestyle femmt/examples/basic_transformer_interleaved.py - pycodestyle femmt/examples/basic_transformer_n_winding.py - pycodestyle femmt/examples/basic_transformer_stacked.py - pycodestyle femmt/examples/basic_transformer_stacked_center_tapped.py - pycodestyle femmt/examples/basic_transformer_three_winding.py - pycodestyle tests/integration/test_femmt.py + #pycodestyle femmt/examples/advanced_inductor_sweep.py + #pycodestyle femmt/examples/advanced_sto.py + #pycodestyle femmt/examples/basic_inductor.py + #pycodestyle femmt/examples/basic_inductor_foil_vertical.py + #pycodestyle femmt/examples/basic_transformer.py + #pycodestyle femmt/examples/basic_transformer_5_windings.py + #pycodestyle femmt/examples/basic_transformer_center_tapped.py + #pycodestyle femmt/examples/basic_transformer_integrated.py + #pycodestyle femmt/examples/basic_transformer_interleaved.py + #pycodestyle femmt/examples/basic_transformer_n_winding.py + #pycodestyle femmt/examples/basic_transformer_stacked.py + #pycodestyle femmt/examples/basic_transformer_stacked_center_tapped.py + #pycodestyle femmt/examples/basic_transformer_three_winding.py + #pycodestyle tests/integration/test_femmt.py - name: install femmt package run: | diff --git a/setup.py b/setup.py index e5d94a5b..b969c1c1 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ 'onelab>=1.0', 'scipy>=1.7.2', 'pytest', + 'pycodestyle', 'PyQt5>=5.15.6', 'mplcursors>=0.5.1', # TODO Is this necessary? 'deepdiff>=6.2.1', # comparing result dicts for pytests diff --git a/tests/integration/test_lint.py b/tests/integration/test_lint.py new file mode 100644 index 00000000..75c7788b --- /dev/null +++ b/tests/integration/test_lint.py @@ -0,0 +1,30 @@ +import pycodestyle + + +def test_files(): + + files = ['../../femmt/examples/advanced_inductor_sweep.py', + '../../femmt/constants.py', + '../../femmt/logparser.py', + '../../femmt/enumerations.py', + '../../femmt/dtos.py', + '../../femmt/examples/advanced_inductor_sweep.py', + '../../femmt/examples/advanced_sto.py', + '../../femmt/examples/basic_inductor.py', + '../../femmt/examples/basic_inductor_foil_vertical.py', + '../../femmt/examples/basic_transformer.py', + '../../femmt/examples/basic_transformer_5_windings.py', + '../../femmt/examples/basic_transformer_center_tapped.py', + '../../femmt/examples/basic_transformer_integrated.py', + '../../femmt/examples/basic_transformer_interleaved.py', + '../../femmt/examples/basic_transformer_n_winding.py', + '../../femmt/examples/basic_transformer_stacked.py', + '../../femmt/examples/basic_transformer_stacked_center_tapped.py', + '../../femmt/examples/basic_transformer_three_winding.py', + '../../tests/integration/test_femmt.py', + 'test_lint.py' + ] + style = pycodestyle.StyleGuide(config_file='../../tox.ini') + result = style.check_files(files) + print(result.total_errors) + assert result.total_errors == 0 From 4ef2cbded102f954ee4c14336fefc267a1e3c573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 20:04:45 +0100 Subject: [PATCH 2/7] mv pytest installation to start of toolchain --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e030ec1f..775df12b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,7 @@ jobs: - name: check linting using pycodestyle run: | pip install pycodestyle + pip install pytest cd tests/integration pytest test_lint.py cd ../.. @@ -65,7 +66,6 @@ jobs: pip install --upgrade pip pip install opencv-python pip install -e . - pip install pytest - name: install material database package run: | From 6cf06f4cf1b0208769d199e1a559a55c9a2d0eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 20:08:56 +0100 Subject: [PATCH 3/7] test file with lining errors --- tests/integration/test_lint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_lint.py b/tests/integration/test_lint.py index 75c7788b..ebf0b84c 100644 --- a/tests/integration/test_lint.py +++ b/tests/integration/test_lint.py @@ -22,7 +22,8 @@ def test_files(): '../../femmt/examples/basic_transformer_stacked_center_tapped.py', '../../femmt/examples/basic_transformer_three_winding.py', '../../tests/integration/test_femmt.py', - 'test_lint.py' + 'test_lint.py', + '../../femmt/optimization/functions_optimization.py' ] style = pycodestyle.StyleGuide(config_file='../../tox.ini') result = style.check_files(files) From 987c8f56d612b89e13544b5670777449944bd00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 21:01:42 +0100 Subject: [PATCH 4/7] add more files for linter --- femmt/optimization/ito_dtos.py | 1 - femmt/optimization/ito_functions.py | 148 ++++---- femmt/optimization/optuna_femmt_parser.py | 4 +- femmt/optimization/to.py | 429 +++------------------- femmt/thermal/thermal_classes.py | 34 +- femmt/thermal/thermal_functions.py | 4 +- tests/integration/test_lint.py | 23 +- tox.ini | 6 +- 8 files changed, 173 insertions(+), 476 deletions(-) diff --git a/femmt/optimization/ito_dtos.py b/femmt/optimization/ito_dtos.py index 3e8ba773..74cc7ff5 100644 --- a/femmt/optimization/ito_dtos.py +++ b/femmt/optimization/ito_dtos.py @@ -118,4 +118,3 @@ class ItoSingleResultFile: secondary_litz_wire_loss: float core_2daxi_total_volume: float total_loss: float - diff --git a/femmt/optimization/ito_functions.py b/femmt/optimization/ito_functions.py index 38f1a1da..b975a03e 100644 --- a/femmt/optimization/ito_functions.py +++ b/femmt/optimization/ito_functions.py @@ -4,6 +4,7 @@ from typing import List, Tuple import inspect +import femmt # 3rd party libraries # femmt libraries @@ -12,12 +13,16 @@ import femmt.functions as ff import femmt as fmt + def _copy_electro_magnetic_necessary_files(src_folder: str, dest_folder: str): - """Inner function. Needed in order to appropriately run parallel simulations since some GetDP files are changed in every simulation instance + """ + Inner function. Needed in order to appropriately run parallel simulations since some GetDP files + are changed in every simulation instance :param src_folder: Path to the base electro_magnetic folder :type src_folder: str - :param dest_folder: Path to the folder where the necessary files shall be stored. The "new" electro_magnetic folder for the corresponding simulation. + :param dest_folder: Path to the folder where the necessary files shall be stored. The "new" electro_magnetic folder + for the corresponding simulation. :type dest_folder: str """ files = ["fields.pro", "ind_axi_python_controlled.pro", "solver.pro", "values.pro"] @@ -73,11 +78,12 @@ def set_up_folder_structure(working_directory: str) -> WorkingDirectories: working_directories = WorkingDirectories( fem_working_directory=os.path.join(working_directory, '00_femmt_simulation'), - reluctance_model_results_directory = os.path.join(working_directory, "01_reluctance_model_results"), - fem_simulation_results_directory = os.path.join(working_directory, "02_fem_simulation_results"), - fem_simulation_filtered_results_directory = os.path.join(working_directory, "02_fem_simulation_results_filtered"), - fem_thermal_simulation_results_directory = os.path.join(working_directory, "03_fem_thermal_simulation_results"), - fem_thermal_filtered_simulation_results_directory = os.path.join(working_directory, "03_fem_thermal_simulation_results_filtered"), + reluctance_model_results_directory=os.path.join(working_directory, "01_reluctance_model_results"), + fem_simulation_results_directory=os.path.join(working_directory, "02_fem_simulation_results"), + fem_simulation_filtered_results_directory=os.path.join(working_directory, "02_fem_simulation_results_filtered"), + fem_thermal_simulation_results_directory=os.path.join(working_directory, "03_fem_thermal_simulation_results"), + fem_thermal_filtered_simulation_results_directory=os.path.join(working_directory, + "03_fem_thermal_simulation_results_filtered"), ) os.makedirs(working_directories.fem_working_directory, exist_ok=True) @@ -85,21 +91,23 @@ def set_up_folder_structure(working_directory: str) -> WorkingDirectories: # generate 50 folders for 50 possible parallel calculations number_of_processes = 50 - for process_number in list(range(1,number_of_processes+1)): + for process_number in list(range(1, number_of_processes+1)): single_process_directory = os.path.join(working_directories.fem_working_directory, f"process_{process_number}") os.makedirs(single_process_directory, exist_ok=True) - electro_magnetic_folder_general = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "electro_magnetic")) + electro_magnetic_folder_general = os.path.abspath(os.path.join(os.path.dirname( + os.path.abspath(__file__)), os.pardir, "electro_magnetic")) strands_coefficients_folder_general = os.path.join(electro_magnetic_folder_general, "Strands_Coefficients") if not os.path.isdir(working_directory): os.mkdir(working_directory) - for process_number in list(range(1,number_of_processes+1)): + for process_number in list(range(1, number_of_processes+1)): # Setup necessary files and directories single_process_directory = os.path.join(working_directories.fem_working_directory, f"process_{process_number}") electro_magnetic_directory_single_process = os.path.join(single_process_directory, "electro_magnetic") - strands_coefficients_directory_single_process = os.path.join(electro_magnetic_directory_single_process, 'Strands_Coefficients') + strands_coefficients_directory_single_process = os.path.join(electro_magnetic_directory_single_process, + 'Strands_Coefficients') if not os.path.isdir(single_process_directory): os.mkdir(single_process_directory) if not os.path.isdir(electro_magnetic_directory_single_process): @@ -107,10 +115,10 @@ def set_up_folder_structure(working_directory: str) -> WorkingDirectories: if not os.path.isdir(strands_coefficients_directory_single_process): os.mkdir(strands_coefficients_directory_single_process) - _copy_electro_magnetic_necessary_files(electro_magnetic_folder_general, electro_magnetic_directory_single_process) - shutil.copytree(strands_coefficients_folder_general, strands_coefficients_directory_single_process, dirs_exist_ok=True) - - + _copy_electro_magnetic_necessary_files(electro_magnetic_folder_general, + electro_magnetic_directory_single_process) + shutil.copytree(strands_coefficients_folder_general, strands_coefficients_directory_single_process, + dirs_exist_ok=True) os.makedirs(working_directories.fem_simulation_results_directory, exist_ok=True) os.makedirs(working_directories.fem_simulation_filtered_results_directory, exist_ok=True) @@ -120,6 +128,7 @@ def set_up_folder_structure(working_directory: str) -> WorkingDirectories: return working_directories + def integrated_transformer_fem_simulation_from_result_dto(config_dto: ItoSingleInputConfig, dto: ItoSingleResultFile, fem_working_directory: str, @@ -132,13 +141,14 @@ def integrated_transformer_fem_simulation_from_result_dto(config_dto: ItoSingleI # 1. chose simulation type geo = fmt.MagneticComponent(component_type=fmt.ComponentType.IntegratedTransformer, working_directory=fem_working_directory, - silent=True) + verbosity=femmt.Verbosity.Silent) window_h = dto.window_h_bot + dto.window_h_top + dto.core_inner_diameter / 4 core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=dto.core_inner_diameter, window_w=dto.window_w, - window_h=window_h) + window_h=window_h, + core_h=window_h + dto.core_inner_diameter / 2) core = fmt.Core(core_type=fmt.CoreType.Single, core_dimensions=core_dimensions, @@ -209,55 +219,62 @@ def integrated_transformer_fem_simulation_from_result_dto(config_dto: ItoSingleI return geo + def integrated_transformer_fem_simulations_from_result_dtos(config_dto: ItoSingleInputConfig, simulation_dto_list: List[ItoSingleResultFile], visualize: bool = False, ): - ito_target_and_fixed_parameters_dto = fmt.optimization.IntegratedTransformerOptimization.calculate_fix_parameters(config_dto) + ito_target_and_fixed_parameters_dto = fmt.optimization.IntegratedTransformerOptimization.calculate_fix_parameters( + config_dto) - time_extracted, current_extracted_1_vec = fr.time_vec_current_vec_from_time_current_vec \ - (config_dto.time_current_1_vec) - time_extracted, current_extracted_2_vec = fr.time_vec_current_vec_from_time_current_vec \ - (config_dto.time_current_2_vec) + time_extracted, current_extracted_1_vec = fr.time_vec_current_vec_from_time_current_vec( + config_dto.time_current_1_vec) + time_extracted, current_extracted_2_vec = fr.time_vec_current_vec_from_time_current_vec( + config_dto.time_current_2_vec) fundamental_frequency = int(1 / time_extracted[-1]) - phase_deg_1, phase_deg_2 = fr.phases_deg_from_time_current(time_extracted, current_extracted_1_vec, current_extracted_2_vec) + phase_deg_1, phase_deg_2 = fr.phases_deg_from_time_current(time_extracted, current_extracted_1_vec, + current_extracted_2_vec) i_peak_1, i_peak_2 = fr.max_value_from_value_vec(current_extracted_1_vec, current_extracted_2_vec) - - for count, dto in enumerate(simulation_dto_list): print(f"FEM simulation {count} of {len(simulation_dto_list)}") try: - geo = integrated_transformer_fem_simulation_from_result_dto(config_dto = config_dto, - dto=dto, - fem_working_directory = ito_target_and_fixed_parameters_dto.working_directories.fem_working_directory, - fundamental_frequency = fundamental_frequency, - i_peak_1=i_peak_1, - i_peak_2=i_peak_2, - phase_deg_1 = phase_deg_1, - phase_deg_2 = phase_deg_2, - visualize= visualize) - - source_json_file = os.path.join(ito_target_and_fixed_parameters_dto.working_directories.fem_working_directory, "results", "log_electro_magnetic.json") - destination_json_file = os.path.join(ito_target_and_fixed_parameters_dto.working_directories.fem_simulation_results_directory, - f'case_{dto.case}.json') + integrated_transformer_fem_simulation_from_result_dto( + config_dto=config_dto, + dto=dto, + fem_working_directory=ito_target_and_fixed_parameters_dto.working_directories.fem_working_directory, + fundamental_frequency=fundamental_frequency, + i_peak_1=i_peak_1, + i_peak_2=i_peak_2, + phase_deg_1=phase_deg_1, + phase_deg_2=phase_deg_2, + visualize=visualize) + + source_json_file = os.path.join( + ito_target_and_fixed_parameters_dto.working_directories.fem_working_directory, + "results", "log_electro_magnetic.json") + destination_json_file = os.path.join( + ito_target_and_fixed_parameters_dto.working_directories.fem_simulation_results_directory, + f'case_{dto.case}.json') shutil.copy(source_json_file, destination_json_file) except Exception as e: print(f"Exception: {e}") -def integrated_transformer_fem_thermal_simulations_from_result_dtos(config_dto: ItoSingleInputConfig, - simulation_dto_list: List[ItoSingleResultFile], - visualize: bool = False, - ): - ito_target_and_fixed_parameters_dto = fmt.optimization.ito_optuna.ItoOptuna.calculate_fix_parameters(config_dto) - time_extracted, current_extracted_1_vec = fr.time_vec_current_vec_from_time_current_vec \ - (config_dto.time_current_1_vec) - time_extracted, current_extracted_2_vec = fr.time_vec_current_vec_from_time_current_vec \ - (config_dto.time_current_2_vec) +def integrated_transformer_fem_thermal_simulations_from_result_dtos( + config_dto: ItoSingleInputConfig, simulation_dto_list: List[ItoSingleResultFile], + visualize: bool = False): + + ito_target_and_fixed_parameters_dto = fmt.optimization.IntegratedTransformerOptimization.calculate_fix_parameters( + config_dto) + + time_extracted, current_extracted_1_vec = fr.time_vec_current_vec_from_time_current_vec( + config_dto.time_current_1_vec) + time_extracted, current_extracted_2_vec = fr.time_vec_current_vec_from_time_current_vec( + config_dto.time_current_2_vec) fundamental_frequency = int(1 / time_extracted[-1]) phase_deg_1, phase_deg_2 = fr.phases_deg_from_time_current(time_extracted, current_extracted_1_vec, @@ -266,16 +283,16 @@ def integrated_transformer_fem_thermal_simulations_from_result_dtos(config_dto: for dto in simulation_dto_list: try: - geo = integrated_transformer_fem_simulation_from_result_dto(config_dto=config_dto, - dto=dto, - fem_working_directory=ito_target_and_fixed_parameters_dto.fem_working_directory, - fundamental_frequency=fundamental_frequency, - i_peak_1=i_peak_1, - i_peak_2=i_peak_2, - phase_deg_1=phase_deg_1, - phase_deg_2=phase_deg_2, - visualize=visualize) - + geo = integrated_transformer_fem_simulation_from_result_dto( + config_dto=config_dto, + dto=dto, + fem_working_directory=ito_target_and_fixed_parameters_dto.working_directories.fem_working_directory, + fundamental_frequency=fundamental_frequency, + i_peak_1=i_peak_1, + i_peak_2=i_peak_2, + phase_deg_1=phase_deg_1, + phase_deg_2=phase_deg_2, + visualize=visualize) thermal_conductivity_dict = { "air": 0.122, # potting epoxy resign @@ -325,14 +342,17 @@ def integrated_transformer_fem_thermal_simulations_from_result_dtos(config_dto: geo.thermal_simulation(thermal_conductivity_dict, boundary_temperatures, boundary_flags, case_gap_top, case_gap_right, - case_gap_bot, show_thermal_simulation_results=visualize, pre_visualize_geometry=False, color_scheme=color_scheme, + case_gap_bot, show_thermal_simulation_results=visualize, + pre_visualize_geometry=False, color_scheme=color_scheme, colors_geometry=colors_geometry) - source_json_file = os.path.join(ito_target_and_fixed_parameters_dto.fem_working_directory, "results", - "results_thermal.json") - destination_json_file = os.path.join(ito_target_and_fixed_parameters_dto.fem_thermal_simulation_results_directory, - f'case_{dto.case}.json') + source_json_file = os.path.join( + ito_target_and_fixed_parameters_dto.working_directories.fem_working_directory, "results", + "results_thermal.json") + destination_json_file = os.path.join( + ito_target_and_fixed_parameters_dto.working_directories.fem_thermal_simulation_results_directory, + f'case_{dto.case}.json') shutil.copy(source_json_file, destination_json_file) - except: - pass \ No newline at end of file + except Exception: + pass diff --git a/femmt/optimization/optuna_femmt_parser.py b/femmt/optimization/optuna_femmt_parser.py index 3e5b5068..37065ed2 100644 --- a/femmt/optimization/optuna_femmt_parser.py +++ b/femmt/optimization/optuna_femmt_parser.py @@ -11,7 +11,7 @@ class OptunaFemmtParser: @staticmethod def parse(frozen_trial: optuna.trial.FrozenTrial) -> ItoSingleResultFile: - return ItoSingleResultFile( + return ItoSingleResultFile( case=frozen_trial.number, # geometry parameters air_gap_top=frozen_trial.user_attrs["air_gap_top"], @@ -22,7 +22,7 @@ def parse(frozen_trial: optuna.trial.FrozenTrial) -> ItoSingleResultFile: n_s_top=frozen_trial.params["n_s_top"], n_s_bot=frozen_trial.params["n_s_bot"], window_h_top=frozen_trial.params["window_h_top"], - window_h_bot = frozen_trial.params["window_h_bot"], + window_h_bot=frozen_trial.params["window_h_bot"], window_w=frozen_trial.params["window_w"], core_material=frozen_trial.params["material"], core_inner_diameter=frozen_trial.params["core_inner_diameter"], diff --git a/femmt/optimization/to.py b/femmt/optimization/to.py index b9dc7ad8..7b5758df 100644 --- a/femmt/optimization/to.py +++ b/femmt/optimization/to.py @@ -2,10 +2,8 @@ import os import shutil import json -import datetime import gc -import gmsh # 3rd party libraries import optuna import pandas as pd @@ -116,11 +114,12 @@ def objective(trial, config: ToSingleInputConfig, :type number_objectives: int :param show_geometries: True to display the geometries :type show_geometries: bool - :param process_number: process number. Important for parallel computing, each simulation works in a separate folder + :param process_number: process number. Important for parallel computing, for each simulation a separate folder :type process_number: int :return: returns volume, loss and absolute error of inductances in % if number_objectives == 3: - total_volume, total_loss, 100 * (abs(difference_l_h / config.l_h_target) + abs(difference_l_s12 / config.l_s12_target)) + total_volume, total_loss, 100 * (abs(difference_l_h / config.l_h_target) + + abs(difference_l_s12 / config.l_s12_target)) elif number_objectives == 4: return total_volume, total_loss, 100 * abs(difference_l_h), 100 * abs(difference_l_s12) """ @@ -139,10 +138,6 @@ def objective(trial, config: ToSingleInputConfig, primary_litz_wire = trial.suggest_categorical("primary_litz_wire", config.primary_litz_wire_list) primary_litz_parameters = ff.litz_database()[primary_litz_wire] - primary_litz_diameter = 2 * primary_litz_parameters["conductor_radii"] - - # Will always be calculated from the given parameters - available_width = window_w - iso_left_core - config.insulations.iso_right_core # suggest categorical core_material = trial.suggest_categorical("material", config.material_list) @@ -160,7 +155,7 @@ def objective(trial, config: ToSingleInputConfig, verbosity = femmt.Verbosity.Silent # calculate core volume for transformer type. Check if this volume is smaller than the given limit - core_height = window_h + core_inner_diameter /2 + core_height = window_h + core_inner_diameter / 2 r_outer = fr.calculate_r_outer(core_inner_diameter, window_w) core_volume = np.pi * r_outer ** 2 * core_height if core_volume > config.max_core_volume: @@ -186,8 +181,8 @@ def objective(trial, config: ToSingleInputConfig, strands_coefficients_folder_single_process) core_dimensions = femmt.dtos.SingleCoreDimensions(core_inner_diameter=core_inner_diameter, - window_w=window_w, - window_h=window_h, core_h=core_height) + window_w=window_w, + window_h=window_h, core_h=core_height) core = femmt.Core(core_type=femmt.CoreType.Single, core_dimensions=core_dimensions, material=core_material, temperature=config.temperature, frequency=target_and_fixed_parameters.fundamental_frequency, @@ -277,12 +272,6 @@ def objective(trial, config: ToSingleInputConfig, center_tapped_study_excitation["linear_losses"]["current_amplitudes"], center_tapped_study_excitation["linear_losses"]["current_phases_deg"], inductance_dict=inductance_dict, core_hyst_loss=[p_hyst]) - # geo.stacked_core_center_tapped_study(center_tapped_study_excitation, - # number_primary_coil_turns=primary_coil_turns) - - # geo.stacked_core_center_tapped_study(time_current_vectors=[[target_and_fixed_parameters.time_extracted_vec, target_and_fixed_parameters.current_extracted_1_vec], - # [target_and_fixed_parameters.time_extracted_vec, target_and_fixed_parameters.current_extracted_2_vec]], - # plot_waveforms=False) # copy result files to result-file folder source_json_file = os.path.join( @@ -309,9 +298,6 @@ def objective(trial, config: ToSingleInputConfig, trial.set_user_attr("l_h", geo.L_h) trial.set_user_attr("l_s12", geo.L_s12) - # TODO: Normalize on goal values here or the whole generation on min and max? for each feature inbetween 0 and 1 - # norm_total_loss, norm_difference_l_h, norm_difference_l_s12 = total_loss/10, abs(difference_l_h/config.l_h_target), abs(difference_l_s12/config.l_s12_target) - # return norm_total_loss, norm_difference_l_h, norm_difference_l_s12 if number_objectives == 3: return total_volume, total_loss, 100 * (abs(difference_l_h / config.l_h_target) + abs( difference_l_s12 / config.l_s12_target)) @@ -325,88 +311,6 @@ def objective(trial, config: ToSingleInputConfig, elif number_objectives == 4: return float('nan'), float('nan'), float('nan'), float('nan') - @staticmethod - def start_proceed_study(study_name: str, config: ToSingleInputConfig, number_trials: int, - number_objectives: int = None, - storage: str = 'sqlite', - sampler=optuna.samplers.NSGAIISampler(), - show_geometries: bool = False, - ) -> None: - """ - Proceed a study which is stored as sqlite database. - - :param study_name: Name of the study - :type study_name: str - :param config: Simulation configuration - :type config: ItoSingleInputConfig - :param number_trials: Number of trials adding to the existing study - :type number_trials: int - :param number_objectives: number of objectives, e.g. 3 or 4 - :type number_objectives: int - :param storage: storage database, e.g. 'sqlite' or 'mysql' - :type storage: str - :param sampler: optuna.samplers.NSGAIISampler() or optuna.samplers.NSGAIIISampler(). Note about the brackets () !! - :type sampler: optuna.sampler-object - :param show_geometries: True to show the geometry of each suggestion (with valid geometry data) - :type show_geometries: bool - """ - - def objective_directions(configure_number_objectives: int): - """ - Checks if the number of objectives is correct and returns the minimizing targets - :param configure_number_objectives: number of objectives - :type configure_number_objectives: int - :returns: objective targets and optimization function - """ - if configure_number_objectives == 3: - # Wrap the objective inside a lambda and call objective inside it - return ["minimize", "minimize", "minimize"] - if configure_number_objectives == 4: - return ["minimize", "minimize", "minimize", 'minimize'] - else: - raise ValueError("Invalid objective number.") - - if os.path.exists(f"{config.working_directory}/study_{study_name}.sqlite3"): - print("Existing study found. Proceeding.") - - target_and_fixed_parameters = femmt.optimization.TransformerOptimization.calculate_fix_parameters(config) - - # introduce study in storage, e.g. sqlite or mysql - if storage == 'sqlite': - # Note: for sqlite operation, there needs to be three slashes '///' even before the path '/home/...' - # Means, in total there are four slashes including the path itself '////home/.../database.sqlite3' - storage = f"sqlite:///{config.working_directory}/study_{study_name}.sqlite3" - elif storage == 'mysql': - storage = "mysql://monty@localhost/mydb", - - # set logging verbosity: https://optuna.readthedocs.io/en/stable/reference/generated/optuna.logging.set_verbosity.html#optuna.logging.set_verbosity - # .INFO: all messages (default) - # .WARNING: fails and warnings - # .ERROR: only errors - # optuna.logging.set_verbosity(optuna.logging.ERROR) - - directions = objective_directions(number_objectives) - - func = lambda \ - trial: femmt.optimization.TransformerOptimization.objective( - trial, config, - target_and_fixed_parameters, number_objectives, show_geometries) - - study_in_storage = optuna.create_study(study_name=study_name, - storage=storage, - directions=directions, - load_if_exists=True, sampler=sampler) - - study_in_memory = optuna.create_study(directions=directions, study_name=study_name, sampler=sampler) - print(f"Sampler is {study_in_memory.sampler.__class__.__name__}") - study_in_memory.add_trials(study_in_storage.trials) - study_in_memory.optimize(func, n_trials=number_trials, show_progress_bar=True, - callbacks=[femmt.TransformerOptimization.run_garbage_collector]) - - study_in_storage.add_trials(study_in_memory.trials[-number_trials:]) - print(f"Finished {number_trials} trials.") - print(f"current time: {datetime.datetime.now()}") - @staticmethod def proceed_multi_core_study(study_name: str, config: ToSingleInputConfig, number_trials: int, number_objectives: int = None, @@ -428,25 +332,26 @@ def proceed_multi_core_study(study_name: str, config: ToSingleInputConfig, numbe :type number_objectives: int :param storage: storage database, e.g. 'sqlite' or mysql-storage, e.g. "mysql://monty@localhost/mydb" :type storage: str - :param sampler: optuna.samplers.NSGAIISampler() or optuna.samplers.NSGAIIISampler(). Note about the brackets () !! + :param sampler: optuna.samplers.NSGAIISampler() or optuna.samplers.NSGAIIISampler(). Note about the brackets ()! :type sampler: optuna.sampler-object :param show_geometries: True to show the geometry of each suggestion (with valid geometry data) :type show_geometries: bool - :type process_number: number of the process, mandatory to split this up for several processes, because they use the same simulation result folder! + :type process_number: number of the process, mandatory to split this up for several processes, because they use + the same simulation result folder! :param process_number: int """ - def objective_directions(number_objectives: int): + def objective_directions(number_target_objectives: int): """ Checks if the number of objectives is correct and returns the minimizing targets - :param number_objectives: number of objectives - :type number_objectives: int + :param number_target_objectives: number of objectives + :type number_target_objectives: int :returns: objective targets and optimization function """ - if number_objectives == 3: + if number_target_objectives == 3: # Wrap the objective inside a lambda and call objective inside it return ["minimize", "minimize", "minimize"] - if number_objectives == 4: + if number_target_objectives == 4: return ["minimize", "minimize", "minimize", 'minimize'] else: raise ValueError("Invalid objective number.") @@ -461,7 +366,8 @@ def objective_directions(number_objectives: int): target_and_fixed_parameters = femmt.optimization.TransformerOptimization.calculate_fix_parameters(config) - # set logging verbosity: https://optuna.readthedocs.io/en/stable/reference/generated/optuna.logging.set_verbosity.html#optuna.logging.set_verbosity + # set logging verbosity: + # https://optuna.readthedocs.io/en/stable/reference/generated/optuna.logging.set_verbosity.html#optuna.logging.set_verbosity # .INFO: all messages (default) # .WARNING: fails and warnings # .ERROR: only errors @@ -469,8 +375,7 @@ def objective_directions(number_objectives: int): directions = objective_directions(number_objectives) - func = lambda \ - trial: femmt.optimization.TransformerOptimization.objective( + func = lambda trial: femmt.optimization.TransformerOptimization.objective( trial, config, target_and_fixed_parameters, number_objectives, show_geometries, process_number) @@ -489,7 +394,7 @@ def run_garbage_collector(study: optuna.Study, _): # as seen so far, a typical study needs 50.000 to 500.000 trials to have good results when performing # the optimization. In the range above 200.000 trials, the RAM has a high occupancy rate. # in case of running 10 or more cores, the garbage collector will run each 10.000 trial - # There could be an improvement to run the garbage collector on every process in future. + # There could be an improvement to run the garbage collector on every process in the future. # Now, it is random on which of the processes the garbage collector runs. # Learn about the garbage collector https://docs.python.org/3/library/gc.html#gc.collect # Every process runs its own garbage collector. So there is no difference between multiple processes @@ -498,266 +403,6 @@ def run_garbage_collector(study: optuna.Study, _): print("Run garbage collector") gc.collect() - @staticmethod - def show_study_results(study_name: str, config: ToSingleInputConfig, - percent_error_difference_l_h: float = 20, - percent_error_difference_l_s12: float = 20) -> None: - """ - Show the results of a study. - - A local .html file is generated under config.working_directory to store the interactive plotly plots on disk. - - :param study_name: Name of the study - :type study_name: str - :param config: Integrated transformer configuration file - :type config: ItoSingleInputConfig - :param percent_error_difference_l_h: relative error allowed in l_h - :type percent_error_difference_l_s12: float - :param percent_error_difference_l_s12: relative error allowed in L_s12 - :type percent_error_difference_l_s12: float - """ - study = optuna.create_study(study_name=study_name, - storage=f"sqlite:///{config.working_directory}/study_{study_name}.sqlite3", - load_if_exists=True) - - # Order: total_volume, total_loss, difference_l_h, difference_l_s - l_h_absolute_error = percent_error_difference_l_h / 100 * config.l_h_target - print(f"{config.l_h_target = }") - print(f"{l_h_absolute_error = }") - - l_s_absolute_error = percent_error_difference_l_s12 / 100 * config.l_s12_target - print(f"{config.l_s12_target = }") - print(f"{l_s_absolute_error = }") - - fig = optuna.visualization.plot_pareto_front(study, targets=lambda t: ( - t.values[0] if -l_h_absolute_error < t.values[2] < l_h_absolute_error else None, - t.values[1] if -l_s_absolute_error < t.values[3] < l_s_absolute_error else None), - target_names=["volume in m³", "loss in W"]) - fig.update_layout( - title=f"{study_name}: Filtering l_h_absolute_error = {l_h_absolute_error * 100} %, l_s_absolute_error = {l_s_absolute_error * 100} %.") - fig.write_html( - f"{config.working_directory}/{study_name}_error_lh_{l_h_absolute_error}_error_ls_{l_s_absolute_error}_{datetime.datetime.now().isoformat(timespec='minutes')}.html") - fig.show() - - @staticmethod - def show_study_results3(study_name: str, config: ToSingleInputConfig, - error_difference_inductance_sum_percent, storage: str = 'sqlite') -> None: - """ - Show the results of a study. - - A local .html file is generated under config.working_directory to store the interactive plotly plots on disk. - - :param study_name: Name of the study - :type study_name: str - :param config: Integrated transformer configuration file - :type config: ItoSingleInputConfig - :param error_difference_inductance_sum_percent: |err(L_s12) + err(L_h)| in % - :type error_difference_inductance_sum_percent: float - :param storage: storage, e.g. 'sqlite' or path to postgresql-database - :type storage: str - - """ - if storage == 'sqlite': - storage = f"sqlite:///{config.working_directory}/study_{study_name}.sqlite3" - - time_start = datetime.datetime.now() - print(f"Start loading study {study_name} from database") - study = optuna.load_study(study_name=study_name, - storage=storage) - - print(f"Loaded study {study_name} contains {len(study.trials)} trials.") - time_stop = datetime.datetime.now() - print(f"Finished loading study {study_name} from database in time: {time_stop - time_start}") - - print(f"{error_difference_inductance_sum_percent = }") - - time_start = datetime.datetime.now() - print(f"start generating Pareto front....") - fig = optuna.visualization.plot_pareto_front(study, targets=lambda t: ( - t.values[0] if error_difference_inductance_sum_percent > t.values[2] else None, - t.values[1] if error_difference_inductance_sum_percent > t.values[2] else None), - target_names=["volume in m³", "loss in W"]) - fig.update_layout( - title=f"{study_name}: Filtering {error_difference_inductance_sum_percent} % of |err(Ls_12)| + |err(L_h)|") - time_stop = datetime.datetime.now() - print(f"Finished generating Pareto front in time: {time_stop - time_start}") - - fig.write_html( - f"{config.working_directory}/{study_name}_error_diff_{error_difference_inductance_sum_percent}%_{datetime.datetime.now().isoformat(timespec='minutes')}.html") - fig.show() - - @staticmethod - def re_simulate_single_result(study_name: str, config: ToSingleInputConfig, number_trial: int, - fft_filter_value_factor: float = 0.01, mesh_accuracy: float = 0.5, - storage: str = "sqlite"): - """ - Performs a single simulation study (inductance, core loss, winding loss) and shows the geometry of - number_trial design inside the study 'study_name'. Loads from an optuna study and is very slow. - - Note: This function does not use the fft_filter_value_factor and mesh_accuracy from the config-file. - The values are given separate. In case of re-simulation, you may want to have more accurate results. - - :param study_name: name of the study - :type study_name: str - :param config: stacked transformer configuration file - :type config: ToSingleInputConfig - :param number_trial: number of trial to simulate - :type number_trial: int - :param fft_filter_value_factor: Factor to filter frequencies from the fft. E.g. 0.01 [default] removes all amplitudes below 1 % of the maximum amplitude from the result-frequency list - :type fft_filter_value_factor: float - :param mesh_accuracy: a mesh_accuracy of 0.5 is recommended. Do not change this parameter, except performing thousands of simulations, e.g. a Pareto optimization. In this case, the value can be set e.g. to 0.8 - :type mesh_accuracy: float - :param storage: storage of the study - :type storage: str - """ - target_and_fixed_parameters = femmt.optimization.TransformerOptimization.calculate_fix_parameters(config) - - if storage == "sqlite": - storage = f"sqlite:///{config.working_directory}/study_{study_name}.sqlite3" - - time_start = datetime.datetime.now() - print(f"Start loading study {study_name} from database") - loaded_study = optuna.create_study(study_name=study_name, - storage=storage, - load_if_exists=True) - - print(f"Loaded study {study_name} contains {len(loaded_study.trials)} trials.") - time_stop = datetime.datetime.now() - print(f"Finished loading study {study_name} from database in time: {time_stop - time_start}") - - loaded_trial = loaded_study.trials[number_trial] - loaded_trial_params = loaded_trial.params - - # suggest core geometry - core_inner_diameter = loaded_trial_params["core_inner_diameter"] - window_w = loaded_trial_params["window_w"] - air_gap_transformer = loaded_trial_params["air_gap_transformer"] - # inner_coil_insulation = trial_params["inner_coil_insulation"] - iso_left_core = loaded_trial_params["iso_left_core"] - - primary_litz_wire = loaded_trial_params["primary_litz_wire"] - - primary_litz_parameters = ff.litz_database()[primary_litz_wire] - primary_litz_diameter = 2 * primary_litz_parameters["conductor_radii"] - - # Will always be calculated from the given parameters - available_width = window_w - iso_left_core - config.insulations.iso_right_core - - # Re-calculation of top window coil - # Theoretically also 0 coil turns possible (number_rows_coil_winding must then be recalculated to avoid neg. values) - primary_coil_turns = loaded_trial_params["primary_coil_turns"] - # Note: int() is used to round down. - number_rows_coil_winding = int((primary_coil_turns * ( - primary_litz_diameter + config.insulations.iso_primary_to_primary) - config.insulations.iso_primary_inner_bobbin) / available_width) + 1 - window_h_top = config.insulations.iso_top_core + config.insulations.iso_bot_core + number_rows_coil_winding * primary_litz_diameter + ( - number_rows_coil_winding - 1) * config.insulations.iso_primary_to_primary - - primary_additional_bobbin = config.insulations.iso_primary_inner_bobbin - iso_left_core - - # Maximum coil air gap depends on the maximum window height top - air_gap_coil = loaded_trial_params["air_gap_coil"] - - # suggest categorical - core_material = Material(loaded_trial_params["material"]) - foil_thickness = loaded_trial_params["foil_thickness"] - - if config.max_transformer_total_height is not None: - # Maximum transformer height - window_h_bot_max = config.max_transformer_total_height - 3 * core_inner_diameter / 4 - window_h_top - window_h_bot_min = config.window_h_bot_min_max_list[0] - if window_h_bot_min > window_h_bot_max: - print(f"{number_rows_coil_winding = }") - print(f"{window_h_top = }") - raise ValueError(f"{window_h_bot_min = } > {window_h_bot_max = }") - - window_h_bot = loaded_trial_params["window_h_bot"] - - else: - window_h_bot = loaded_trial_params["window_h_bot"] - - geo = femmt.MagneticComponent(component_type=femmt.ComponentType.IntegratedTransformer, - working_directory=target_and_fixed_parameters.working_directories.fem_working_directory, - verbosity=femmt.Verbosity.Silent, - simulation_name=f"Single_Case_{loaded_trial._trial_id - 1}") - # Note: The _trial_id starts counting from 1, while the normal cases count from zero. So a correction needs to be made - - geo.update_mesh_accuracies(mesh_accuracy, mesh_accuracy, mesh_accuracy, mesh_accuracy) - - core_dimensions = femmt.dtos.StackedCoreDimensions(core_inner_diameter=core_inner_diameter, window_w=window_w, - window_h_top=window_h_top, window_h_bot=window_h_bot) - - core = femmt.Core(core_type=femmt.CoreType.Stacked, core_dimensions=core_dimensions, - material=core_material, temperature=config.temperature, - frequency=target_and_fixed_parameters.fundamental_frequency, - permeability_datasource=config.permeability_datasource, - permeability_datatype=config.permeability_datatype, - permeability_measurement_setup=config.permeability_measurement_setup, - permittivity_datasource=config.permittivity_datasource, - permittivity_datatype=config.permittivity_datatype, - permittivity_measurement_setup=config.permittivity_measurement_setup) - - geo.set_core(core) - - air_gaps = femmt.AirGaps(femmt.AirGapMethod.Stacked, core) - air_gaps.add_air_gap(femmt.AirGapLegPosition.CenterLeg, air_gap_coil, - stacked_position=femmt.StackedPosition.Top) - air_gaps.add_air_gap(femmt.AirGapLegPosition.CenterLeg, air_gap_transformer, - stacked_position=femmt.StackedPosition.Bot) - geo.set_air_gaps(air_gaps) - - # set_center_tapped_windings() automatically places the condu - insulation, coil_window, transformer_window = femmt.functions_topologies.set_center_tapped_windings( - core=core, - - # primary litz - primary_additional_bobbin=primary_additional_bobbin, - primary_turns=config.n_target, - primary_radius=primary_litz_parameters["conductor_radii"], - primary_number_strands=primary_litz_parameters["strands_numbers"], - primary_strand_radius=primary_litz_parameters["strand_radii"], - - # secondary foil - secondary_parallel_turns=2, - secondary_thickness_foil=foil_thickness, - - # insulation - iso_top_core=config.insulations.iso_top_core, iso_bot_core=config.insulations.iso_bot_core, - iso_left_core=iso_left_core, iso_right_core=config.insulations.iso_right_core, - iso_primary_to_primary=config.insulations.iso_primary_to_primary, - iso_secondary_to_secondary=config.insulations.iso_secondary_to_secondary, - iso_primary_to_secondary=config.insulations.iso_primary_to_secondary, - bobbin_coil_top=config.insulations.iso_top_core, - bobbin_coil_bot=config.insulations.iso_bot_core, - bobbin_coil_left=config.insulations.iso_primary_inner_bobbin, - bobbin_coil_right=config.insulations.iso_right_core, - center_foil_additional_bobbin=0e-3, - interleaving_scheme=InterleavingSchemesFoilLitz(loaded_trial_params['interleaving_scheme']), - - # misc - interleaving_type=CenterTappedInterleavingType(loaded_trial_params['interleaving_type']), - primary_coil_turns=primary_coil_turns, - winding_temperature=config.temperature) - - geo.set_insulation(insulation) - geo.set_winding_windows([coil_window, transformer_window]) - - geo.create_model(freq=target_and_fixed_parameters.fundamental_frequency, pre_visualize_geometry=True) - - # geo.single_simulation(freq=target_and_fixed_parameters.fundamental_frequency, - # current=[target_and_fixed_parameters.i_peak_1, target_and_fixed_parameters.i_peak_2 / 2, target_and_fixed_parameters.i_peak_2 / 2], - # phi_deg=[target_and_fixed_parameters.i_phase_deg_1, target_and_fixed_parameters.i_phase_deg_2, target_and_fixed_parameters.i_phase_deg_2], - # show_fem_simulation_results=False) - - center_tapped_study_excitation = geo.center_tapped_pre_study( - time_current_vectors=[[target_and_fixed_parameters.time_extracted_vec, - target_and_fixed_parameters.current_extracted_1_vec], - [target_and_fixed_parameters.time_extracted_vec, - target_and_fixed_parameters.current_extracted_2_vec]], - fft_filter_value_factor=fft_filter_value_factor) - - geo.stacked_core_center_tapped_study(center_tapped_study_excitation, - number_primary_coil_turns=primary_coil_turns) - @staticmethod def re_simulate_from_df(df: pd.DataFrame, config: ToSingleInputConfig, number_trial: int, fft_filter_value_factor: float = 0.01, mesh_accuracy: float = 0.5, @@ -775,9 +420,11 @@ def re_simulate_from_df(df: pd.DataFrame, config: ToSingleInputConfig, number_tr :type config: ToSingleInputConfig :param number_trial: number of trial to simulate :type number_trial: int - :param fft_filter_value_factor: Factor to filter frequencies from the fft. E.g. 0.01 [default] removes all amplitudes below 1 % of the maximum amplitude from the result-frequency list + :param fft_filter_value_factor: Factor to filter frequencies from the fft. + E.g. 0.01 [default] removes all amplitudes below 1 % of the maximum amplitude from the result-frequency list :type fft_filter_value_factor: float - :param mesh_accuracy: a mesh_accuracy of 0.5 is recommended. Do not change this parameter, except performing thousands of simulations, e.g. a Pareto optimization. In this case, the value can be set e.g. to 0.8 + :param mesh_accuracy: a mesh_accuracy of 0.5 is recommended. Do not change this parameter, except performing + thousands of simulations, e.g. a Pareto optimization. In this case, the value can be set e.g. to 0.8 :type mesh_accuracy: float :param show_simulation_results: visualize the simulation results of the FEM simulation :type show_simulation_results: bool @@ -805,7 +452,9 @@ def re_simulate_from_df(df: pd.DataFrame, config: ToSingleInputConfig, number_tr window_h = loaded_trial_params["params_window_h"] geo = femmt.MagneticComponent(component_type=femmt.ComponentType.Transformer, - working_directory=os.path.join(target_and_fixed_parameters.working_directories.fem_working_directory, 'process_1'), + working_directory=os.path.join( + target_and_fixed_parameters.working_directories.fem_working_directory, + 'process_1'), verbosity=femmt.Verbosity.Silent, simulation_name=f"Single_Case_{loaded_trial_params['number']}") @@ -817,7 +466,7 @@ def re_simulate_from_df(df: pd.DataFrame, config: ToSingleInputConfig, number_tr core_h = window_h + core_inner_diameter / 2 core_dimensions = femmt.dtos.SingleCoreDimensions(core_inner_diameter=core_inner_diameter, window_w=window_w, - window_h=window_h, core_h=core_h) + window_h=window_h, core_h=core_h) core = femmt.Core(core_type=femmt.CoreType.Single, core_dimensions=core_dimensions, material=core_material, temperature=config.temperature, @@ -886,8 +535,9 @@ def re_simulate_from_df(df: pd.DataFrame, config: ToSingleInputConfig, number_tr geo.mesh.generate_electro_magnetic_mesh() geo.generate_load_litz_approximation_parameters() - geo.excitation(frequency=center_tapped_study_excitation["hysteresis"]["frequency"], amplitude_list=center_tapped_study_excitation["hysteresis"]["transformer"][ - "current_amplitudes"], phase_deg_list=center_tapped_study_excitation["hysteresis"]["transformer"]["current_phases_deg"], + geo.excitation(frequency=center_tapped_study_excitation["hysteresis"]["frequency"], + amplitude_list=center_tapped_study_excitation["hysteresis"]["transformer"]["current_amplitudes"], + phase_deg_list=center_tapped_study_excitation["hysteresis"]["transformer"]["current_phases_deg"], plot_interpolation=False) geo.check_model_mqs_condition() @@ -999,9 +649,11 @@ def create_full_report(df: pd.DataFrame, trials_numbers: list[int], config: ToSi :type thermal_config: ThermalConfig :param current_waveforms_operating_points: Trial numbers in a list to re-simulate :type current_waveforms_operating_points: List[int] - :param fft_filter_value_factor: Factor to filter frequencies from the fft. E.g. 0.01 [default] removes all amplitudes below 1 % of the maximum amplitude from the result-frequency list + :param fft_filter_value_factor: Factor to filter frequencies from the fft. + E.g. 0.01 [default] removes all amplitudes below 1 % of the maximum amplitude from the result-frequency list :type fft_filter_value_factor: float - :param mesh_accuracy: a mesh_accuracy of 0.5 is recommended. Do not change this parameter, except performing thousands of simulations, e.g. a Pareto optimization. In this case, the value can be set e.g. to 0.8 + :param mesh_accuracy: a mesh_accuracy of 0.5 is recommended. Do not change this parameter, except performing + thousands of simulations, e.g. a Pareto optimization. In this case, the value can be set e.g. to 0.8 :type mesh_accuracy: float """ @@ -1018,14 +670,15 @@ def create_full_report(df: pd.DataFrame, trials_numbers: list[int], config: ToSi count_current_waveform].time_current_2_vec # perform the electromagnetic simulation - geo_sim = femmt.TransformerOptimization.re_simulate_from_df(df, config, - number_trial=trial_number, - show_simulation_results=False, - fft_filter_value_factor=fft_filter_value_factor, - mesh_accuracy=mesh_accuracy) + geo_sim = femmt.TransformerOptimization.re_simulate_from_df( + df, config, + number_trial=trial_number, + show_simulation_results=False, + fft_filter_value_factor=fft_filter_value_factor, + mesh_accuracy=mesh_accuracy) # perform the thermal simulation femmt.TransformerOptimization.thermal_simulation_from_geo(geo_sim, thermal_config, - show_visual_outputs=False) + show_visual_outputs=False) electromagnetoquasistatic_result_dict = geo_sim.read_log() thermal_result_dict = geo_sim.read_thermal_log() @@ -1036,7 +689,7 @@ def create_full_report(df: pd.DataFrame, trials_numbers: list[int], config: ToSi f"max_temp_core_{current_waveform.name}": thermal_result_dict["core_parts"]["total"]["max"], f"max_temp_winding_{current_waveform.name}": thermal_result_dict["windings"]["total"]["max"], "volume": electromagnetoquasistatic_result_dict["misc"]["core_2daxi_total_volume"] - } + } simulation_waveforms_dict.update(simulation_waveform_dict) diff --git a/femmt/thermal/thermal_classes.py b/femmt/thermal/thermal_classes.py index 558c0ac7..8f1e9126 100644 --- a/femmt/thermal/thermal_classes.py +++ b/femmt/thermal/thermal_classes.py @@ -1,6 +1,7 @@ # Python standard libraries from typing import List, Tuple + class ConstraintPro: # For boundary constraints, the tuple contains (key, region, value) boundary_constraints: List[Tuple[str, str, str]] @@ -23,6 +24,7 @@ def create_file(self, file_path): fd.write(ConstraintPro.create_boundary_if_string(bc[0], bc[1], bc[2])) fd.write("\t\t}\n\t}\n}") + class GroupPro: regions: dict @@ -32,7 +34,7 @@ def __init__(self): def add_regions(self, more_regions): self.regions.update(more_regions) - def create_file(self, file_path, air_gaps_enabled = False, insulation_enabled = False): + def create_file(self, file_path, air_gaps_enabled=False, insulation_enabled=False): with open(file_path, "w") as fd: fd.write("Group {\n") for key, value in self.regions.items(): @@ -49,6 +51,7 @@ def create_file(self, file_path, air_gaps_enabled = False, insulation_enabled = fd.write("\tTotal = Region[{Warm, Cold}];\n") fd.write("}") + class ParametersPro: """ For creating a parameters.pro @@ -76,6 +79,7 @@ def create_file(self, file_path): fd.write(line + "\n") + class FunctionPro: """ For creating a function.pro @@ -89,11 +93,11 @@ def __init__(self): @staticmethod def dict_as_function_str(name, dct): - str = "" + dict_as_str = "" for key, value in dct.items(): - str += f"\t{name}[{key}] = {value};\n" + dict_as_str += f"\t{name}[{key}] = {value};\n" - return str + return dict_as_str def add_dicts(self, k, q_vol): """ @@ -111,6 +115,7 @@ def create_file(self, file_path): fd.write(FunctionPro.dict_as_function_str("qVol", self.q_vol)) fd.write("}") + class PostOperationPro: """ For creating a post_operation.pro @@ -120,22 +125,24 @@ class PostOperationPro: def __init__(self): self.statements = [] - def add_on_point_statement(self, field, x, y, format, file_name, point_name, append = False): + def add_on_point_statement(self, field, x, y, formatting, file_name, point_name, append=False): format_str = "" - if format is not None: - format_str = f"Format {format}, " + if formatting is not None: + format_str = f"Format {formatting}, " append_str = "> " if append else "" - self.statements.append(f"Print [ {field}, OnPoint {{{x}, {y}, 0}}, {format_str}Name \"{point_name}\", File {append_str}\"{file_name}\" ];") + self.statements.append( + f"Print [ {field}, OnPoint {{{x}, {y}, 0}}, {format_str}Name \"{point_name}\", File {append_str}\"{file_name}\" ];") - def add_on_elements_of_statement(self, field, region, file_name, format = None, depth = None, name = None, append = False): + def add_on_elements_of_statement(self, field, region, file_name, formatting=None, depth=None, + name=None, append=False): format_str = "" depth_str = "" name_str = "" - if format is not None: - format_str = f"Format {format}, " + if formatting is not None: + format_str = f"Format {formatting}, " if depth is not None: depth_str = f"Depth {depth}, " @@ -145,11 +152,12 @@ def add_on_elements_of_statement(self, field, region, file_name, format = None, append_str = "> " if append else "" - self.statements.append(f"Print [ {field}, OnElementsOf {region}, {format_str}{depth_str}{name_str}File {append_str}\"{file_name}\"];") + self.statements.append( + f"Print [ {field}, OnElementsOf {region}, {format_str}{depth_str}{name_str}File {append_str}\"{file_name}\"];") def create_file(self, file_path): with open(file_path, 'w') as fd: fd.write("PostOperation map UsingPost The {\n") for statement in self.statements: fd.write(f"\t{statement}\n") - fd.write("}") \ No newline at end of file + fd.write("}") diff --git a/femmt/thermal/thermal_functions.py b/femmt/thermal/thermal_functions.py index 502dd36b..9309db4c 100644 --- a/femmt/thermal/thermal_functions.py +++ b/femmt/thermal/thermal_functions.py @@ -6,6 +6,7 @@ # 3rd party libraries import numpy as np + def calculate_heat_flux_round_wire(power, wire_radius, wire_distance): """ :param power: losses in Watts @@ -17,6 +18,7 @@ def calculate_heat_flux_round_wire(power, wire_radius, wire_distance): return power/volume + def read_results_log(results_log_file_path: str) -> Dict: """ Read loss results from logfile to get the losses to perform a thermal simulation. @@ -30,4 +32,4 @@ def read_results_log(results_log_file_path: str) -> Dict: content = json.loads(fd.read()) losses = content["total_losses"] - return losses \ No newline at end of file + return losses diff --git a/tests/integration/test_lint.py b/tests/integration/test_lint.py index ebf0b84c..95579cbc 100644 --- a/tests/integration/test_lint.py +++ b/tests/integration/test_lint.py @@ -3,11 +3,12 @@ def test_files(): - files = ['../../femmt/examples/advanced_inductor_sweep.py', - '../../femmt/constants.py', + files = ['../../femmt/constants.py', '../../femmt/logparser.py', '../../femmt/enumerations.py', '../../femmt/dtos.py', + + # examples '../../femmt/examples/advanced_inductor_sweep.py', '../../femmt/examples/advanced_sto.py', '../../femmt/examples/basic_inductor.py', @@ -21,10 +22,22 @@ def test_files(): '../../femmt/examples/basic_transformer_stacked.py', '../../femmt/examples/basic_transformer_stacked_center_tapped.py', '../../femmt/examples/basic_transformer_three_winding.py', - '../../tests/integration/test_femmt.py', + + # tests + 'test_femmt.py', 'test_lint.py', - '../../femmt/optimization/functions_optimization.py' - ] + + # optimization + '../../femmt/optimization/sto_dtos.py', + '../../femmt/optimization/optuna_femmt_parser.py', + '../../femmt/optimization/to.py', + '../../femmt/optimization/to_dtos.py', + '../../femmt/optimization/ito_dtos.py', + '../../femmt/optimization/ito_functions.py', + + # thermal files + '../../femmt/thermal/thermal_functions.py'] + style = pycodestyle.StyleGuide(config_file='../../tox.ini') result = style.check_files(files) print(result.total_errors) diff --git a/tox.ini b/tox.ini index 7dbfe7de..d92edd41 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [pycodestyle] count = False -ignore = E226,E302,E71, W293, W291 +ignore = E226,E302,E71, W293, W291, E731 max-line-length = 120 -statistics = True \ No newline at end of file +statistics = True + +# E731: do not assign a lambda expression, use a def \ No newline at end of file From 57cb6f031b760ddbeeb9eeac2799febebbc6dc7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 21:31:09 +0100 Subject: [PATCH 5/7] add more files for linter --- femmt/functions_reluctance.py | 144 +++++++++++++++++++++------------ femmt/hpc.py | 52 ++++++++---- tests/integration/test_lint.py | 2 + tox.ini | 5 +- 4 files changed, 134 insertions(+), 69 deletions(-) diff --git a/femmt/functions_reluctance.py b/femmt/functions_reluctance.py index f273e180..0e822b3c 100644 --- a/femmt/functions_reluctance.py +++ b/femmt/functions_reluctance.py @@ -11,7 +11,6 @@ from matplotlib import pyplot as plt - def calculate_ls_lh_n_from_inductance_matrix(inductance_matrix): """ Calculates the transformer primary concentrated circuit parameters from matrix @@ -49,8 +48,9 @@ def calculate_inductance_matrix_from_ls_lh_n(l_s_target_value, l_h_target_value, return inductance_matrix -def power_losses_hysteresis_cylinder_radial_direction_mu_r_imag(flux, cylinder_height, cylinder_inner_radius, cylinder_outer_radius, - fundamental_frequency, mu_r_abs, flux_density_data_vec, mu_r_imag_data_vec): +def power_losses_hysteresis_cylinder_radial_direction_mu_r_imag( + flux, cylinder_height, cylinder_inner_radius, cylinder_outer_radius, + fundamental_frequency, mu_r_abs, flux_density_data_vec, mu_r_imag_data_vec): """ This function calculates the hysteresis losses inside a cylinder, where the flux flows in radial direction. @@ -95,19 +95,25 @@ def power_loss_density_cylinder_envelope(cylinder_radius, flux_in_cylinder, heig Note: function parameter names differ from outer parameters to avoid 'shadows name from outer scope'. """ - mu_r_imag = np.interp(flux_density_cylinder_envelope(cylinder_radius, flux_in_cylinder, height_of_cylinder), flux_density_data_vec, mu_r_imag_data_vec) + mu_r_imag = np.interp(flux_density_cylinder_envelope(cylinder_radius, flux_in_cylinder, height_of_cylinder), + flux_density_data_vec, mu_r_imag_data_vec) + + return 2 * np.pi * cylinder_radius * height_of_cylinder * np.pi * fundamental_frequency * mu_0 * mu_r_imag * \ + (flux_density_cylinder_envelope(cylinder_radius, + flux_in_cylinder, height_of_cylinder) / mu_r_abs / mu_0) ** 2 - return 2 * np.pi * cylinder_radius * height_of_cylinder * np.pi * fundamental_frequency * mu_0 * mu_r_imag * (flux_density_cylinder_envelope(cylinder_radius, flux_in_cylinder, height_of_cylinder) / mu_r_abs / mu_0) ** 2 + return scipy.integrate.quad(power_loss_density_cylinder_envelope, cylinder_inner_radius, cylinder_outer_radius, + args=(flux, cylinder_height), epsabs=1e-4)[0] - return scipy.integrate.quad(power_loss_density_cylinder_envelope, cylinder_inner_radius, cylinder_outer_radius, args=(flux, cylinder_height), - epsabs=1e-4)[0] -def hyst_losses_core_half_mu_r_imag(core_inner_diameter, window_h_half, window_w, mu_r_abs, flux_max, fundamental_frequency, flux_density_data_vec, mu_r_imag_data_vec): +def hyst_losses_core_half_mu_r_imag(core_inner_diameter, window_h_half, window_w, mu_r_abs, + flux_max, fundamental_frequency, flux_density_data_vec, mu_r_imag_data_vec): """ Calculates the losses of a core cylinder half. means: losses in inner cylinder + losses in outer cylinder + losses in ONE cylinder_radial e.g. for top or bot - Note: To calculate the hysteresis losses of an inductor, you need to run this function twice with each the half window_h + Note: To calculate the hysteresis losses of an inductor, you need to run this function twice + with each the half window_h :param core_inner_diameter: core inner diameter :param window_h_half: window height of the core-half to consider @@ -122,18 +128,18 @@ def hyst_losses_core_half_mu_r_imag(core_inner_diameter, window_h_half, window_w flux_density_max = flux_max / core_cross_section volume_cylinder_inner = core_cross_section * window_h_half - losses_cylinder_inner = power_loss_hysteresis_simple_volume_mu_r_imag(fundamental_frequency, flux_density_max, mu_r_abs, volume_cylinder_inner, - flux_density_data_vec, mu_r_imag_data_vec) + losses_cylinder_inner = power_loss_hysteresis_simple_volume_mu_r_imag(fundamental_frequency, flux_density_max, + mu_r_abs, volume_cylinder_inner, + flux_density_data_vec, mu_r_imag_data_vec) cylinder_inner_radius = core_inner_diameter / 2 cylinder_outer_radius = core_inner_diameter / 2 + window_w - losses_cylinder_radial = power_losses_hysteresis_cylinder_radial_direction_mu_r_imag(flux_max, core_inner_diameter / 4, - cylinder_inner_radius, cylinder_outer_radius, - fundamental_frequency, mu_r_abs, flux_density_data_vec, mu_r_imag_data_vec) + losses_cylinder_radial = power_losses_hysteresis_cylinder_radial_direction_mu_r_imag( + flux_max, core_inner_diameter / 4, cylinder_inner_radius, cylinder_outer_radius, + fundamental_frequency, mu_r_abs, flux_density_data_vec, mu_r_imag_data_vec) return 2 * losses_cylinder_inner + losses_cylinder_radial - def calculate_core_2daxi_total_volume(core_inner_diameter, window_h, window_w): """ Calculates the full volume of a rotationally symmetric core. @@ -150,6 +156,7 @@ def calculate_core_2daxi_total_volume(core_inner_diameter, window_h, window_w): return core_2daxi_total_volume + def calculate_r_outer(core_inner_diameter, window_w): """ calculate outer core radius. @@ -163,7 +170,8 @@ def calculate_r_outer(core_inner_diameter, window_w): return outer_core_radius -def power_losses_hysteresis_cylinder_radial_direction(flux, cylinder_height, cylinder_inner_radius, cylinder_outer_radius, +def power_losses_hysteresis_cylinder_radial_direction(flux, cylinder_height, cylinder_inner_radius, + cylinder_outer_radius, fundamental_frequency, mu_r_imag, mu_r_abs): """ This function calculates the hysteresis losses inside a cylinder, where the flux flows in radial direction. @@ -207,25 +215,30 @@ def power_loss_density_cylinder_envelope(cylinder_radius, flux_in_cylinder, heig Note: this is the simplified version to calculate the losses using a fixed mu_r_imag. In case of using the u_r_imag in dependence of the different flux density, what varies inside the cylinder, use this as a hint to modify the formula. - AnalyticalCoreData.f_N95_mu_imag(fundamental_frequency, flux_density_cylinder_envelope(cylinder_radius, flux, cylinder_height)) + AnalyticalCoreData.f_N95_mu_imag(fundamental_frequency, flux_density_cylinder_envelope(cylinder_radius, + flux, cylinder_height)) The [0] in the return for only handling the result itself to the output, not the error. Note: function parameter names differ from outer parameters to avoid 'shadows name from outer scope'. """ - return 2 * np.pi * cylinder_radius * height_of_cylinder * np.pi * fundamental_frequency * mu_0 * mu_r_imag * (flux_density_cylinder_envelope(cylinder_radius, flux_in_cylinder, height_of_cylinder) / mu_r_abs / mu_0) ** 2 + return 2 * np.pi * cylinder_radius * height_of_cylinder * np.pi * fundamental_frequency * mu_0 * mu_r_imag * \ + (flux_density_cylinder_envelope(cylinder_radius, + flux_in_cylinder, height_of_cylinder) / mu_r_abs / mu_0) ** 2 - return scipy.integrate.quad(power_loss_density_cylinder_envelope, cylinder_inner_radius, cylinder_outer_radius, args=(flux, cylinder_height), - epsabs=1e-4)[0] + return scipy.integrate.quad(power_loss_density_cylinder_envelope, cylinder_inner_radius, cylinder_outer_radius, + args=(flux, cylinder_height), epsabs=1e-4)[0] -def hyst_losses_core_half(core_inner_diameter, window_h_half, window_w, mu_r_imag, mu_r_abs, flux_max, fundamental_frequency): +def hyst_losses_core_half(core_inner_diameter, window_h_half, window_w, mu_r_imag, + mu_r_abs, flux_max, fundamental_frequency): """ Calculates the losses of a core cylinder half. means: losses in inner cylinder + losses in outer cylinder + losses in ONE cylinder_radial e.g. for top or bot - Note: To calculate the hysteresis losses of an inductor, you need to run this function twice with each the half window_h + Note: To calculate the hysteresis losses of an inductor, you need to run this + function twice with each the half window_h :param core_inner_diameter: core inner diameter :param window_h_half: window height of the core-half to consider @@ -238,15 +251,17 @@ def hyst_losses_core_half(core_inner_diameter, window_h_half, window_w, mu_r_ima core_cross_section = (core_inner_diameter / 2) ** 2 * np.pi flux_density_max = flux_max / core_cross_section volume_cylinder_inner = core_cross_section * window_h_half - losses_cylinder_inner = power_loss_hysteresis_simple_volume(fundamental_frequency, mu_r_imag, flux_density_max, mu_r_abs, volume_cylinder_inner) + losses_cylinder_inner = power_loss_hysteresis_simple_volume(fundamental_frequency, mu_r_imag, + flux_density_max, mu_r_abs, volume_cylinder_inner) cylinder_inner_radius = core_inner_diameter / 2 cylinder_outer_radius = core_inner_diameter / 2 + window_w - losses_cylinder_radial = power_losses_hysteresis_cylinder_radial_direction(flux_max, core_inner_diameter / 4, - cylinder_inner_radius, cylinder_outer_radius, - fundamental_frequency, mu_r_imag, mu_r_abs) + losses_cylinder_radial = power_losses_hysteresis_cylinder_radial_direction( + flux_max, core_inner_diameter / 4, cylinder_inner_radius, cylinder_outer_radius, + fundamental_frequency, mu_r_imag, mu_r_abs) return 2 * losses_cylinder_inner + losses_cylinder_radial + def calculate_reluctance_matrix(winding_matrix, inductance_matrix): """ Calculates the Reluctance Matrix. @@ -274,6 +289,7 @@ def calculate_reluctance_matrix(winding_matrix, inductance_matrix): return np.matmul(np.matmul(winding_matrix, L_invert), np.transpose(winding_matrix)) + def calculate_inductance_matrix(reluctance_matrix, winding_matrix): """ Calculates the inductance matrix out of reluctance matrix and winding matrix. @@ -300,6 +316,7 @@ def calculate_inductance_matrix(reluctance_matrix, winding_matrix): return np.matmul(np.matmul(np.transpose(winding_matrix), reluctance_matrix_invert), winding_matrix) + def calculate_flux_matrix(reluctance_matrix, winding_matrix, current_matrix): """ calculates the flux for e.g. an integrated transformer @@ -317,7 +334,6 @@ def calculate_flux_matrix(reluctance_matrix, winding_matrix, current_matrix): flux_matrix = [ [flux_1], [flux_2] ] """ - if np.ndim(reluctance_matrix) == 0: reluctance_matrix_invert = 1 / reluctance_matrix else: @@ -352,7 +368,8 @@ def flux_vec_from_current_vec(current_vec_1, current_vec_2, winding_matrix, indu current_value_timestep = [current_vec_1[count], current_vec_2[count]] # simplified formula: flux = L * I / N - [flux_top_timestep, flux_bot_timestep] = np.matmul(np.matmul(np.linalg.inv(np.transpose(winding_matrix)), inductance_matrix), + [flux_top_timestep, flux_bot_timestep] = np.matmul(np.matmul(np.linalg.inv(np.transpose(winding_matrix)), + inductance_matrix), np.transpose(current_value_timestep)) flux_stray_timestep = flux_bot_timestep - flux_top_timestep @@ -384,6 +401,7 @@ def visualize_current_and_flux(time, flux_top_vec, flux_bot_vec, flux_stray_vec, axis[1].grid() plt.show() + def max_value_from_value_vec(*args): """ Returns the peak values from the vectors @@ -432,8 +450,8 @@ def power_loss_hysteresis_simple_volume(fundamental_frequency, mu_r_imag, flux_d return core_volume * np.pi * fundamental_frequency * mu_r_imag * mu_0 * (flux_density_max / mu_0 / mu_r_abs) ** 2 - -def power_loss_hysteresis_simple_volume_mu_r_imag(fundamental_frequency, flux_density_max, mu_r_abs, core_volume, flux_density_data_vec, mu_r_imag_data_vec): +def power_loss_hysteresis_simple_volume_mu_r_imag(fundamental_frequency, flux_density_max, + mu_r_abs, core_volume, flux_density_data_vec, mu_r_imag_data_vec): """ Calculates the hysteresis losses depending on the input parameters. The output are the losses for a certain volume of core. @@ -462,14 +480,17 @@ def r_basic_round_inf(air_gap_radius, air_gap_basic_height, core_height): [according to "A Novel Approach for 3D Air Gap Reluctance Calculations" - J. Mühlethaler, J.W. Kolar, A. Ecklebe] :param air_gap_radius: air gap radius - :param air_gap_basic_height: air gap height for the BASIC-AIR-GAP (e.g. if you use a round-round structure, this is half of the total air gap). + :param air_gap_basic_height: air gap height for the BASIC-AIR-GAP (e.g. if you use a round-round structure, + this is half of the total air gap). :param core_height: core height :return: basic reluctance for round - infinite structure """ - conductance_basic = mu_0 * (air_gap_radius * 2 / 2 / air_gap_basic_height + 2 / np.pi * (1 + np.log(np.pi * core_height / 4 / air_gap_basic_height))) + conductance_basic = mu_0 * (air_gap_radius * 2 / 2 / air_gap_basic_height + 2 / np.pi * \ + (1 + np.log(np.pi * core_height / 4 / air_gap_basic_height))) return 1 / conductance_basic + def sigma_round(r_equivalent, air_gap_radius, air_gap_total_height): """ Do not use this function directly! @@ -485,6 +506,7 @@ def sigma_round(r_equivalent, air_gap_radius, air_gap_total_height): """ return r_equivalent * mu_0 * air_gap_radius / air_gap_total_height + def r_air_gap_round_round(air_gap_total_height, core_inner_diameter, core_height_upper, core_height_lower): """ Returns the reluctance of a round-round air gap structure and includes finging effects. @@ -525,8 +547,12 @@ def r_air_gap_round_round(air_gap_total_height, core_inner_diameter, core_height return r_air_gap -def r_air_gap_round_round_sct(air_gap_total_height, core_inner_diameter, core_height_upper, core_height_lower, target_reluctance): - return r_air_gap_round_round(air_gap_total_height, core_inner_diameter, core_height_upper, core_height_lower) - target_reluctance + +def r_air_gap_round_round_sct(air_gap_total_height, core_inner_diameter, + core_height_upper, core_height_lower, target_reluctance): + return r_air_gap_round_round(air_gap_total_height, core_inner_diameter, core_height_upper, core_height_lower) - \ + target_reluctance + def r_air_gap_round_inf(air_gap_total_height, core_inner_diameter, core_height): """ @@ -553,11 +579,11 @@ def r_air_gap_round_inf(air_gap_total_height, core_inner_diameter, core_height): return r_air_gap + def r_air_gap_round_inf_sct(air_gap_total_height, core_inner_diameter, core_height, target_reluctance): return r_air_gap_round_inf(air_gap_total_height, core_inner_diameter, core_height) - target_reluctance - def r_basic_tablet_cyl(tablet_height, air_gap_basic_height, tablet_radius): """ Do not use this function directly! @@ -571,17 +597,20 @@ def r_basic_tablet_cyl(tablet_height, air_gap_basic_height, tablet_radius): Note: this is the same function as r_basic_round_inf, but with clear variable names for tablet-cylinder structure :param tablet_height: tablet height = air gap width for tablet-cylinder structure - :param air_gap_basic_height: air gap height for the BASIC-AIR-GAP (e.g. if you use a round-round structure, this is half of the total air gap). + :param air_gap_basic_height: air gap height for the BASIC-AIR-GAP (e.g. if you use a round-round structure, this + is half of the total air gap). :param tablet_radius: tablet radius :return: basic reluctance for tablet - cylinder structure """ if air_gap_basic_height == 0: raise ZeroDivisionError(f"Division by zero: {air_gap_basic_height = }") - conductance_basic = mu_0 * (tablet_height / 2 / air_gap_basic_height + 2 / np.pi * (1 + np.log(np.pi * tablet_radius / 4 / air_gap_basic_height))) + conductance_basic = mu_0 * (tablet_height / 2 / air_gap_basic_height + 2 / \ + np.pi * (1 + np.log(np.pi * tablet_radius / 4 / air_gap_basic_height))) return 1 / conductance_basic + def sigma_tablet_cyl(r_equivalent, tablet_height, air_gap_total_height): """ Do not use this function directly! @@ -630,9 +659,12 @@ def r_air_gap_tablet_cyl(tablet_height, air_gap_total_height, core_inner_diamete return r_air_gap -def r_air_gap_tablet_cylinder_sct(air_gap_total_height, core_inner_diameter, tablet_height, window_w, target_reluctance): + +def r_air_gap_tablet_cylinder_sct(air_gap_total_height, core_inner_diameter, + tablet_height, window_w, target_reluctance): return r_air_gap_tablet_cyl(tablet_height, air_gap_total_height, core_inner_diameter, window_w) - target_reluctance + def r_air_gap_tablet_cyl_no_2d_axi(tablet_height, air_gap_total_length, core_inner_diameter, window_w): """ Returns the reluctance of a cylinder-tablet air gap structure and includes fringing effects @@ -655,7 +687,8 @@ def r_air_gap_tablet_cyl_no_2d_axi(tablet_height, air_gap_total_length, core_inn raise Exception("air_gap_total_height is greater than window_w") air_gap_basic_height = air_gap_total_length - r_basic = r_basic_tablet_cyl(tablet_height, air_gap_basic_height, (core_inner_diameter + 2 * window_w - 2 * air_gap_total_length) / 2) + r_basic = r_basic_tablet_cyl(tablet_height, air_gap_basic_height, (core_inner_diameter + 2 * window_w - 2 * \ + air_gap_total_length) / 2) r_equivalent = r_basic / 2 sigma = sigma_tablet_cyl(r_equivalent, tablet_height, air_gap_total_length) @@ -663,25 +696,32 @@ def r_air_gap_tablet_cyl_no_2d_axi(tablet_height, air_gap_total_length, core_inn raise Exception("Failure in calculating reluctance. Sigma was calculated to >1. Check input parameters!") # Note: - # the circumference of the air gap differs for open cores (e.g. PQ40/40) to closed ones (ideal rotationally symmetric) - # The circumference is no more (diameter * pi), but (2 * pi - 4 * alpha) * (core_inner_diameter/2 + window_w), with alpha = arccos(core_dimension_x / (core_inner_diameter + 2 * window_w)) + # the circumference of the air gap differs for open cores (e.g. PQ40/40) to closed ones + # (ideal rotationally symmetric) + # The circumference is no more (diameter * pi), but (2 * pi - 4 * alpha) * (core_inner_diameter/2 + window_w), + # with alpha = arccos(core_dimension_x / (core_inner_diameter + 2 * window_w)) # See: Dissertation Lukas Keuck # For equal pq core sizes (e.g. PQ 40/40), it has been found out that - # core_dimension_x / core_dimension_y = 1.45, the error over all available shapes is maximum 7% (compared to datasheet value) + # core_dimension_x / core_dimension_y = 1.45, the error over all available shapes is maximum 7% + # (compared to datasheet value) # Now, the new and partly circumference of the stray-path air gap can be calculated # First, the core dimension_y needs to be calculated. # Formular 1: core_dimension_x / core_dimension_y = 1.45 - # Formular 2: core_dimension_x * core_dimension_y - (core_inner_diameter / 2 + window_w) ** 2 * np.pi = (core_inner_diameter / 2 ) ** 2 * np.pi + # Formular 2: core_dimension_x * core_dimension_y - (core_inner_diameter / 2 + window_w) ** 2 * np.pi + # = (core_inner_diameter / 2 ) ** 2 * np.pi # Formular 2 assumes that the outer core cross-section of the core is equal to the inner core cross-section # Formular 1 & 2 needs to be solved to get core_dimension_y: - core_dimension_y = np.sqrt( ( core_inner_diameter ** 2 / 4 + (core_inner_diameter / 2 + window_w) ** 2 ) * np.pi / 1.45) - r_air_gap_ideal_partly = np.log(r_inner / (r_inner - air_gap_total_length)) / mu_0 / (2 * np.pi - 4 * np.arccos(core_dimension_y / 2 / r_inner)) / tablet_height + core_dimension_y = np.sqrt((core_inner_diameter ** 2 / 4 + \ + (core_inner_diameter / 2 + window_w) ** 2) * np.pi / 1.45) + r_air_gap_ideal_partly = np.log(r_inner / (r_inner - air_gap_total_length)) / \ + mu_0 / (2 * np.pi - 4 * np.arccos(core_dimension_y / 2 / r_inner)) / tablet_height r_air_gap = sigma * r_air_gap_ideal_partly return r_air_gap + def r_core_tablet(tablet_height, tablet_radius, mu_r_abs, core_inner_diameter): """ Calculates the magnetic resistance of the core tablet @@ -694,6 +734,7 @@ def r_core_tablet(tablet_height, tablet_radius, mu_r_abs, core_inner_diameter): return np.log(tablet_radius / (core_inner_diameter / 2)) / (2 * np.pi * mu_0 * mu_r_abs * tablet_height) + def r_core_top_bot_radiant(core_inner_diameter, window_w, mu_r_abs, core_top_bot_height): """ Calculates the top or bottom core material part @@ -704,7 +745,9 @@ def r_core_top_bot_radiant(core_inner_diameter, window_w, mu_r_abs, core_top_bot :param core_top_bot_height: height of the core material top / bottom of the winding window """ - return np.log( (core_inner_diameter + 2 * window_w) / core_inner_diameter) / (2 * np.pi * mu_0 * mu_r_abs * core_top_bot_height) + return np.log((core_inner_diameter + 2 * window_w) / core_inner_diameter) / \ + (2 * np.pi * mu_0 * mu_r_abs * core_top_bot_height) + def r_core_round(core_inner_diameter, core_round_height, mu_r_abs): """ @@ -718,7 +761,7 @@ def r_core_round(core_inner_diameter, core_round_height, mu_r_abs): def resistance_solid_wire(core_inner_diameter: float, window_w: float, turns_count: int, conductor_radius: float, - material: str='Copper') -> float: + material: str = 'Copper') -> float: """ Calculates the resistance of a solid wire. @@ -754,11 +797,12 @@ def resistance_solid_wire(core_inner_diameter: float, window_w: float, turns_cou # return R = rho * l / A return total_turn_length / (conductor_radius ** 2 * np.pi) / sigma_copper + def i_rms(time_current_matrix: np.array) -> float: """ RMS calculation from a time-current-vector - :param time_current_matrix: time and current in format [[0, 0.5e-6, 2.5e-6, 3e-6, 5e-6], [16.55, -10.55, -16.55, 10.55, 16.55]] + :param time_current_matrix: time and current in format [[0, 0.5e-6, 2.5e-6, 3e-6], [16.55, -10.55, -16.55, 10.55]] :type time_current_matrix: np.array :return: rms current :rtype: float @@ -778,11 +822,11 @@ def i_rms(time_current_matrix: np.array) -> float: gradient = delta_current / delta_time # calculate solution of (partly) square integral - square_integral = gradient ** 2 / 3 * delta_time ** 3 + gradient * delta_time ** 2 * y_axis + y_axis ** 2 * delta_time + square_integral = gradient ** 2 / 3 * delta_time ** 3 + gradient * delta_time ** 2 * \ + y_axis + y_axis ** 2 * delta_time # add (partly) square integral to total integration sum square_integral_sum += square_integral # return "mean" and "root" to finalize rms calculation return np.sqrt(square_integral_sum / time[-1]) - diff --git a/femmt/hpc.py b/femmt/hpc.py index ef6066b4..a08b9c04 100644 --- a/femmt/hpc.py +++ b/femmt/hpc.py @@ -7,12 +7,16 @@ # Third parry libraries from femmt import MagneticComponent + def _copy_electro_magnetic_necessary_files(src_folder: str, dest_folder: str): - """Inner function. Needed in order to appropriately run parallel simulations since some GetDP files are changed in every simulation instance + """ + Inner function. Needed in order to appropriately run parallel simulations since some GetDP files are + changed in every simulation instance :param src_folder: Path to the base electro_magnetic folder :type src_folder: str - :param dest_folder: Path to the folder where the necessary files shall be stored. The "new" electro_magnetic folder for the corresponding simulation. + :param dest_folder: Path to the folder where the necessary files shall be stored. The "new" electro_magnetic + folder for the corresponding simulation. :type dest_folder: str """ files = ["fields.pro", "ind_axi_python_controlled.pro", "solver.pro", "values.pro"] @@ -22,12 +26,17 @@ def _copy_electro_magnetic_necessary_files(src_folder: str, dest_folder: str): to_path = os.path.join(dest_folder, file) shutil.copy(from_path, to_path) + def hpc_single_simulation(parameters: Dict): - """The default function which is used for parallel execution. Using this function for every model create_model() and single_simulation will be executed. - If for the parallel simulation a custom function is needed you can take this as an example. The parameters dictionary is always given to the parallel executed function. + """ + The default function which is used for parallel execution. Using this function for every model create_model() + and single_simulation will be executed. + If for the parallel simulation a custom function is needed you can take this as an example. The parameters + dictionary is always given to the parallel executed function. - :param parameters: Dictionary containing the needed parameters. One parameters is always 'model' which is the MagneticComponent. The other one is - always 'simulation_parameters' which is also a dict and then contains the parameters given to the run() function (for this specific model). + :param parameters: Dictionary containing the needed parameters. One parameter is always 'model' which is the + MagneticComponent. The other one is always 'simulation_parameters' which is also a dict and then contains + the parameters given to the run() function (for this specific model). :type parameters: Dict """ model = parameters["model"] @@ -45,21 +54,30 @@ def hpc_single_simulation(parameters: Dict): model.create_model(freq=freq, pre_visualize_geometry=False, save_png=False) model.single_simulation(freq=freq, current=current, plot_interpolation=False, show_fem_simulation_results=False) -def run_hpc(n_processes: int, models: List[MagneticComponent], simulation_parameters: List[Dict], working_directory: str, custom_hpc: Callable = None): - """Executes the given models on the given number of parallel processes. Typically this number shouldn't be higher than the number of cores of the processor. - :param n_processes: Number of parallel processes. If this is equal to None the number returned by os.cpu_count() is used. +def run_hpc(n_processes: int, models: List[MagneticComponent], simulation_parameters: List[Dict], + working_directory: str, custom_hpc: Callable = None): + """ + Executes the given models on the given number of parallel processes. Typically, this number + shouldn't be higher than the number of cores of the processor. + + :param n_processes: Number of parallel processes. If None, the number returned by os.cpu_count() is used. :type n_processes: int :param models: List of MagneticComponents which shall be simulated in parallel. :type models: List[MagneticComponent] - :param simulation_parameters: List of dictionaries containing the parameters for the parallel simulation. The Nth item corresponds to the Nth MagneticComponent in models. - For the default hpc_function this dictionary needs the frequency and the current (which is needed for the single_simulation function). - Since the dictionary is given to the hpc_function directly, if a custom_hpc function is used the needed parameters can be added through this dict. + :param simulation_parameters: List of dictionaries containing the parameters for the parallel simulation. + The Nth item corresponds to the Nth MagneticComponent in models. + For the default hpc_function this dictionary needs the frequency and the current + (which is needed for the single_simulation function). + Since the dictionary is given to the hpc_function directly, if a custom_hpc function is used the + needed parameters can be added through this dict. :type simulation_parameters: List[Dict] - :param working_directory: The directory which will store the model data and results data for every parallel simulation. + :param working_directory: The directory stores the model data and results data for every parallel simulation. :type working_directory: str - :param custom_hpc: If set to None the default hpc will be used (create_model() and single_simulation() are executed). If a custom_hpc is set this function - will be called for the parallel execution and the funtion parameters is the simulation_parameter dict taken from the simulation_parameters list., defaults to None + :param custom_hpc: If set to None the default hpc will be used (create_model() and single_simulation() + are executed). If a custom_hpc is set this function + will be called for the parallel execution and the funtion parameters is the simulation_parameter dict taken + from the simulation_parameters list., defaults to None :type custom_hpc: Callable, optional """ electro_magnetic_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "electro_magnetic") @@ -81,7 +99,8 @@ def run_hpc(n_processes: int, models: List[MagneticComponent], simulation_parame _copy_electro_magnetic_necessary_files(electro_magnetic_folder, model_electro_magnetic_directory) # Update directories for each model - model.file_data.update_paths(model_working_directory, model_electro_magnetic_directory, strands_coefficients_folder) + model.file_data.update_paths(model_working_directory, model_electro_magnetic_directory, + strands_coefficients_folder) model.file_data.clear_previous_simulation_results() # Create pool of workers and apply _hpc to it @@ -98,4 +117,3 @@ def run_hpc(n_processes: int, models: List[MagneticComponent], simulation_parame pool.map(hpc_single_simulation, parameters) else: pool.map(custom_hpc, parameters) - \ No newline at end of file diff --git a/tests/integration/test_lint.py b/tests/integration/test_lint.py index 95579cbc..c91e5954 100644 --- a/tests/integration/test_lint.py +++ b/tests/integration/test_lint.py @@ -7,6 +7,8 @@ def test_files(): '../../femmt/logparser.py', '../../femmt/enumerations.py', '../../femmt/dtos.py', + '../../femmt/hpc.py', + '../../femmt/functions_reluctance.py', # examples '../../femmt/examples/advanced_inductor_sweep.py', diff --git a/tox.ini b/tox.ini index d92edd41..977af4c8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [pycodestyle] count = False -ignore = E226,E302,E71, W293, W291, E731 +ignore = E226,E302,E71, W293, W291, E731, E502 max-line-length = 120 statistics = True -# E731: do not assign a lambda expression, use a def \ No newline at end of file +# E502: the backslash is redundant between brackets +# E731: do not assign a lambda expression, use a def From f037cc26995a2aaef03ca81c8cef04f735534ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 21:48:31 +0100 Subject: [PATCH 6/7] fix lint should not run with pytest by default --- .github/workflows/main.yml | 2 +- tests/integration/{test_lint.py => lint.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/integration/{test_lint.py => lint.py} (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 775df12b..92b35504 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: pip install pycodestyle pip install pytest cd tests/integration - pytest test_lint.py + pytest lint.py cd ../.. # code diff --git a/tests/integration/test_lint.py b/tests/integration/lint.py similarity index 100% rename from tests/integration/test_lint.py rename to tests/integration/lint.py From a73aac5bd54a47bc6ef31bc066ad59393386bdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 21:56:48 +0100 Subject: [PATCH 7/7] fix filename after renaming --- tests/integration/lint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/lint.py b/tests/integration/lint.py index c91e5954..fada8313 100644 --- a/tests/integration/lint.py +++ b/tests/integration/lint.py @@ -27,7 +27,7 @@ def test_files(): # tests 'test_femmt.py', - 'test_lint.py', + 'lint.py', # optimization '../../femmt/optimization/sto_dtos.py',