diff --git a/.gitignore b/.gitignore index 073ab3a8..bbae290e 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,4 @@ femmt/examples/paper_thermal_validation.py femmt/examples/example_results # pyspelling -dictionary.dic +dictionary.dic \ No newline at end of file diff --git a/docs/wordlist b/docs/wordlist index e227b54d..6690365e 100644 --- a/docs/wordlist +++ b/docs/wordlist @@ -452,9 +452,79 @@ NbSteps - - - +# electrostatic +Kapton +Capacitances +capacitances +pF +ADE +ACvsBDE +ADvsBCE +DvsABCE +CvsABDE +BvsACDE +AvsBCDE +ABEvsCD +ABCDvsE +ABvsCDE +CDE +NaN +xlsx +InsulationMaterial +CoreMaterial +ZeroVoltage +MagD +MagE +EleSta +airgaps +OutBoundary +DirResValsVoltage +DirResValsCapacitance +DirResValsCapacitanceFromQV +DirResValsCharge +QV +DirResValsTurn +Tetrafluoroethylene +ETFE +Fluorinated +FEP +Propylene +ECTFE +Ethylenechlorotrifluoroethylene +PVDF +Polyvinylidene +Elastomers +TPE +Polyamide +PBT +Polybutylene +Terephthalate +PUR +CPE +TPR +Polychloroprene +Butadiene +SBR +Styrene +EPR +SBR +CSPE +Chlorosulfonated +fluoropolymer +PFA +Perfluoroalkoxy +PTFE +Polytetrafluoroethylene +LCP +DAP +Diallyl +Phthalate +Nomex +Phenolic +bOPET +xlsxwriter +Thermoset +Polyphenylene # logging FEMMTLogger diff --git a/femmt/component.py b/femmt/component.py index 14ef2769..6d5a4690 100644 --- a/femmt/component.py +++ b/femmt/component.py @@ -136,6 +136,11 @@ def __init__(self, simulation_type: SimulationType = SimulationType.FreqDomain, self.current = [] # Defined for every conductor self.current_density = [] # Defined for every conductor self.voltage = [] # Defined for every conductor + self.charge = [] + self.v_core = None + # self.v_ground_core = None + self.v_ground_out_boundary = None + self.capacitance_matrix_nodes = {} self.time = [] # Defined for time domain simulation self.average_currents = [] # Defined for average currents for every winding self.rms_currents = [] # Defined for rms currents for every winding @@ -433,6 +438,14 @@ def set_insulation(self, insulation: Insulation): :param insulation: insulation object :type insulation: Insulation """ + if self.simulation_type == SimulationType.ElectroStatic: + insulation.bobbin_dimensions = True + else: + insulation.bobbin_dimensions = False + + if self.simulation_type == SimulationType.ElectroStatic and insulation.bobbin_dimensions is None: + raise Exception("bobbin parameters must be set in electrostatic simulations") + if insulation.cond_cond is None or not insulation.cond_cond: raise Exception("insulations between the conductors must be set") @@ -1218,12 +1231,111 @@ def excitation_time_domain(self, current_list: list[list[float]], time_list: lis for num in range(len(self.windings)): self.red_freq[num] = 0 + def excitation_electrostatic(self, voltage: list[list[float]] = None, core_voltage: float = None, charge: list[list[float]] = None, + ground_outer_boundary: bool = False, plot_interpolation: bool = False): + """ + Run the electrostatic simulation. + + - Excitation of the electrostatic problem using voltage applied for each turn as [[V_winding_1_turn_1, V_winding_1_turn_2], [V_winding_2_turn_1, + V_winding_2_turn_2],....] or charge applied to each + turn as [[Q_winding_1_turn_1, Q_winding_1_turn_2], [Q_winding_2_turn_1, Q_winding_2_turn_2],....]. + + :param voltage: Values to apply to each turn in each winding as voltages. Example: [[V_winding_1_turn_1, V_winding_2_turn_2], [V_winding_2_turn_1, + V_winding2_turn_42]] + :type voltage: list[list[float]] + :param charge: Values to apply to each turn in each winding as charges. Example: [[Q_winding_1_turn_1, Q_winding_1_turn_2], + [Q_winding_2_turn_1, Q_winding_2_turn_2]] + :type charge: list[list[float]] + :param core_voltage: excite the core with a voltage + :type core_voltage: float + :param ground_outer_boundary: If True, ground the outer boundary. Defaults to False. + :type ground_outer_boundary: bool + :param plot_interpolation: If True, plot the interpolation between the provided values for the material. + :type plot_interpolation: bool + """ + # prevent ground the core and induce a voltage to it in the same time + # if ground_core and core_voltage is not None: + # raise ValueError("Cannot use 'ground_core=True' and 'core_potential' at the same time. Choose one.") + + # Validate input + if voltage is None and charge is None: + raise ValueError("Either 'voltage_list' or 'charge_list' must be provided.") + + if voltage is not None and charge is not None: + raise ValueError("Only one of 'voltage_list' or 'charge_list' should be provided, not both.") + + # Determine excitation type and values + if voltage is not None: + excitation_type = 'voltage' + value_list = voltage + else: + excitation_type = 'charge' + value_list = charge + + # Validate that value_list contains nested lists with the correct number of turns + if not isinstance(value_list, list) or not all(isinstance(inner_list, list) for inner_list in value_list): + raise ValueError( + f"{excitation_type.capitalize()} list should be a list of lists, where each inner list represents values for each turn in a winding.") + + # Check that no negative voltages are provided if using voltage excitation + # if excitation_type == 'voltage': + # for winding_voltages in value_list: + # for voltage in winding_voltages: + # if voltage < 0: + # raise ValueError("Negative voltages are not allowed in this setup. Please adjust the excitation accordingly.") + + # Print excitation details + logger.info(f"\n---\n" + f"Excitation: \n" + f"{excitation_type.capitalize()} Excitation\n" + f"Value(s): {value_list}\n") + + # Set the excitation type (voltage or charge) + self.flag_excitation_type = excitation_type + + # Update material permittivity for the electrostatic analysis if not custom defined + if self.core.permittivity["datasource"] != MaterialDataSource.Custom: + self.core.update_core_material_pro_file(0, self.file_data.electro_magnetic_folder_path, + plot_interpolation) # No frequency is used in electrostatics + + # Apply the excitation to each turn in each winding + num_windings = len(self.windings) + + if excitation_type == 'charge': + # Initialize charges with zero values + self.charge = [[0.0] * len(turns) for turns in value_list] + for winding_index in range(num_windings): + for turn_index in range(len(value_list[winding_index])): + # Assign charge value to each turn in each winding + self.charge[winding_index][turn_index] = value_list[winding_index][turn_index] + else: + # Initialize voltages with zero values + self.voltage = [[0.0] * len(turns) for turns in value_list] + for winding_index in range(num_windings): + for turn_index in range(len(value_list[winding_index])): + # Assign voltage value to each turn in each winding + self.voltage[winding_index][turn_index] = value_list[winding_index][turn_index] + + # assign a voltage to the core + # self.v_core = core_voltage if core_voltage else None + self.v_core = core_voltage if core_voltage is not None else None + + # Set grounding conditions for core and outer boundary + # self.v_ground_core = 0 if ground_core else None + self.v_ground_out_boundary = 0 if ground_outer_boundary else None + + # Set reduced frequency as 0 for DC-like electrostatic setup + self.red_freq = [0 for _ in range(num_windings)] + + logger.info("Electrostatic problem set up for excitation.") + def simulate(self): """Initialize the onelab client. Provides the GetDP based solver with the created mesh file.""" logger.info("\n---\n" "Initialize ONELAB API\n" "Run Simulation\n") - self.log_material_properties() + if not self.simulation_type == SimulationType.ElectroStatic: + self.log_material_properties() # -- Simulation -- # create a new onelab client @@ -1232,10 +1344,10 @@ def simulate(self): gmsh.clear() # get model file names with correct path - solver_freq = os.path.join(self.file_data.electro_magnetic_folder_path, "ind_axi_python_controlled.pro") solver_time = os.path.join(self.file_data.electro_magnetic_folder_path, "ind_axi_python_controlled_time.pro") - + solver_electrostatic = os.path.join(self.file_data.electro_magnetic_folder_path, "ind_axi_python_controlled_electrostatic.pro") + # solver_electrostatic = os.path.join(self.file_data.electro_magnetic_folder_path, "ind_axi_python_controlled_electrostatic_2.pro") os.chdir(self.file_data.working_directory) if self.verbosity == Verbosity.Silent: @@ -1258,6 +1370,9 @@ def simulate(self): " -solve Analysis -pos Map_local " + verbose + to_file_str) # self.onelab_client.runSubClient("myGetDP", getdp_filepath + " " + solver + " -msh " + self.file_data.e_m_mesh_file + # " -solve Analysis -v2 " + verbose) # freeing solutions + if self.simulation_type == SimulationType.ElectroStatic: + self.onelab_client.runSubClient("myGetDP", getdp_filepath + " " + solver_electrostatic + " -msh " + \ + self.file_data.e_m_mesh_file + " -solve EleSta_v -v2 " + verbose + to_file_str) def write_simulation_parameters_to_pro_files(self): """ @@ -1384,8 +1499,7 @@ def single_simulation(self, freq: float, current: list[float], phi_deg: list[flo self.visualize() logger.info(f"The electromagnetic results are stored here: {self.file_data.e_m_results_log_path}") - def time_domain_simulation(self, current_period_vec: list[list[float]], time_period_vec: list[float], - number_of_periods: int, + def time_domain_simulation(self, current_period_vec: list[list[float]], time_period_vec: list[float], number_of_periods: int, plot_interpolation: bool = False, show_fem_simulation_results: bool = True, show_rolling_average: bool = True, rolling_avg_window_size: int = 5, benchmark: bool = False): """ @@ -1455,6 +1569,86 @@ def time_domain_simulation(self, current_period_vec: list[list[float]], time_per if show_rolling_average: self.get_rolling_average(window_size=rolling_avg_window_size) + def electrostatic_simulation(self, voltage: list[list[float]] = None, charge: list[list[float]] = None, core_voltage: float = None, + ground_outer_boundary: bool = False, plot_interpolation: bool = False, + show_fem_simulation_results: bool = True, benchmark: bool = False, save_to_excel_file: bool = False): + """ + Start an electrostatic ONELAB simulation. + + :param voltage: Voltage is a list of list, indicating a voltage of each turn in every winding. + :type voltage: list[list[float]]. + :param charge: Charge is a list of list, indicating a voltage of each turn in every winding. + :type charge: list[list[float]]. + :param core_voltage: assign a voltage to the core + :type core_voltage: float + :param ground_outer_boundary: default to false + :type ground_outer_boundary: bool + :param plot_interpolation: Plot interpolation for the used material between the given data from the material database + :type plot_interpolation: bool + :param show_fem_simulation_results: Set to True to show the simulation results after the simulation has finished + :type show_fem_simulation_results: bool + :param benchmark: Benchmark simulation (stop time). Defaults to False. + :type benchmark: bool + :param save_to_excel_file: Save the log to excel file. + :type save_to_excel_file: bool + """ + # Check if the voltage list is valid + # Check if the voltage or charge is provided as a single list + if isinstance(voltage, list) and all(isinstance(v, (int, float)) for v in voltage): + raise ValueError("The 'voltage' parameter should be a list of lists, with each inner list representing voltages for each turn in a winding.") + + if isinstance(charge, list) and all(isinstance(q, (int, float)) for q in charge): + raise ValueError("The 'charge' parameter should be a list of lists, with each inner list representing charges for each turn in a winding.") + + if benchmark: + start_time = time.time() + self.mesh.generate_electro_magnetic_mesh() + generate_electro_magnetic_mesh_time = time.time() - start_time + + start_time = time.time() + self.excitation_electrostatic(voltage=voltage, charge=charge, core_voltage=core_voltage, + ground_outer_boundary=ground_outer_boundary, plot_interpolation=plot_interpolation) + self.check_create_empty_material_log() + self.write_simulation_parameters_to_pro_files() + prepare_simulation_time = time.time() - start_time + + start_time = time.time() + self.simulate() + real_simulation_time = time.time() - start_time + + start_time = time.time() + self.calculate_and_write_electrostatic_log() + # Convert the log JSON file to Excel + if save_to_excel_file: + json_file_path = self.file_data.electrostatic_results_log_path + output_excel_path = os.path.splitext(json_file_path)[0] + ".xlsx" + ff.json_to_excel(json_file_path, output_excel_path) + logger.info(f"Data has been successfully written to {output_excel_path}") + logging_time = time.time() - start_time + + if show_fem_simulation_results: + self.visualize() + + return generate_electro_magnetic_mesh_time, prepare_simulation_time, real_simulation_time, logging_time + else: + self.mesh.generate_electro_magnetic_mesh() + self.excitation_electrostatic(voltage=voltage, charge=charge, core_voltage=core_voltage, + ground_outer_boundary=ground_outer_boundary, plot_interpolation=plot_interpolation) + self.write_simulation_parameters_to_pro_files() + self.simulate() + self.calculate_and_write_electrostatic_log() + # Convert the log JSON file to Excel + if save_to_excel_file: + json_file_path = self.file_data.electrostatic_results_log_path + output_excel_path = os.path.splitext(json_file_path)[0] + ".xlsx" + ff.json_to_excel(json_file_path, output_excel_path) + logger.info(f"Data has been successfully written to {output_excel_path}") + + if show_fem_simulation_results: + self.visualize() + + logger.info(f"The electrostatic results are stored here: {self.file_data.electrostatic_results_log_path}") + def excitation_sweep(self, frequency_list: list, current_list_list: list, phi_deg_list_list: list, show_last_fem_simulation: bool = False, excitation_meshing_type: ExcitationMeshingType = None, skin_mesh_factor: float = 0.5, @@ -2802,7 +2996,254 @@ def get_inductance_from_reluctance(self): return inductance_matrix # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Post-Processing + # Post-Processing --- Capacitance extraction--- + def get_capacitance_of_inductor_component(self, freq_for_mesh: float = 0.0, show_visual_outputs: bool = False, plot_interpolation: bool = False, + show_fem_simulation_results: bool = False, benchmark: bool = False, save_to_excel: bool = False): + """ + Needed for finding parasitic capacitance of an inductor. A represent the first turn, B represents the last turn. e represents the core. + + A--.------.---A + | | + | | + | c1 + | | + | | + B--|------.---B + | | + c2 c3 + | | + E--.-----.----E + + :param freq_for_mesh: the frequency is used here for generating the mesh + :type freq_for_mesh: float = 0.0 + :param show_visual_outputs: show the electrostatic model before simulation + :type show_visual_outputs: bool, optional + :param plot_interpolation: if True, plot the interpolation between the provided values for the material. + :type plot_interpolation: bool + :param show_fem_simulation_results: if True, show the simulation results after the simulation has finished + :type show_fem_simulation_results: bool + :param benchmark: Benchmark simulation (stop time). Defaults to False. + :type benchmark: bool + :param save_to_excel: save the result to excel file. + :type save_to_excel: bool + """ + potentials = ff.get_defined_potentials("inductor") + m = ff.generate_voltage_matrix("inductor", potentials) # 3×3 matrix + + # 2️⃣ Run simulations and collect energies + energies = [] + self.simulation_type = SimulationType.ElectroStatic + + num_turns = ff.get_number_of_turns_of_winding(self.winding_windows, self.windings, 0) + + for idx, (A, B, E) in enumerate(potentials): + voltages = [ff.distribute_potential_linearly(A, B, num_turns)] + self.create_model(freq=freq_for_mesh, + pre_visualize_geometry=show_visual_outputs, + save_png=False, + skin_mesh_factor=0.5) + + self.electrostatic_simulation( + voltage=voltages, + core_voltage=E, + ground_outer_boundary=False, + plot_interpolation=plot_interpolation, + show_fem_simulation_results=show_fem_simulation_results, + benchmark=benchmark, + save_to_excel_file=save_to_excel + ) + + with open(self.file_data.electrostatic_results_log_path, "r") as f: + energy = json.load(f)["energy"]["stored_component"] + logger.info(f" → Stored Electrostatic Energy (case {idx + 1}): {energy:.4e} J") + energies.append(energy) + + # 3️⃣ Solve for capacitance + c_vec = ff.solve_capacitance(m, np.array(energies)) + logger.info("--- Capacitance Results ---") + labels = ["C(A‑B)", "C(A‑0)", "C(B‑0)"] + + for lbl, val in zip(labels, c_vec): + logger.info(f"{lbl}: {val:.4e} F") + + # 4️⃣ Compute total C_AB + c_ab, c_ae, c_be = c_vec + c_ab_stray = c_ab + (c_ae * c_be) / (c_ae + c_be) + logger.info(f"→ C_AB (incl. parasitic): {c_ab_stray:.4e} F") + + return c_vec + + def get_stray_capacitance_of_inductor_component(self, freq_for_mesh: float = 0.0, show_visual_outputs: bool = False, plot_interpolation: bool = False, + show_fem_simulation_results: bool = True, benchmark: bool = False, save_to_excel: bool = False): + """ + Compute the stray (parasitic) capacitance of an inductor via a single electrostatic simulation. + + Assumes a 1V potential applied across the windings. The effect of the core is ignored. + :param freq_for_mesh: the frequency is used here for generating the mesh + :type freq_for_mesh: float = 0.0 + :param show_visual_outputs: show the electrostatic model before simulation + :type show_visual_outputs: bool, optional + :param plot_interpolation: if True, plot the interpolation between the provided values for the material. + :type plot_interpolation: bool + :param show_fem_simulation_results: if True, show the simulation results after the simulation has finished + :type show_fem_simulation_results: bool + :param benchmark: Benchmark simulation (stop time). Defaults to False. + :type benchmark: bool + :param save_to_excel: save the result to excel file. + :type save_to_excel: bool + """ + # Define terminal voltages for the simulation + v_a = 1 + v_b = 0 + + # Set the simulation type to ElectroStatic + self.simulation_type = SimulationType.ElectroStatic + + # Number of turns (assumes one winding) + winding_index = 0 + num_turns = ff.get_number_of_turns_of_winding(self.winding_windows, self.windings, winding_index) + + # Linear voltage distribution from V_A to V_B across the turns + winding_voltages = [v_a - (v_a - v_b) * j / (num_turns - 1) for j in range(num_turns)] + + # Format the voltages for FEMMT + voltages = [winding_voltages] + self.create_model(freq=freq_for_mesh, pre_visualize_geometry=show_visual_outputs, save_png=False, skin_mesh_factor=0.5) + # Run the electrostatic FEM simulation + self.electrostatic_simulation( + voltage=voltages, + core_voltage=None, + ground_outer_boundary=False, + plot_interpolation=plot_interpolation, + show_fem_simulation_results=show_fem_simulation_results, + benchmark=benchmark, + save_to_excel_file=save_to_excel + ) + + # Retrieve stored electrostatic energy from the log + log_path = self.file_data.electrostatic_results_log_path + with open(log_path, "r", encoding='utf-8') as f: + log = json.load(f) + energy = log["energy"]["stored_component"] + + logger.info(f" → Stored Electrostatic Energy: {energy:.4e} J") + + # Compute stray capacitance using: C = 2W / (V^2), with V = 1V ⇒ C = 2W + c_ab_stray = 2 * energy + + logger.info("\n--- Capacitance Results ---") + logger.info(f"→ Total C_AB: {c_ab_stray:.4e} F") + + return c_ab_stray + + def get_capacitance_of_transformer(self, freq_for_mesh: float = 0.0, c_meas_open: float | None = None, + c_meas_short: float | None = None, measured_capacitances: tuple | list | None = None, + flip_the_sec_terminal: bool = False, show_visual_outputs: bool = False, plot_interpolation: bool = False, + show_fem_simulation_results: bool = False, benchmark: bool = False, save_to_excel: bool = False, + show_plot_comparison: bool = True): + r""" + Get 10 parasitic capacitance of a transformer. + + Perform 10 electrostatic simulations and calculate the 10 parasitic capacitance through W = 0.5 CV^2. + + A--.------.-----c4--------.------.------C + | | \ / | | + | | c5 c6 | | + | c1 \/ c2 | + | | / \ | | + | | / \ | | + B--|------.-----c3--------.------|------D + | | | | + c7 c8 c10 c9 + | | | | + E--.-----.-----------------------.------E + + The result can be compared to the results obtain from the measurement. + + :param freq_for_mesh: the frequency is used here for generating the mesh + :type freq_for_mesh: float = 0.0 + :param c_meas_open: measured open-circuit capacitance provided by the user. + :type c_meas_open: float + :param c_meas_short: measured short-circuit capacitance provided by the user. + :type c_meas_short: float + :param measured_capacitances: measured 10 capacitances provided by the user. + :type measured_capacitances: float + :param flip_the_sec_terminal: flip the terminal C and D of the secondary. + :type flip_the_sec_terminal: bool + :param show_visual_outputs: show the electrostatic model before simulation + :type show_visual_outputs: bool, optional + :param plot_interpolation: if True, plot the interpolation between the provided values for the material. + :type plot_interpolation: bool + :param show_fem_simulation_results: if True, show the simulation results after the simulation has finished + :type show_fem_simulation_results: bool + :param benchmark: Benchmark simulation (stop time). Defaults to False. + :type benchmark: bool + :param save_to_excel: save the result to excel file. + :type save_to_excel: bool + :param show_plot_comparison: compare between the simulation and measurement results. + :type show_plot_comparison: bool + """ + potentials = ff.get_defined_potentials('transformer') + m = ff.generate_voltage_matrix("transformer", potentials, flip_the_sec_terminal) + + w_e = [] # Electrostatic energies for each case + # Set the simulation type to ElectroStatic before running the open-circuit simulations + self.simulation_type = SimulationType.ElectroStatic + # Get number of turns for both windings + num_turns_w1 = ff.get_number_of_turns_of_winding(self.winding_windows, self.windings, 0) + num_turns_w2 = ff.get_number_of_turns_of_winding(self.winding_windows, self.windings, 1) + # Run electrostatic simulation + for i, (A, B, C, D) in enumerate(potentials): + # Interpolate voltages across both windings + voltages_winding_1 = ff.distribute_potential_linearly(A, B, num_turns_w1) + voltages_winding_2 = ff.distribute_potential_linearly(C, D, num_turns_w2) + # Run electrostatic simulation + self.create_model(freq=freq_for_mesh, pre_visualize_geometry=show_visual_outputs, save_png=False, skin_mesh_factor=0.5) + self.electrostatic_simulation( + voltage=[voltages_winding_1, voltages_winding_2], + core_voltage=0, + ground_outer_boundary=False, + plot_interpolation=plot_interpolation, + show_fem_simulation_results=show_fem_simulation_results, + benchmark=benchmark, + save_to_excel_file=save_to_excel + ) + # Read energy log + log_path = self.file_data.electrostatic_results_log_path + with open(log_path, "r", encoding="utf-8") as f: + log = json.load(f) + energy = log["energy"]["stored_component"] + + logger.info(f" → Stored Electrostatic Energy (Scenario {i + 1}): {energy:.4e} J") + w_e.append(energy) + + c_vec = ff.solve_capacitance(m, np.array(w_e)) + logger.info("\n--- Capacitance Coefficients (Transformer) ---") + for idx, c_val in enumerate(c_vec): + logger.info(f"C[{idx + 1}]: {c_val:.4e} F") + + # ------------------------------------------------------------------ + # 0) internal mapping of the 10 connection sums + # ------------------------------------------------------------------ + if measured_capacitances is not None: + ff.compare_and_plot_connection_capacitance_of_transformer(c_vec, measured_capacitance=measured_capacitances, show_plot=show_plot_comparison) + # ------------------------------------------------------------------ + # 1) open circuit capacitance + # ------------------------------------------------------------------ + c_sim_open = ff.get_open_circuit_capacitance(c_vec=c_vec, num_turns_w1=num_turns_w1, num_turns_w2=num_turns_w2) + logger.info(f"\n→ Calculated Open Capacitance (C_sim_open): {c_sim_open:.4e} F") + # ------------------------------------------------------------------ + # 2) short circuit capacitance + # ------------------------------------------------------------------ + c_sim_short = ff.get_short_circuit_capacitance(c_vec=c_vec) + logger.info(f"\n→ Calculated short Capacitance (C_sim_short): {c_sim_short:.4e} F") + + # comparison + if show_plot_comparison and (c_meas_open is not None or c_meas_short is not None): + ff.plot_open_and_short_comparison(c_sim_open, c_sim_short, c_meas_open, c_meas_short) + + return c_vec + def get_inductances(self, I0: float, op_frequency: float = 0, skin_mesh_factor: float = 1, visualize_last_fem_simulation: bool = False, silent: bool = False, compare_with_inductance_from_reluctance: bool = False): """ @@ -3098,44 +3539,53 @@ def write_electro_magnetic_parameter_pro(self): text_file.write("Flag_Time_Domain = 1;\n") text_file.write("Flag_Freq_Domain = 0;\n") text_file.write("Flag_Static = 0;\n") + if self.simulation_type == SimulationType.ElectroStatic: + text_file.write("Flag_Static = 1;\n") + text_file.write("Flag_Freq_Domain = 0;\n") + text_file.write("Flag_Time_Domain = 0;\n") - # Frequency - text_file.write("Freq = %s;\n" % self.frequency) - text_file.write(f"delta = {self.delta};\n") - - # Core Loss - text_file.write(f"Flag_Steinmetz_loss = {self.core.steinmetz_loss};\n") - text_file.write(f"Flag_Generalized_Steinmetz_loss = {self.core.generalized_steinmetz_loss};\n") - - if self.core.sigma != 0 and self.core.sigma is not None: - text_file.write("Flag_Conducting_Core = 1;\n") - if isinstance(self.core.sigma, str): - # TODO: Make following definition general - # self.core.sigma = 2 * np.pi * self.frequency * epsilon_0 * f_N95_er_imag(f=self.frequency) + 1 / 6 - self.core.sigma = 1 / 6 - text_file.write(f"sigma_core = {self.core.sigma.real};\n") - text_file.write(f"sigma_core_imag = {self.core.sigma.imag};\n") - else: - text_file.write("Flag_Conducting_Core = 0;\n") - - if self.core.steinmetz_loss: - text_file.write(f"ki = {self.core.ki};\n") - text_file.write(f"alpha = {self.core.alpha};\n") - text_file.write(f"beta = {self.core.beta};\n") - if self.core.generalized_steinmetz_loss: - text_file.write(f"t_rise = {self.t_rise};\n") - text_file.write(f"t_fall = {self.t_fall};\n") - text_file.write(f"f_switch = {self.f_switch};\n") - # time domain parameters - if self.simulation_type == SimulationType.TimeDomain: - text_file.write(f"T = {self.time_period};\n") - text_file.write(f"time0 = {self.initial_time};\n") - text_file.write(f"timemax = {self.max_time};\n") - text_file.write(f"NbStepsPerPeriod = {self.nb_steps_per_period};\n") - text_file.write(f"NbSteps = {self.nb_steps};\n") - text_file.write(f"delta_t = {self.step_time};\n") - time_values_str = ', '.join(map(str, self.time)) - text_file.write(f"TimeList = {{{time_values_str}}};\n") # TimeList is interpolated with current lists in the solver + # Airgap number + if self.simulation_type == SimulationType.ElectroStatic: + text_file.write("n_airgaps = {};\n".format(len(self.air_gaps.midpoints))) + + # Frequency + if not self.simulation_type == SimulationType.ElectroStatic: + text_file.write("Freq = %s;\n" % self.frequency) + text_file.write(f"delta = {self.delta};\n") + + # Core Loss + text_file.write(f"Flag_Steinmetz_loss = {self.core.steinmetz_loss};\n") + text_file.write(f"Flag_Generalized_Steinmetz_loss = {self.core.generalized_steinmetz_loss};\n") + + if self.core.sigma != 0 and self.core.sigma is not None: + text_file.write("Flag_Conducting_Core = 1;\n") + if isinstance(self.core.sigma, str): + # TODO: Make following definition general + # self.core.sigma = 2 * np.pi * self.frequency * epsilon_0 * f_N95_er_imag(f=self.frequency) + 1 / 6 + self.core.sigma = 1 / 6 + text_file.write(f"sigma_core = {self.core.sigma.real};\n") + text_file.write(f"sigma_core_imag = {self.core.sigma.imag};\n") + else: + text_file.write("Flag_Conducting_Core = 0;\n") + + if self.core.steinmetz_loss: + text_file.write(f"ki = {self.core.ki};\n") + text_file.write(f"alpha = {self.core.alpha};\n") + text_file.write(f"beta = {self.core.beta};\n") + if self.core.generalized_steinmetz_loss: + text_file.write(f"t_rise = {self.t_rise};\n") + text_file.write(f"t_fall = {self.t_fall};\n") + text_file.write(f"f_switch = {self.f_switch};\n") + # time domain parameters + if self.simulation_type == SimulationType.TimeDomain: + text_file.write(f"T = {self.time_period};\n") + text_file.write(f"time0 = {self.initial_time};\n") + text_file.write(f"timemax = {self.max_time};\n") + text_file.write(f"NbStepsPerPeriod = {self.nb_steps_per_period};\n") + text_file.write(f"NbSteps = {self.nb_steps};\n") + text_file.write(f"delta_t = {self.step_time};\n") + time_values_str = ', '.join(map(str, self.time)) + text_file.write(f"TimeList = {{{time_values_str}}};\n") # TimeList is interpolated with current lists in the solver # Conductor specific definitions for winding_number in range(len(self.windings)): @@ -3163,6 +3613,13 @@ def write_electro_magnetic_parameter_pro(self): text_file.write(f"NbrCond_{winding_number + 1} = {turns};\n") text_file.write(f"AreaCell_{winding_number + 1} = {self.windings[winding_number].a_cell};\n") + # relative permittivity + # Turn insulation is not implemented yet for RectangularSolid + if self.windings[winding_number].conductor_type == ConductorType.RectangularSolid: + text_file.write(f"er_turns_insulation_{winding_number + 1} = 1.0;\n") + else: + text_file.write(f"er_turns_insulation_{winding_number + 1} = {self.insulation.er_turn_insulation[winding_number]};\n") + # For stranded Conductors: # text_file.write(f"NbrstrandedCond = {self.turns};\n") # redundant if self.windings[winding_number].conductor_type == ConductorType.RoundLitz: @@ -3195,9 +3652,31 @@ def write_electro_magnetic_parameter_pro(self): text_file.write(f"Val_EE_{winding_number + 1} = {self.current_density[winding_number]};\n") raise NotImplementedError - if self.flag_excitation_type == 'voltage': - text_file.write(f"Val_EE_{winding_number + 1} = {self.voltage[winding_number]};\n") - raise NotImplementedError + if self.simulation_type == SimulationType.ElectroStatic: + if self.voltage is not None: + text_file.write("Flag_voltage = 1;\n") + text_file.write("Flag_charge = 0;\n") + turns = self.voltage[winding_number] + for turn_index, voltage_value in enumerate(turns): + text_file.write(f"Voltage_{winding_number + 1}_{turn_index + 1} = {voltage_value};\n") + elif self.charge is not None: + text_file.write("Flag_charge = 1;\n") + text_file.write("Flag_voltage = 0;\n") + turns = self.charge[winding_number] + for turn_index, charge_value in enumerate(turns): + text_file.write(f"Charge_{winding_number + 1}_{turn_index + 1} = {charge_value};\n") + + if self.v_core is not None: + text_file.write("v_core = {};\n".format(self.v_core)) + text_file.write("Flag_excite_core = 1;\n") + else: + text_file.write("Flag_excite_core = 0;\n") + + if self.v_ground_out_boundary == 0: + text_file.write("Flag_ground_OutBoundary = 1;\n") + text_file.write("v_ground_OutBoundary = 0;\n") + else: + text_file.write("Flag_ground_OutBoundary = 0;\n") logger.info(f"Cell surface area: {self.windings[winding_number].a_cell} \n" f"Reduced frequency: {self.red_freq[winding_number]}") @@ -3219,6 +3698,11 @@ def write_electro_magnetic_parameter_pro(self): text_file.write("mu0 = 4.e-7 * Pi;\n") text_file.write("nu0 = 1 / mu0;\n") text_file.write(f"e0 = {epsilon_0};\n") + if self.insulation.er_layer_insulation is not None: + text_file.write(f"er_layer_insulation = {self.insulation.er_layer_insulation};\n") + else: + text_file.write("er_layer_insulation = 1.0;\n") + text_file.write(f"er_bobbin = {self.insulation.er_bobbin};\n") # Material Properties @@ -3271,9 +3755,28 @@ def write_electro_magnetic_post_pro(self): text_file.write(f"DirResVals = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/\";\n") text_file.write( f"DirResValsCore = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/core_parts/\";\n") + text_file.write( + f"DirResValsCapacitance = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Capacitance/\";\n") for i in range(1, len(self.windings) + 1): text_file.write( f"DirResValsWinding_{i} = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Winding_{i}/\";\n") + text_file.write( + f"DirResValsCharge_{i} = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Winding_{i}/Charges/\";\n" + ) + text_file.write( + f"DirResValsVoltage_{i} = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Winding_{i}/Voltages/\";\n" + ) + text_file.write( + f"DirResValsCapacitanceFromQV_{i} = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Winding_{i}/Capacitance_From_QV/\";\n" + ) + turns = ff.get_number_of_turns_of_winding(winding_windows=self.winding_windows, windings=self.windings, + winding_number=i - 1) + # Write directories for each turn within the respective capacitance folder + for turn in range(1, turns + 1): + + text_file.write( + f"DirResValsTurn_{turn} = \"{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Capacitance/Turn_{turn}/\";\n" + ) # text_file.write(f"DirResValsSecondary = \ # "{self.file_data.e_m_values_folder_path.replace(backslash, '/')}/Secondary/\";\n") # text_file.write(f"DirResValsTertiary = \ @@ -3852,6 +4355,101 @@ def write_and_calculate_common_log(self, inductance_dict: dict = None): return log_dict + def calculate_and_write_electrostatic_log(self): + """ + Extract data from the electrostatic simulation and write it into a log file. + + This file includes: + * charge + * energy stored in air and component + * average voltages on the core + * calculated capacitance between winding turns and between different windings + + """ + log_dict = {"charges": {}, "energy": {}, "average_voltages": {}, "capacitances": {}} + + # Extract charge and energy + values_folder = self.file_data.e_m_values_folder_path + + log_dict["charges"] = self.load_result("charge_Air", res_type="value", last_n=1)[0] + log_dict["energy"]["stored_air"] = self.load_result("Energy_Stored_Air", res_type="value", last_n=1)[0] + log_dict["energy"]["stored_core"] = self.load_result("Energy_Stored_Core", res_type="value", last_n=1)[0] + log_dict["average_voltages"]["core"] = self.load_result("Avg_Core_voltage", res_type="circuit", last_n=1)[0] + log_dict["energy"]["stored_component"] = self.load_result("Energy_Stored_Component", res_type="value", last_n=1)[0] + + # Extract capacitance data + capacitance_folder = os.path.join(values_folder, "Capacitance") + log_dict["capacitances"] = {"within_winding": {}, "between_windings": {}, "between_turns_core": {}} + + for winding_number in range(len(self.windings)): + turns = ff.get_number_of_turns_of_winding(winding_windows=self.winding_windows, windings=self.windings, + winding_number=winding_number) + winding_name = f"Winding_{winding_number + 1}" + log_dict["capacitances"]["within_winding"][winding_name] = {} + + # Capacitance between turns within the same winding + for turn1 in range(1, turns + 1): + turn_key = f"Turn_{turn1}" + if turn_key not in log_dict["capacitances"]["within_winding"][winding_name]: + log_dict["capacitances"]["within_winding"][winding_name][turn_key] = {} + for turn2 in range(1, turns + 1): + if turn1 != turn2: + capacitance_key = f"C_{winding_number + 1}_{min(turn1, turn2)}_{max(turn1, turn2)}" + capacitance_file_path = os.path.join(capacitance_folder, f"Turn_{min(turn1, turn2)}", capacitance_key) + capacitance_value = self.load_result(res_name=capacitance_file_path, res_type="value", last_n=1)[0] + log_dict["capacitances"]["within_winding"][winding_name][turn_key][f"to_Turn_{turn2}"] = capacitance_value + + # Capacitance between turns in different windings + for winding1 in range(len(self.windings)): + winding1_name = f"Winding_{winding1 + 1}" + log_dict["capacitances"]["between_windings"][winding1_name] = {} + for winding2 in range(len(self.windings)): + if winding1 == winding2: + continue + turns1 = ff.get_number_of_turns_of_winding(winding_windows=self.winding_windows, windings=self.windings, + winding_number=winding1) + turns2 = ff.get_number_of_turns_of_winding(winding_windows=self.winding_windows, windings=self.windings, + winding_number=winding2) + winding2_name = f"Winding_{winding2 + 1}" + log_dict["capacitances"]["between_windings"][winding1_name][winding2_name] = {} + + for turn1 in range(1, turns1 + 1): + turn1_key = f"Turn_{turn1}" + if turn1_key not in log_dict["capacitances"]["between_windings"][winding1_name][winding2_name]: + log_dict["capacitances"]["between_windings"][winding1_name][winding2_name][turn1_key] = {} + for turn2 in range(1, turns2 + 1): + capacitance_key = f"C_Cross_{winding1 + 1}_{turn1}_{winding2 + 1}_{turn2}" + capacitance_file_path = os.path.join(capacitance_folder, f"Turn_{turn1}", capacitance_key) + capacitance_value = self.load_result(res_name=capacitance_file_path, res_type="value", last_n=1)[0] + log_dict["capacitances"]["between_windings"][winding1_name][winding2_name][turn1_key][f"to_Turn_{turn2}"] = capacitance_value + + # Adding symmetric capacitance value + if winding2_name not in log_dict["capacitances"]["between_windings"]: + log_dict["capacitances"]["between_windings"][winding2_name] = {} + if winding1_name not in log_dict["capacitances"]["between_windings"][winding2_name]: + log_dict["capacitances"]["between_windings"][winding2_name][winding1_name] = {} + if f"Turn_{turn2}" not in log_dict["capacitances"]["between_windings"][winding2_name][winding1_name]: + log_dict["capacitances"]["between_windings"][winding2_name][winding1_name][f"Turn_{turn2}"] = {} + log_dict["capacitances"]["between_windings"][winding2_name][winding1_name][f"Turn_{turn2}"][f"to_Turn_{turn1}"] = capacitance_value + + # Capacitance between each turn and the core + for winding_number in range(len(self.windings)): + winding_name = f"Winding_{winding_number + 1}" + log_dict["capacitances"]["between_turns_core"][winding_name] = {} + + turns = ff.get_number_of_turns_of_winding(winding_windows=self.winding_windows, windings=self.windings, + winding_number=winding_number) + for turn in range(1, turns + 1): + turn_key = f"Turn_{turn}" + capacitance_key = f"C_{winding_number + 1}_{turn}_Core" + capacitance_file_path = os.path.join(capacitance_folder, f"Turn_{turn}", capacitance_key) + capacitance_value = self.load_result(res_name=capacitance_file_path, res_type="value", last_n=1)[0] + log_dict["capacitances"]["between_turns_core"][winding_name][turn_key] = capacitance_value + + # ====== save data as JSON ====== + with open(self.file_data.electrostatic_results_log_path, "w+", encoding='utf-8') as outfile: + json.dump(log_dict, outfile, indent=2, ensure_ascii=False) + def read_log(self) -> dict: """ Read results from electromagnetic simulation. @@ -4072,6 +4670,78 @@ def visualize(self): gmsh.option.setNumber(f"View[{view}].NbIso", 40) view += 1 + if self.simulation_type == SimulationType.ElectroStatic: + # Visualization for electrostatic simulations + view = 0 + + # Potentials + if any(self.windings[i].conductor_type != ConductorType.RoundLitz for i in range(len(self.windings))): + gmsh.open(os.path.join(self.file_data.e_m_fields_folder_path, "Potential.pos")) + gmsh.option.setNumber(f"View[{view}].ScaleType", 1) + gmsh.option.setNumber(f"View[{view}].RangeType", 2) + gmsh.option.setNumber(f"View[{view}].SaturateValues", 1) + gmsh.option.setNumber(f"View[{view}].CustomMin", gmsh.option.getNumber(f"View[{view}].Min") + epsilon) + gmsh.option.setNumber(f"View[{view}].CustomMax", gmsh.option.getNumber(f"View[{view}].Max")) + gmsh.option.setNumber(f"View[{view}].ColormapNumber", 1) + gmsh.option.setNumber(f"View[{view}].IntervalsType", 2) + gmsh.option.setNumber(f"View[{view}].NbIso", 40) + gmsh.option.setNumber(f"View[{view}].ShowTime", 0) + view += 1 + + # Potential on core + gmsh.open(os.path.join(self.file_data.e_m_fields_folder_path, "Voltage_Core_Map.pos")) + gmsh.option.setNumber(f"View[{view}].ScaleType", 1) + gmsh.option.setNumber(f"View[{view}].RangeType", 2) + gmsh.option.setNumber(f"View[{view}].SaturateValues", 1) + gmsh.option.setNumber(f"View[{view}].CustomMin", gmsh.option.getNumber(f"View[{view}].Min") + epsilon) + gmsh.option.setNumber(f"View[{view}].CustomMax", gmsh.option.getNumber(f"View[{view}].Max")) + gmsh.option.setNumber(f"View[{view}].ColormapNumber", 1) + gmsh.option.setNumber(f"View[{view}].IntervalsType", 2) + gmsh.option.setNumber(f"View[{view}].NbIso", 40) + gmsh.option.setNumber(f"View[{view}].ShowTime", 0) + view += 1 + + # Electric Field + gmsh.open(os.path.join(self.file_data.e_m_fields_folder_path, "MagE.pos")) + gmsh.option.setNumber(f"View[{view}].ScaleType", 2) + gmsh.option.setNumber(f"View[{view}].RangeType", 2) + gmsh.option.setNumber(f"View[{view}].SaturateValues", 1) + gmsh.option.setNumber(f"View[{view}].CustomMin", gmsh.option.getNumber(f"View[{view}].Min") + epsilon) + gmsh.option.setNumber(f"View[{view}].CustomMax", gmsh.option.getNumber(f"View[{view}].Max")) + gmsh.option.setNumber(f"View[{view}].ColormapNumber", 1) + gmsh.option.setNumber(f"View[{view}].IntervalsType", 2) + gmsh.option.setNumber(f"View[{view}].NbIso", 40) + gmsh.option.setNumber(f"View[{view}].ShowTime", 0) + + view += 1 + + # Stored energy + # gmsh.open(os.path.join(self.file_data.e_m_fields_folder_path, "We.pos")) + # gmsh.option.setNumber(f"View[{view}].ScaleType", 2) + # gmsh.option.setNumber(f"View[{view}].RangeType", 2) + # gmsh.option.setNumber(f"View[{view}].SaturateValues", 1) + # gmsh.option.setNumber(f"View[{view}].CustomMin", gmsh.option.getNumber(f"View[{view}].Min") + epsilon) + # gmsh.option.setNumber(f"View[{view}].CustomMax", gmsh.option.getNumber(f"View[{view}].Max")) + # gmsh.option.setNumber(f"View[{view}].ColormapNumber", 1) + # gmsh.option.setNumber(f"View[{view}].IntervalsType", 2) + # gmsh.option.setNumber(f"View[{view}].NbIso", 40) + # gmsh.option.setNumber(f"View[{view}].ShowTime", 0) + + view += 1 + + # Displacement Field + gmsh.open(os.path.join(self.file_data.e_m_fields_folder_path, "MagD.pos")) + gmsh.option.setNumber(f"View[{view}].ScaleType", 2) + gmsh.option.setNumber(f"View[{view}].RangeType", 2) + gmsh.option.setNumber(f"View[{view}].SaturateValues", 1) + gmsh.option.setNumber(f"View[{view}].CustomMin", gmsh.option.getNumber(f"View[{view}].Min") + epsilon) + gmsh.option.setNumber(f"View[{view}].CustomMax", gmsh.option.getNumber(f"View[{view}].Max")) + gmsh.option.setNumber(f"View[{view}].ColormapNumber", 1) + gmsh.option.setNumber(f"View[{view}].IntervalsType", 2) + gmsh.option.setNumber(f"View[{view}].NbIso", 40) + gmsh.option.setNumber(f"View[{view}].ShowTime", 0) + view += 1 + if self.simulation_type == SimulationType.TimeDomain: # merge view files gmsh.merge(os.path.join(self.file_data.e_m_fields_folder_path, 'j2F_density.pos')) @@ -4348,7 +5018,7 @@ def load_result(self, res_name: str, res_type: str = "value", last_n: int = 1, p else: raise ValueError(f"Invalid res_type: {res_type}") - if self.simulation_type == SimulationType.FreqDomain: + if self.simulation_type == SimulationType.FreqDomain or self.simulation_type == SimulationType.ElectroStatic: with open(os.path.join(res_path, f"{res_name}.dat")) as fd: lines = fd.readlines()[-last_n:] @@ -4891,6 +5561,721 @@ def write_femm_log(self): json.dump(log, file, indent=2, ensure_ascii=False) file.close() + def femm_reference_electrostatic(self, voltages: list[list[float]], ground_core: bool = False, ground_outer_boundary: bool = False, non_visualize: int = 0, + mesh_size: float = 0.0, mesh_size_conductor: float = 0.0, save_to_excel_file: bool = False, + compare_femmt_to_femm: bool = False) -> None: + """ + Set up a reference electrostatic simulation using FEMM. + + :param voltages: A list of voltages to be applied to each winding. + :type voltages: list[list[float]] + :param ground_core: Ground core. + :type ground_core: bool + :param ground_outer_boundary: Grounding outer boundary. + :type ground_outer_boundary: bool + :param non_visualize: Flag to control visualization. + :param mesh_size: Mesh size for general areas. + :param mesh_size_conductor: Mesh size specifically for conductors. + :param save_to_excel_file: Save the log to excel file. + :type save_to_excel_file: bool + :param compare_femmt_to_femm: compare file logs between FEMMT and FEMM in excel form. + :type compare_femmt_to_femm: bool + : + """ + automesh = 1 if mesh_size == 0.0 else 0 + automesh_conductor = 1 if mesh_size_conductor == 0.0 else 0 + + if os.name == 'nt': + ff.install_pyfemm_if_missing() + if not self.femm_is_imported: + globals()["femm"] = __import__("femm") + self.femm_is_imported = True + else: + raise Exception('This command is only executable on Windows computers.') + + self.file_data.create_folders(self.file_data.femm_folder_path) + + # Ensure voltages is a list + if not isinstance(voltages, list): + voltages = [voltages] + + # == Initialize FEMM == + femm.openfemm(non_visualize) + femm.newdocument(1) # 1 is for electrostatics + + # Proper use of required parameters + femm.ei_probdef('meters', 'axi', 1e-8, 0, 30) # units, type, precision, depth, and minangle + + # == Materials == + # Define materials with proper relative permittivity and volume charge density + femm.ei_addmaterial('Air', 1, 1, 0, ) + + # Core material + core_permittivity = 5000 # Value for core material permittivity + femm.ei_addmaterial('CoreMaterial', core_permittivity, core_permittivity, 0) + + # Insulation + insulation_permittivity = 5.5 + femm.ei_addmaterial('InsulationMaterial', insulation_permittivity, insulation_permittivity, 0) + + # Conductor material + femm.ei_addmaterial('Copper', 1, 1, 0) + + # == Geometry == + # Add core + if self.air_gaps.number == 0: + femm.ei_drawline(self.two_d_axi.p_window[4, 0], + self.two_d_axi.p_window[4, 1], + self.two_d_axi.p_window[5, 0], + self.two_d_axi.p_window[5, 1]) + femm.ei_drawline(self.two_d_axi.p_window[5, 0], + self.two_d_axi.p_window[5, 1], + self.two_d_axi.p_window[7, 0], + self.two_d_axi.p_window[7, 1]) + femm.ei_drawline(self.two_d_axi.p_window[7, 0], + self.two_d_axi.p_window[7, 1], + self.two_d_axi.p_window[6, 0], + self.two_d_axi.p_window[6, 1]) + femm.ei_drawline(self.two_d_axi.p_window[6, 0], + self.two_d_axi.p_window[6, 1], + self.two_d_axi.p_window[4, 0], + self.two_d_axi.p_window[4, 1]) + femm.ei_drawline(0, + self.two_d_axi.p_outer[2, 1], + self.two_d_axi.p_outer[3, 0], + self.two_d_axi.p_outer[3, 1]) + femm.ei_drawline(self.two_d_axi.p_outer[3, 0], + self.two_d_axi.p_outer[3, 1], + self.two_d_axi.p_outer[1, 0], + self.two_d_axi.p_outer[1, 1]) + femm.ei_drawline(self.two_d_axi.p_outer[1, 0], + self.two_d_axi.p_outer[1, 1], + 0, + self.two_d_axi.p_outer[0, 1]) + femm.ei_drawline(0, + self.two_d_axi.p_outer[0, 1], + 0, + self.two_d_axi.p_outer[2, 1]) + elif self.air_gaps.number > 0: + femm.ei_drawline(0, + self.two_d_axi.p_air_gaps[0, 1], + self.two_d_axi.p_air_gaps[1, 0], + self.two_d_axi.p_air_gaps[1, 1]) + femm.ei_drawline(self.two_d_axi.p_air_gaps[1, 0], + self.two_d_axi.p_air_gaps[1, 1], + self.two_d_axi.p_window[4, 0], + self.two_d_axi.p_window[4, 1]) + femm.ei_drawline(self.two_d_axi.p_window[4, 0], + self.two_d_axi.p_window[4, 1], + self.two_d_axi.p_window[5, 0], + self.two_d_axi.p_window[5, 1]) + femm.ei_drawline(self.two_d_axi.p_window[5, 0], + self.two_d_axi.p_window[5, 1], + self.two_d_axi.p_window[7, 0], + self.two_d_axi.p_window[7, 1]) + femm.ei_drawline(self.two_d_axi.p_window[7, 0], + self.two_d_axi.p_window[7, 1], + self.two_d_axi.p_window[6, 0], + self.two_d_axi.p_window[6, 1]) + femm.ei_drawline(self.two_d_axi.p_window[6, 0], + self.two_d_axi.p_window[6, 1], + self.two_d_axi.p_air_gaps[3, 0], + self.two_d_axi.p_air_gaps[3, 1]) + femm.ei_drawline(self.two_d_axi.p_air_gaps[3, 0], + self.two_d_axi.p_air_gaps[3, 1], + 0, + self.two_d_axi.p_air_gaps[2, 1]) + femm.ei_drawline(0, + self.two_d_axi.p_air_gaps[2, 1], + 0, + self.two_d_axi.p_outer[2, 1]) + femm.ei_drawline(0, + self.two_d_axi.p_outer[2, 1], + self.two_d_axi.p_outer[3, 0], + self.two_d_axi.p_outer[3, 1]) + femm.ei_drawline(self.two_d_axi.p_outer[3, 0], + self.two_d_axi.p_outer[3, 1], + self.two_d_axi.p_outer[1, 0], + self.two_d_axi.p_outer[1, 1]) + femm.ei_drawline(self.two_d_axi.p_outer[1, 0], + self.two_d_axi.p_outer[1, 1], + 0, + self.two_d_axi.p_outer[0, 1]) + femm.ei_drawline(0, + self.two_d_axi.p_outer[0, 1], + 0, + self.two_d_axi.p_air_gaps[0, 1]) + else: + raise Exception("Negative air gap number is not allowed") + + # Add Insulation + if self.insulation.bobbin_dimensions: + if self.insulation.max_aspect_ratio == 0: + # If no aspect ratio is set insulations will not be drawn + return + else: + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + + # Handle the insulation delta for electrostatic transformer + # top - bot + bobbin_h = self.insulation.bobbin_window_h + insulation_delta_top_bot = (self.core.window_h - bobbin_h) / 2 + # left + bobbin_inner_diameter = self.insulation.bobbin_inner_diameter / 2 + core_inner_diameter = self.core.core_inner_diameter / 2 + insulation_delta_left = bobbin_inner_diameter - core_inner_diameter + # Useful points for insulation creation + left_x = self.core.core_inner_diameter / 2 + insulation_delta_left + top_y = self.core.window_h / 2 - insulation_delta_top_bot + right_x = self.core.r_inner - insulation_delta + bot_y = -self.core.window_h / 2 + insulation_delta_top_bot + + # Useful lengths + left_iso_width = self.insulation.core_cond[2] - insulation_delta - insulation_delta + top_iso_height = self.insulation.core_cond[0] - insulation_delta - insulation_delta + right_iso_width = self.insulation.core_cond[3] - insulation_delta - insulation_delta + bot_iso_height = self.insulation.core_cond[1] - insulation_delta - insulation_delta + + # Left insulation + femm.ei_drawline(left_x, + top_y - top_iso_height - insulation_delta, + left_x + left_iso_width, + top_y - top_iso_height - insulation_delta) + femm.ei_drawline(left_x + left_iso_width, + top_y - top_iso_height - insulation_delta, + left_x + left_iso_width, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(left_x + left_iso_width, + bot_y + bot_iso_height + insulation_delta, + left_x, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(left_x, + bot_y + bot_iso_height + insulation_delta, + left_x, + top_y - top_iso_height - insulation_delta) + + # top insulation + femm.ei_drawline(left_x, + top_y, + right_x, + top_y) + femm.ei_drawline(right_x, + top_y, + right_x, + top_y - top_iso_height) + femm.ei_drawline(right_x, + top_y - top_iso_height, + left_x, + top_y - top_iso_height) + femm.ei_drawline(left_x, + top_y - top_iso_height, + left_x, + top_y) + + # right insulation + femm.ei_drawline(right_x - right_iso_width, + top_y - top_iso_height - insulation_delta, + right_x, + top_y - top_iso_height - insulation_delta) + femm.ei_drawline(right_x, + top_y - top_iso_height - insulation_delta, + right_x, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(right_x, + bot_y + bot_iso_height + insulation_delta, + right_x - right_iso_width, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(right_x - right_iso_width, + bot_y + bot_iso_height + insulation_delta, + right_x - right_iso_width, + top_y - top_iso_height - insulation_delta) + # bot insulation + femm.ei_drawline(left_x, + bot_y + bot_iso_height, + right_x, + bot_y + bot_iso_height) + femm.ei_drawline(right_x, + bot_y + bot_iso_height, + right_x, + bot_y) + femm.ei_drawline(right_x, + bot_y, + left_x, + bot_y) + femm.ei_drawline(left_x, + bot_y, + left_x, + bot_y + bot_iso_height) + else: + if self.insulation.max_aspect_ratio == 0: + # If no aspect ratio is set insulations will not be drawn + return + else: + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + + # Useful points for insulation creation + left_x = self.core.core_inner_diameter / 2 + insulation_delta + top_y = self.core.window_h / 2 - insulation_delta + right_x = self.core.r_inner - insulation_delta + bot_y = -self.core.window_h / 2 + insulation_delta + + # Useful lengths + left_iso_width = self.insulation.core_cond[2] - insulation_delta - insulation_delta + top_iso_height = self.insulation.core_cond[0] - insulation_delta - insulation_delta + right_iso_width = self.insulation.core_cond[3] - insulation_delta - insulation_delta + bot_iso_height = self.insulation.core_cond[1] - insulation_delta - insulation_delta + + # Left insulation + femm.ei_drawline(left_x, + top_y - top_iso_height - insulation_delta, + left_x + left_iso_width, + top_y - top_iso_height - insulation_delta) + femm.ei_drawline(left_x + left_iso_width, + top_y - top_iso_height - insulation_delta, + left_x + left_iso_width, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(left_x + left_iso_width, + bot_y + bot_iso_height + insulation_delta, + left_x, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(left_x, + bot_y + bot_iso_height + insulation_delta, + left_x, + top_y - top_iso_height - insulation_delta) + + # top insulation + femm.ei_drawline(left_x, + top_y, + right_x, + top_y) + femm.ei_drawline(right_x, + top_y, + right_x, + top_y - top_iso_height) + femm.ei_drawline(right_x, + top_y - top_iso_height, + left_x, + top_y - top_iso_height) + femm.ei_drawline(left_x, + top_y - top_iso_height, + left_x, + top_y) + + # right insulation + femm.ei_drawline(right_x - right_iso_width, + top_y - top_iso_height - insulation_delta, + right_x, + top_y - top_iso_height - insulation_delta) + femm.ei_drawline(right_x, + top_y - top_iso_height - insulation_delta, + right_x, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(right_x, + bot_y + bot_iso_height + insulation_delta, + right_x - right_iso_width, + bot_y + bot_iso_height + insulation_delta) + femm.ei_drawline(right_x - right_iso_width, + bot_y + bot_iso_height + insulation_delta, + right_x - right_iso_width, + top_y - top_iso_height - insulation_delta) + # bot insulation + femm.ei_drawline(left_x, + bot_y + bot_iso_height, + right_x, + bot_y + bot_iso_height) + femm.ei_drawline(right_x, + bot_y + bot_iso_height, + right_x, + bot_y) + femm.ei_drawline(right_x, + bot_y, + left_x, + bot_y) + femm.ei_drawline(left_x, + bot_y, + left_x, + bot_y + bot_iso_height) + + # Now select the segments to assign zero voltage for core + if ground_core: + boundary_segments = [ + # Segments for the core and winding, including the two horizontal air gap lines + (0, self.two_d_axi.p_air_gaps[0, 1], self.two_d_axi.p_air_gaps[1, 0], self.two_d_axi.p_air_gaps[1, 1]), + (self.two_d_axi.p_air_gaps[1, 0], self.two_d_axi.p_air_gaps[1, 1], self.two_d_axi.p_window[4, 0], self.two_d_axi.p_window[4, 1]), + (self.two_d_axi.p_window[4, 0], self.two_d_axi.p_window[4, 1], self.two_d_axi.p_window[5, 0], self.two_d_axi.p_window[5, 1]), + (self.two_d_axi.p_window[5, 0], self.two_d_axi.p_window[5, 1], self.two_d_axi.p_window[7, 0], self.two_d_axi.p_window[7, 1]), + (self.two_d_axi.p_window[7, 0], self.two_d_axi.p_window[7, 1], self.two_d_axi.p_window[6, 0], self.two_d_axi.p_window[6, 1]), + (self.two_d_axi.p_window[6, 0], self.two_d_axi.p_window[6, 1], self.two_d_axi.p_air_gaps[3, 0], self.two_d_axi.p_air_gaps[3, 1]), + (self.two_d_axi.p_air_gaps[3, 0], self.two_d_axi.p_air_gaps[3, 1], 0, self.two_d_axi.p_air_gaps[2, 1]), + ] + + for segment in boundary_segments: + x_start, y_start, x_end, y_end = segment + femm.ei_selectsegment((x_start + x_end) / 2, (y_start + y_end) / 2) + femm.ei_addboundprop('ZeroVoltage', 0, 0, 0, 0, 0) + femm.ei_setsegmentprop('ZeroVoltage', 0, 1, 0, 1, '') + femm.ei_clearselected() + + if ground_outer_boundary: + # Now ground the outer boundary lines + outer_boundary_segments = [ + # Left bottom vertical segment + # (0, self.two_d_axi.p_outer[0, 1], 0, self.two_d_axi.p_air_gaps[0, 1]), + # bottom horizontal segment + (self.two_d_axi.p_outer[1, 0], self.two_d_axi.p_outer[1, 1], 0, self.two_d_axi.p_outer[0, 1]), + # right vertical segment + (self.two_d_axi.p_outer[3, 0], self.two_d_axi.p_outer[3, 1], self.two_d_axi.p_outer[1, 0], self.two_d_axi.p_outer[1, 1]), + # top horizontal segment + (0, self.two_d_axi.p_outer[2, 1], self.two_d_axi.p_outer[3, 0], self.two_d_axi.p_outer[3, 1]), + # Left top vertical segment + # (0, self.two_d_axi.p_outer[0, 3], 0, self.two_d_axi.p_air_gaps[2, 2]) + ] + + for segment in outer_boundary_segments: + x_start, y_start, x_end, y_end = segment + femm.ei_selectsegment((x_start + x_end) / 2, (y_start + y_end) / 2) # Select the segment by its midpoint + femm.ei_addboundprop('Ground', 0, 0, 0, 0, 0) # Add boundary property for grounding + femm.ei_setsegmentprop('Ground', 0, 1, 0, 1, '') # Set segment property to apply grounding + femm.ei_clearselected() + + turn_index = 0 # Initialize the turn index for tracking + # Iterate through each winding + for winding_index, winding_voltages in enumerate(voltages): + # Calculate the number of turns for the current winding + num_turns = int(self.two_d_axi.p_conductor[winding_index].shape[0] / 5) + + if len(winding_voltages) != num_turns: + raise ValueError(f"Mismatch between the number of turns and voltages for winding {winding_index + 1}") + + # Iterate through each turn in the winding + for i in range(num_turns): + # Draw arcs for the conductor (turn) + femm.ei_drawarc(self.two_d_axi.p_conductor[winding_index][5 * i + 1][0], + self.two_d_axi.p_conductor[winding_index][5 * i + 1][1], + self.two_d_axi.p_conductor[winding_index][5 * i + 3][0], + self.two_d_axi.p_conductor[winding_index][5 * i + 3][1], 180, 2.5) + femm.ei_addarc(self.two_d_axi.p_conductor[winding_index][5 * i + 3][0], + self.two_d_axi.p_conductor[winding_index][5 * i + 3][1], + self.two_d_axi.p_conductor[winding_index][5 * i + 1][0], + self.two_d_axi.p_conductor[winding_index][5 * i + 1][1], 180, 2.5) + # Calculate the center and radius for selecting the conductor turn + x_center = self.two_d_axi.p_conductor[winding_index][5 * i][0] + y_center = self.two_d_axi.p_conductor[winding_index][5 * i][1] + radius = x_center - self.two_d_axi.p_conductor[winding_index][5 * i + 1][0] + # Select all entities within the circular region around the conductor center + femm.ei_selectcircle(x_center, y_center, radius, 4) + + conductor_name = f'Turn{turn_index + 1}' + femm.ei_setarcsegmentprop(2.5, '', 0, turn_index + 5, conductor_name) + femm.ei_clearselected() + + # Add a block label at the center of the conductor turn + femm.ei_addblocklabel(x_center, y_center) + + # Select the block label and apply conductor properties + femm.ei_selectlabel(x_center, y_center) + + # Assign conductor properties with a unique name and apply voltage + femm.ei_addconductorprop(conductor_name, winding_voltages[i], 0, 1) + + # Set block properties, using a unique group for each conductor + femm.ei_setblockprop('Copper', automesh_conductor, mesh_size_conductor, turn_index + 5) + + # Clear selection to ensure no overlap in label assignments + femm.ei_clearselected() + + # Increment the turn index for the next iteration + turn_index += 1 + + # Define an open boundary condition for electrostatics + if not ground_outer_boundary and not ground_core: + femm.ei_makeABC(5, 0.5, 0.0, 0.0, 1) + else: + femm.ei_makeABC() + # femm.ei_makeABC(5, 0.5, 0.0, 0.0, 1) + + # == Labels/Designations == + # Label for core + femm.ei_addblocklabel(self.two_d_axi.p_outer[3, 0] - 0.001, self.two_d_axi.p_outer[3, 1] - 0.001) + femm.ei_selectlabel(self.two_d_axi.p_outer[3, 0] - 0.001, self.two_d_axi.p_outer[3, 1] - 0.001) + femm.ei_setblockprop('CoreMaterial', automesh, mesh_size, 1) + femm.ei_clearselected() + + # Labels for air inside the core + if self.air_gaps.number == 0: + femm.ei_addblocklabel(self.two_d_axi.r_inner - 0.0001, 0) + femm.ei_selectlabel(self.two_d_axi.r_inner - 0.001, 1) + femm.ei_setblockprop('Air', automesh, mesh_size, 2) + femm.ei_clearselected() + else: + femm.ei_addblocklabel(0.001, 0) + femm.ei_selectlabel(0.001, 0) + femm.ei_setblockprop('Air', automesh, mesh_size, 2) + femm.ei_clearselected() + + # Add a block label for the outer region to define it as 'Air' + femm.ei_addblocklabel(self.two_d_axi.p_outer[3, 0] + 0.001, self.two_d_axi.p_outer[3, 1] + 0.001) + femm.ei_selectlabel(self.two_d_axi.p_outer[3, 0] + 0.001, self.two_d_axi.p_outer[3, 1] + 0.001) + femm.ei_setblockprop('Air', automesh, mesh_size, 3) # Assign 'Air' as the material + femm.ei_clearselected() + + # Add Block Labels for Insulation + if self.insulation.flag_insulation: + # Calculate positions for insulation block labels + left_insulation_label_x = left_x + left_iso_width / 2 + left_insulation_label_y = (top_y - top_iso_height - insulation_delta + bot_y + bot_iso_height + insulation_delta) / 2 + + top_insulation_label_x = (left_x + right_x) / 2 + top_insulation_label_y = top_y - top_iso_height / 2 + + right_insulation_label_x = right_x - right_iso_width / 2 + right_insulation_label_y = (top_y - top_iso_height - insulation_delta + bot_y + bot_iso_height + insulation_delta) / 2 + + bot_insulation_label_x = (left_x + right_x) / 2 + bot_insulation_label_y = bot_y + bot_iso_height / 2 + + # Add and set block properties for each insulation region + # Left insulation + femm.ei_addblocklabel(left_insulation_label_x, left_insulation_label_y) + femm.ei_selectlabel(left_insulation_label_x, left_insulation_label_y) + femm.ei_setblockprop('InsulationMaterial', automesh, mesh_size, 4) + femm.ei_clearselected() + + # Top insulation + femm.ei_addblocklabel(top_insulation_label_x, top_insulation_label_y) + femm.ei_selectlabel(top_insulation_label_x, top_insulation_label_y) + femm.ei_setblockprop('InsulationMaterial', automesh, mesh_size, 4) + femm.ei_clearselected() + + # Right insulation + femm.ei_addblocklabel(right_insulation_label_x, right_insulation_label_y) + femm.ei_selectlabel(right_insulation_label_x, right_insulation_label_y) + femm.ei_setblockprop('InsulationMaterial', automesh, mesh_size, 4) + femm.ei_clearselected() + + # Bottom insulation + femm.ei_addblocklabel(bot_insulation_label_x, bot_insulation_label_y) + femm.ei_selectlabel(bot_insulation_label_x, bot_insulation_label_y) + femm.ei_setblockprop('InsulationMaterial', automesh, mesh_size, 4) + femm.ei_clearselected() + + # Save, analyze, and load the solution + femm.ei_zoomnatural() + femm.ei_saveas(os.path.join(self.file_data.femm_folder_path, 'coil_electrostatic.FEE')) + femm.ei_analyze() + femm.ei_loadsolution() + + # Log results + self.write_femm_electrostatic_log(voltages) + # Convert the log JSON file to Excel + if save_to_excel_file: + json_file_path = self.file_data.femm_results_log_path + output_excel_path = os.path.splitext(json_file_path)[0] + ".xlsx" + ff.json_to_excel(json_file_path, output_excel_path) + # Compare .xlsx files between FEMMT and FEMM. + if compare_femmt_to_femm: + femmt_excel_path = os.path.splitext(self.file_data.electrostatic_results_log_path)[0] + ".xlsx" + femm_excel_path = os.path.splitext(self.file_data.femm_results_log_path)[0] + ".xlsx" + comparison_output_directory = os.path.dirname(self.file_data.electrostatic_results_log_path) + comparison_output_path = os.path.join(comparison_output_directory, "comparison_results.xlsx") + ff.compare_excel_files(femmt_excel_path, femm_excel_path, comparison_output_path) + logger.info(f"Comparison data for all sheets has been successfully written to {comparison_output_path}") + + def write_femm_electrostatic_log(self, voltages): + """ + Write log file for electrostatic simulation to compute capacitance based on stored energy. + + :param voltages: A list of voltages to be applied to each winding. + :type voltages: List of List + """ + if os.name == 'nt': + ff.install_pyfemm_if_missing() + if not self.femm_is_imported: + globals()["femm"] = __import__("femm") + self.femm_is_imported = True + else: + raise Exception('This command is only executable on Windows computers.') + + file = open(self.file_data.femm_results_log_path, 'w+', encoding='utf-8') + + log_dict = {"charges": {}, "energy": {}, "average_voltages": {}, "capacitances": {}} + + # Select air inside the core + femm.eo_groupselectblock(2) + # Get stored energy in the air inside the core (0 represents stored energy) + stored_energy_air = femm.eo_blockintegral(0) + log_dict["energy"]["stored_air"] = stored_energy_air[0] + femm.eo_clearblock() + + # Select the core + femm.eo_groupselectblock(1) + stored_energy_core = femm.eo_blockintegral(0) + log_dict["energy"]["stored_core"] = stored_energy_core[0] + femm.eo_clearblock() + + # select insulation + # if self.insulation.flag_insulation: + # femm.eo_groupselectblock(4) + # stored_energy_insulation = femm.eo_blockintegral(0) + # log_dict["energy"]["stored_bobbin"] = stored_energy_insulation[0] + # femm.eo_clearblock() + + # select the core + femm.eo_groupselectblock(1) + # Select air inside the core + femm.eo_groupselectblock(2) + stored_energy = femm.eo_blockintegral(0) + # select insulation + if self.insulation.flag_insulation: + femm.eo_groupselectblock(4) + + # Select all turn groups across all windings + for winding_index, _ in enumerate(voltages): + num_turns = int(self.two_d_axi.p_conductor[winding_index].shape[0] / 5) + base_turn_group = winding_index * num_turns + for turn_number in range(5, 5 + num_turns): + turn_group = base_turn_group + turn_number + femm.eo_groupselectblock(turn_group) + + stored_energy_component = femm.eo_blockintegral(0) + log_dict["energy"]["stored_component"] = stored_energy_component[0] + femm.eo_clearblock() + + # If stored_energy is a list, take the first element + if isinstance(stored_energy, list): + stored_energy = stored_energy[0] + + log_dict["energy"]["stored_component"] = stored_energy + + # === Calculate Average Voltage in Core === + # Define points within the core to sample voltage values + core_points = [ + # Bottom boundary points + (self.two_d_axi.p_window[4, 0], self.two_d_axi.p_window[4, 1]), + (self.two_d_axi.p_window[5, 0], self.two_d_axi.p_window[5, 1]), + # Top boundary points + (self.two_d_axi.p_window[6, 0], self.two_d_axi.p_window[6, 1]), + (self.two_d_axi.p_window[7, 0], self.two_d_axi.p_window[7, 1]), + # Left boundary points + (self.two_d_axi.p_air_gaps[0, 0], self.two_d_axi.p_air_gaps[0, 1]), + (self.two_d_axi.p_air_gaps[3, 0], self.two_d_axi.p_air_gaps[3, 1]), + # Right boundary points + (self.two_d_axi.p_air_gaps[1, 0], self.two_d_axi.p_air_gaps[1, 1]), + (self.two_d_axi.p_air_gaps[2, 0], self.two_d_axi.p_air_gaps[2, 1]), + # (self.two_d_axi.p_window[4, 0] + 0.001, self.two_d_axi.p_window[4, 1] + 0.001), + # (self.two_d_axi.p_window[6, 0] - 0.001, self.two_d_axi.p_window[6, 1] - 0.001) + (0, self.two_d_axi.p_outer[1, 1]), + (self.two_d_axi.p_outer[3, 0], self.two_d_axi.p_outer[1, 1]), + (self.two_d_axi.p_outer[3, 0], self.two_d_axi.p_outer[3, 1]), + (0, self.two_d_axi.p_outer[3, 1]) + ] + + total_voltage = 0 + num_points = len(core_points) + + # Sum the voltages at each sampled point + for (x, y) in core_points: + voltage = femm.eo_getv(x, y) # Directly get the voltage at the point + if voltage is not None: + total_voltage += voltage + + # Calculate average voltage in the core + average_voltage_core = total_voltage / num_points + log_dict["average_voltages"] = {} + log_dict["average_voltages"]["core"] = average_voltage_core + # Calculate capacitance between each pair of turns using the stored energy and voltage differences + capacitance_within = {} + + # Iterate through each winding + for w1_index, winding_voltages in enumerate(voltages): + winding_name = f"Winding_{w1_index + 1}" + capacitance_within[winding_name] = {} + + # Capacitance within the same winding + for i, voltage1 in enumerate(winding_voltages): + turn_name = f"Turn_{i + 1}" + capacitance_within[winding_name][turn_name] = {} + + for j, voltage2 in enumerate(winding_voltages): + if i != j: + voltage_difference = abs(voltage2 - voltage1) + + if voltage_difference == 0: + if stored_energy == 0: + capacitance_within[winding_name][turn_name][f"to_Turn_{j + 1}"] = float('NaN') + else: + capacitance_within[winding_name][turn_name][f"to_Turn_{j + 1}"] = float('Infinity') + else: + capacitance = 2 * stored_energy / (voltage_difference ** 2) + capacitance_within[winding_name][turn_name][f"to_Turn_{j + 1}"] = capacitance + + # # Capacitance between different windings + capacitance_between = {} + for w1_index in range(len(voltages)): + for w2_index in range(w1_index + 1, len(voltages)): + winding1_voltages = voltages[w1_index] + winding2_voltages = voltages[w2_index] + winding1_name = f"Winding_{w1_index + 1}" + winding2_name = f"Winding_{w2_index + 1}" + + if winding1_name not in capacitance_between: + capacitance_between[winding1_name] = {} + if winding2_name not in capacitance_between[winding1_name]: + capacitance_between[winding1_name][winding2_name] = {} + + # Calculate capacitance between every pair of turns from different windings + for i, voltage1 in enumerate(winding1_voltages): + turn1_name = f"Turn_{i + 1}" + if turn1_name not in capacitance_between[winding1_name][winding2_name]: + capacitance_between[winding1_name][winding2_name][turn1_name] = {} + + for j, voltage2 in enumerate(winding2_voltages): + turn2_name = f"Turn_{j + 1}" + voltage_difference = abs(voltage2 - voltage1) + + if voltage_difference == 0: + if stored_energy == 0: + capacitance_between[winding1_name][winding2_name][turn1_name][f"to_{turn2_name}"] = float('NaN') + else: + capacitance_between[winding1_name][winding2_name][turn1_name][f"to_{turn2_name}"] = float('Infinity') + else: + capacitance = 2 * stored_energy / (voltage_difference ** 2) + capacitance_between[winding1_name][winding2_name][turn1_name][f"to_{turn2_name}"] = capacitance + + # Add reciprocal capacitance for symmetry + if winding2_name not in capacitance_between: + capacitance_between[winding2_name] = {} + if winding1_name not in capacitance_between[winding2_name]: + capacitance_between[winding2_name][winding1_name] = {} + if turn2_name not in capacitance_between[winding2_name][winding1_name]: + capacitance_between[winding2_name][winding1_name][turn2_name] = {} + capacitance_between[winding2_name][winding1_name][turn2_name][f"to_{turn1_name}"] = capacitance + + # Capacitance between each turn and the core + capacitance_between_turns_core = {} + for winding_index, winding_voltages in enumerate(voltages): + winding_name = f"Winding_{winding_index + 1}" + capacitance_between_turns_core[winding_name] = {} + + for i, voltage in enumerate(winding_voltages): + turn_name = f"Turn_{i + 1}" + voltage_difference = abs(voltage - average_voltage_core) + if voltage_difference == 0: + capacitance_between_turns_core[winding_name][turn_name] = float('NaN') if stored_energy == 0 else float('Infinity') + else: + capacitance = 2 * stored_energy / (voltage_difference ** 2) + capacitance_between_turns_core[winding_name][turn_name] = capacitance + + log_dict["capacitances"]["within_winding"] = capacitance_within + log_dict["capacitances"]["between_windings"] = capacitance_between + log_dict["capacitances"]["between_turns_core"] = capacitance_between_turns_core + + # Write to file + json.dump(log_dict, file, indent=2, ensure_ascii=False) + file.close() + + logger.info(f"Electrostatic capacitance log saved to: {self.file_data.femm_results_log_path}") + @staticmethod def calculate_point_average(x1: float, y1: float, x2: float, y2: float) -> tuple[float, float]: """Calculate the middle point between two given points. diff --git a/femmt/data.py b/femmt/data.py index 76230bff..a0e2038a 100644 --- a/femmt/data.py +++ b/femmt/data.py @@ -92,6 +92,12 @@ def update_paths(self, working_directory: str, electro_magnetic_folder_path: str # Setup file paths self.e_m_results_log_path = os.path.join(self.results_folder_path, "log_electro_magnetic.json") + ### + # for electrostatic + self.electrostatic_results_log_path = os.path.join(self.results_folder_path, "log_electro_static.json") + self.capacitance_result_log_path = os.path.join(self.results_folder_path, "capacitance_result.json") + self.capacitance_matrix_path = os.path.join(self.results_folder_path, "log_capacitance_matrix.json") + ### self.coordinates_description_log_path = os.path.join(self.results_folder_path, "log_coordinates_description.json") self.reluctance_log_path = os.path.join(self.results_folder_path, "log_reluctance_and_inductance.json") self.material_log_path = os.path.join(self.results_folder_path, "log_material.json") diff --git a/femmt/drawing.py b/femmt/drawing.py index 8c431f5e..4d5cdf88 100644 --- a/femmt/drawing.py +++ b/femmt/drawing.py @@ -23,6 +23,7 @@ class TwoDaxiSymmetric: stray_path: StrayPath insulation: Insulation component_type: ComponentType + simulation_type: SimulationType mesh_data: MeshData number_of_windings: int verbosity: Verbosity @@ -39,10 +40,12 @@ class TwoDaxiSymmetric: def __init__(self, core: Core, mesh_data: MeshData, air_gaps: AirGaps, winding_windows: list[WindingWindow], stray_path: StrayPath, insulation: Insulation, component_type: ComponentType, number_of_windings: int, verbosity: Verbosity): + self.core = core self.mesh_data = mesh_data self.winding_windows = winding_windows self.air_gaps = air_gaps + self.simulation_type = SimulationType self.component_type = component_type self.stray_path = stray_path self.insulation = insulation @@ -60,11 +63,14 @@ def __init__(self, core: Core, mesh_data: MeshData, air_gaps: AirGaps, winding_w self.p_window_bot = np.zeros((4, 4)) # TODO: just for right side? make it the same as for single core geometry self.p_air_gaps = np.zeros((4 * air_gaps.number, 4)) self.p_conductor = [] + self.p_iso_conductor = [] self.p_iso_core = [] self.p_iso_pri_sec = [] + self.p_iso_layer = [] for i in range(number_of_windings): self.p_conductor.insert(i, []) + self.p_iso_conductor.insert(i, []) self.r_inner = core.r_inner self.r_outer = core.r_outer @@ -919,46 +925,67 @@ def draw_conductors(self, draw_top_down: bool = True): ConductorDistribution.VerticalDownward_HorizontalRightward, ConductorDistribution.HorizontalRightward_VerticalUpward, ConductorDistribution.HorizontalRightward_VerticalDownward] - + # define a delta insulation + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + thickness_of_the_insulation_layer = self.insulation.thickness_of_insulation # Set the starting position and step size based on initial conditions if vertical_first: if upward_movement: - start_y = bot_bound + winding.conductor_radius # Start from the bottom - step_y = winding.conductor_radius * 2 + self.insulation.cond_cond[num][num] + start_y = bot_bound + winding.conductor_radius + self.insulation.turn_ins[num] # Start from the bottom + step_y = winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] else: - start_y = top_bound - winding.conductor_radius # Start from the top - step_y = -(winding.conductor_radius * 2 + self.insulation.cond_cond[num][num]) + start_y = top_bound - winding.conductor_radius - self.insulation.turn_ins[num] # Start from the top + step_y = -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num]) if rightward_movement: - start_x = left_bound + winding.conductor_radius # Moving right after completing a column - step_x = winding.conductor_radius * 2 + self.insulation.cond_cond[num][num] + # Moving right after completing a column + start_x = left_bound + winding.conductor_radius + self.insulation.turn_ins[num] + step_x = winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer + # For insulation between layer + start_x_layer = left_bound + 2 * winding.conductor_radius + 2 * self.insulation.turn_ins[num] + step_x_layer = winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer else: - start_x = right_bound - winding.conductor_radius # Moving left after completing a column - step_x = -(winding.conductor_radius * 2 + self.insulation.cond_cond[num][num]) - # Determine if the first movement is horizontally (rightward or leftward) + # Moving left after completing a column + start_x = right_bound - winding.conductor_radius - self.insulation.turn_ins[num] + step_x = -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer) + # For insulation between layer + start_x_layer = right_bound - 2 * winding.conductor_radius - 2 * self.insulation.turn_ins[num] + step_x_layer = -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer) + # Determine if the first movement is horizontally (rightward or leftward) else: if rightward_movement: - start_x = left_bound + winding.conductor_radius # start from the left - step_x = winding.conductor_radius * 2 + self.insulation.cond_cond[num][num] + start_x = left_bound + winding.conductor_radius + self.insulation.turn_ins[num] # start from the left + step_x = winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] else: - start_x = right_bound - winding.conductor_radius # Start from the right - step_x = -(winding.conductor_radius * 2 + self.insulation.cond_cond[num][num]) + start_x = right_bound - winding.conductor_radius - self.insulation.turn_ins[num] # Start from the right + step_x = -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num]) if upward_movement: - start_y = bot_bound + winding.conductor_radius # Moving top after completing a raw - step_y = winding.conductor_radius * 2 + self.insulation.cond_cond[num][num] + start_y = bot_bound + winding.conductor_radius + self.insulation.turn_ins[num] # Moving top after completing a raw + step_y = winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer + # For insulation between layer + start_y_layer = bot_bound + 2 * winding.conductor_radius + 2 * self.insulation.turn_ins[num] + step_y_layer = winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer else: - start_y = top_bound - winding.conductor_radius # Moving bottom after completing a raw - step_y = -(winding.conductor_radius * 2 + self.insulation.cond_cond[num][num]) + start_y = top_bound - winding.conductor_radius - self.insulation.turn_ins[num] # Moving bottom after completing a raw + step_y = -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer) + # For insulation between layer + start_y_layer = top_bound - 2 * winding.conductor_radius - 2 * self.insulation.turn_ins[num] + step_y_layer = -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + thickness_of_the_insulation_layer) # Loop and place conductors accordingly x = start_x y = start_y + x_l = start_x_layer if vertical_first else None + y_l = start_y_layer if not vertical_first else None i = 0 + counter_layer = 0 # Vertically movement if vertical_first: while i < turns and left_bound + winding.conductor_radius <= x <= right_bound - winding.conductor_radius: while i < turns and bot_bound + winding.conductor_radius <= y <= top_bound - winding.conductor_radius: + y_last = y + # drawing conductor self.p_conductor[num].append( [x, y, 0, self.mesh_data.c_center_conductor[num]]) self.p_conductor[num].append( @@ -969,21 +996,72 @@ def draw_conductors(self, draw_top_down: bool = True): [x + winding.conductor_radius, y, 0, self.mesh_data.c_conductor[num]]) self.p_conductor[num].append( [x, y - winding.conductor_radius, 0, self.mesh_data.c_conductor[num]]) - y += step_y + + # if the number of layer is less than len(insulation.cond_air_cond), then count it as zero + if counter_layer > len(self.insulation.cond_cond[num]) - 1: + self.insulation.cond_cond[num].append(insulation_delta) + + if self.insulation.consistent_ins is True: + winding_insulation = self.insulation.cond_cond[num][num] + else: + winding_insulation = self.insulation.cond_cond[num][counter_layer] + # Increment y based on the cond-air-cond insulation and the movement type + if upward_movement: + if zigzag: + if counter_layer % 2 == 0: # even.. first layer(0) + y += step_y + winding_insulation + elif counter_layer % 2 == 1: # odd.. sec layer(1) + y += step_y - winding_insulation + else: + y += step_y + winding_insulation + else: # Downward movement + if zigzag: + if counter_layer % 2 == 0: # even.. first layer(0) + y += step_y - winding_insulation + elif counter_layer % 2 == 1: # odd.. sec layer(1) + y += step_y + winding_insulation + else: + y += step_y - winding_insulation + i += 1 + + # Insert insulation after each layer + mesh_to_conductor = min(self.mesh_data.c_conductor) + if rightward_movement: + layer_insulation_points = [ + [x_l + insulation_delta, top_bound, 0, mesh_to_conductor], + [x_l + thickness_of_the_insulation_layer - insulation_delta, top_bound, 0, mesh_to_conductor], + [x_l + thickness_of_the_insulation_layer - insulation_delta, bot_bound, 0, mesh_to_conductor], + [x_l + insulation_delta, bot_bound, 0, mesh_to_conductor] + ] + else: + layer_insulation_points = [ + [x_l - insulation_delta, top_bound, 0, mesh_to_conductor], + [x_l - thickness_of_the_insulation_layer + insulation_delta, top_bound, 0, mesh_to_conductor], + [x_l - thickness_of_the_insulation_layer + insulation_delta, bot_bound, 0, mesh_to_conductor], + [x_l - insulation_delta, bot_bound, 0, mesh_to_conductor] + ] + # Add the points to the layer insulation data structure + if self.insulation.draw_insulation_between_layers: + self.p_iso_layer.append(layer_insulation_points) if not zigzag: # Start the next column with the same starting point (consistent direction) y = start_y else: # Alternating between top and bottom for the Zigzag movement + # step_y *= -1 + # y += step_y + y = y_last # **no extra step** step_y *= -1 - y += step_y - # Moving one step horizontally (right or left) + # increment x x += step_x + x_l += step_x_layer + counter_layer += 1 # Horizontally movement else: while i < turns and bot_bound + winding.conductor_radius <= y <= top_bound - winding.conductor_radius: while i < turns and left_bound + winding.conductor_radius <= x <= right_bound - winding.conductor_radius: + x_last = x self.p_conductor[num].append( [x, y, 0, self.mesh_data.c_center_conductor[num]]) self.p_conductor[num].append( @@ -994,17 +1072,64 @@ def draw_conductors(self, draw_top_down: bool = True): [x + winding.conductor_radius, y, 0, self.mesh_data.c_conductor[num]]) self.p_conductor[num].append( [x, y - winding.conductor_radius, 0, self.mesh_data.c_conductor[num]]) - x += step_x + + # if the number of layer is less than len(insulation.cond_air_cond), then the air insulation as zero + if counter_layer > len(self.insulation.cond_cond[num]) - 1: + self.insulation.cond_cond[num].append(insulation_delta) + + if self.insulation.consistent_ins is True: + winding_insulation = self.insulation.cond_cond[num][num] + else: + winding_insulation = self.insulation.cond_cond[num][counter_layer] + # Increment x based on the cond-air-cond insulation and the movement type + if rightward_movement: + if zigzag: + if counter_layer % 2 == 0: + x += step_x + winding_insulation + elif counter_layer % 2 == 1: + x += step_x - winding_insulation + else: + x += step_x + winding_insulation + else: + if zigzag: + if counter_layer % 2 == 0: + x += step_x - winding_insulation + elif counter_layer % 2 == 1: + x += step_x + winding_insulation + else: + x += step_x - winding_insulation i += 1 + # Insert layer insulation after each layer + if upward_movement: + layer_insulation_points = [ + [left_bound, y_l + insulation_delta, 0, self.mesh_data.c_window], + [left_bound, y_l + thickness_of_the_insulation_layer - insulation_delta, 0, self.mesh_data.c_window], + [right_bound, y_l + thickness_of_the_insulation_layer - insulation_delta, 0, self.mesh_data.c_window], + [right_bound, y_l + insulation_delta, 0, self.mesh_data.c_window] + ] + else: + layer_insulation_points = [ + [left_bound, y_l - insulation_delta, 0, self.mesh_data.c_window], + [left_bound, y_l - thickness_of_the_insulation_layer + insulation_delta, 0, self.mesh_data.c_window], + [right_bound, y_l - thickness_of_the_insulation_layer + insulation_delta, 0, self.mesh_data.c_window], + [right_bound, y_l - insulation_delta, 0, self.mesh_data.c_window] + ] + # Add the points to the layer insulation data structure + if self.insulation.draw_insulation_between_layers: + self.p_iso_layer.append(layer_insulation_points) if not zigzag: # Start the next raw with the same starting point (consistent direction) x = start_x else: # Alternating between right and left for the Zigzag movement + # step_x *= -1 + # x += step_x + x = x_last # **no extra step** step_x *= -1 - x += step_x # Moving one step vertically (top or bottom) y += step_y + y_l += step_y_layer + counter_layer += 1 # Align to edges if alignment == Align.ToEdges: @@ -1046,52 +1171,177 @@ def draw_conductors(self, draw_top_down: bool = True): self.p_conductor[num][i][0] += adjustment_x elif conductor_arrangement == ConductorArrangement.Hexagonal: - y = bot_bound + winding.conductor_radius - x = left_bound + winding.conductor_radius - i = 0 - base_line = True - # Case n_conductors higher that "allowed" is missing - while x < right_bound - winding.conductor_radius \ - and i < turns: - while y < top_bound - winding.conductor_radius and \ - i < turns: - self.p_conductor[num].append([ - x, - y, - 0, - self.mesh_data.c_center_conductor[num]]) - self.p_conductor[num].append([ - x - winding.conductor_radius, - y, - 0, - self.mesh_data.c_conductor[num]]) - self.p_conductor[num].append([ - x, - y + winding.conductor_radius, - 0, - self.mesh_data.c_conductor[num]]) - self.p_conductor[num].append([ - x + winding.conductor_radius, - y, - 0, - self.mesh_data.c_conductor[num]]) - self.p_conductor[num].append([ - x, - y - winding.conductor_radius, - 0, - self.mesh_data.c_conductor[num]]) - i += 1 - y += winding.conductor_radius * 2 + self.insulation.cond_cond[num][num] # from bottom to top - x += 2 * np.cos(np.pi / 6) * (winding.conductor_radius + self.insulation.cond_cond[num][num] / 2) - # * np.sqrt(2 / 3 * np.pi / np.sqrt(3)) # one step from left to right - # depending on what line, hexa scheme starts shifted - # reset y to "new" bottom - base_line = (not base_line) - if base_line: - y = bot_bound + winding.conductor_radius - else: - y = bot_bound + 2 * winding.conductor_radius + self.insulation.cond_cond[num][num] / 2 + thickness_of_the_insulation_layer = self.insulation.thickness_of_insulation + if placing_strategy == ConductorDistribution.VerticalUpward_HorizontalRightward: + + y = bot_bound + winding.conductor_radius + self.insulation.turn_ins[num] + x = left_bound + winding.conductor_radius + self.insulation.turn_ins[num] + start_x_layer = left_bound + 2 * winding.conductor_radius + 2 * self.insulation.turn_ins[num] + step_x_layer = 2 * np.cos(np.pi / 6) * (winding.conductor_radius + self.insulation.turn_ins[num] / 2 + \ + thickness_of_the_insulation_layer) + mesh_to_conductor = min(self.mesh_data.c_conductor) + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + i = 0 + counter_layer = 0 + base_line = True + # Case n_conductors higher that "allowed" is missing + while x < right_bound - winding.conductor_radius \ + and i < turns: + while i < turns and bot_bound + winding.conductor_radius <= y <= top_bound - winding.conductor_radius: + self.p_conductor[num].append([ + x, + y, + 0, + self.mesh_data.c_center_conductor[num]]) + self.p_conductor[num].append([ + x - winding.conductor_radius, + y, + 0, + self.mesh_data.c_conductor[num]]) + self.p_conductor[num].append([ + x, + y + winding.conductor_radius, + 0, + self.mesh_data.c_conductor[num]]) + self.p_conductor[num].append([ + x + winding.conductor_radius, + y, + 0, + self.mesh_data.c_conductor[num]]) + self.p_conductor[num].append([ + x, + y - winding.conductor_radius, + 0, + self.mesh_data.c_conductor[num]]) + if counter_layer > len(self.insulation.cond_cond[num]) - 1: + self.insulation.cond_cond[num].append(insulation_delta) + i += 1 + + if self.insulation.consistent_ins is True: + winding_insulation = self.insulation.cond_cond[num][num] + else: + winding_insulation = self.insulation.cond_cond[num][counter_layer] + + if zigzag: + if counter_layer % 2 == 0: + y += winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + winding_insulation + elif counter_layer % 2 == 1: + y += -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + winding_insulation) + else: + y += winding.conductor_radius * 2 + 2 * self.insulation.cond_cond[num][num] + winding_insulation + # from bottom to top + layer_insulation_points = [ + [start_x_layer + insulation_delta, top_bound, 0, mesh_to_conductor], + [start_x_layer + thickness_of_the_insulation_layer - insulation_delta, top_bound, 0, mesh_to_conductor], + [start_x_layer + thickness_of_the_insulation_layer - insulation_delta, bot_bound, 0, mesh_to_conductor], + [start_x_layer + insulation_delta, bot_bound, 0, mesh_to_conductor] + ] + if self.insulation.draw_insulation_between_layers: + self.p_iso_layer.append(layer_insulation_points) + x += 2 * np.cos(np.pi / 6) * (winding.conductor_radius + self.insulation.turn_ins[num] / 2 + \ + thickness_of_the_insulation_layer) + start_x_layer += step_x_layer + counter_layer += 1 + # * np.sqrt(2 / 3 * np.pi / np.sqrt(3)) # one step from left to right + # depending on what line, hexa scheme starts shifted + # reset y to "new" bottom + base_line = not base_line + if not zigzag: + if base_line: + y = bot_bound + winding.conductor_radius + self.insulation.turn_ins[num] + else: + y = bot_bound + 2 * winding.conductor_radius + self.insulation.turn_ins[num] / 2 + elif zigzag: + if base_line: + y = bot_bound + 4 * winding.conductor_radius + self.insulation.turn_ins[num] + else: + y = top_bound - 3 * winding.conductor_radius - self.insulation.turn_ins[num] / 2 + + elif placing_strategy == ConductorDistribution.HorizontalRightward_VerticalUpward: + + y = bot_bound + winding.conductor_radius + self.insulation.turn_ins[num][num] + x = left_bound + winding.conductor_radius + self.insulation.turn_ins[num][num] + start_y_layer = bot_bound + 2 * winding.conductor_radius + 2 * self.insulation.turn_ins[num] + step_y_layer = 2 * np.cos(np.pi / 6) * (winding.conductor_radius + self.insulation.turn_ins[num] / 2 + \ + thickness_of_the_insulation_layer) + mesh_to_conductor = min(self.mesh_data.c_conductor) + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + i = 0 + counter_layer = 0 + base_line = True + + while y < top_bound - winding.conductor_radius and i < turns: + + while i < turns and left_bound + winding.conductor_radius <= x <= right_bound - winding.conductor_radius: + self.p_conductor[num].append([ + x, + y, + 0, + self.mesh_data.c_center_conductor[num]]) + self.p_conductor[num].append([ + x - winding.conductor_radius, + y, + 0, + self.mesh_data.c_conductor[num]]) + self.p_conductor[num].append([ + x, + y + winding.conductor_radius, + 0, + self.mesh_data.c_conductor[num]]) + self.p_conductor[num].append([ + x + winding.conductor_radius, + y, + 0, + self.mesh_data.c_conductor[num]]) + self.p_conductor[num].append([ + x, + y - winding.conductor_radius, + 0, + self.mesh_data.c_conductor[num]]) + if counter_layer > len(self.insulation.cond_cond[num]) - 1: + self.insulation.cond_cond[num].append(insulation_delta) + + i += 1 + + if self.insulation.consistent_ins is True: + winding_insulation = self.insulation.cond_cond[num][num] + else: + winding_insulation = self.insulation.cond_cond[num][counter_layer] + if zigzag: + if counter_layer % 2 == 0: + x += winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + winding_insulation + elif counter_layer % 2 == 1: + x += -(winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + winding_insulation) + else: + x += winding.conductor_radius * 2 + 2 * self.insulation.turn_ins[num] + winding_insulation + + # Insert layer insulation after each layer + layer_insulation_points = [ + [left_bound, start_y_layer + insulation_delta, 0, mesh_to_conductor], + [left_bound, start_y_layer + thickness_of_the_insulation_layer - insulation_delta, 0, mesh_to_conductor], + [right_bound, start_y_layer + thickness_of_the_insulation_layer - insulation_delta, 0, mesh_to_conductor], + [right_bound, start_y_layer + insulation_delta, 0, mesh_to_conductor] + ] + if self.insulation.draw_insulation_between_layers: + self.p_iso_layer.append(layer_insulation_points) + + y += 2 * np.cos(np.pi / 6) * (winding.conductor_radius + self.insulation.turn_ins[num] / 2 + \ + thickness_of_the_insulation_layer) + start_y_layer += step_y_layer + counter_layer += 1 + # Determine x starting point: shifted or not + base_line = not base_line + if not zigzag: + if base_line: + x = left_bound + winding.conductor_radius + self.insulation.turn_ins[num] + else: + x = left_bound + 2 * winding.conductor_radius + self.insulation.turn_ins[num] / 2 + elif zigzag: + if base_line: + x = left_bound + winding.conductor_radius + self.insulation.turn_ins[num] + else: + x = right_bound - 3 * winding.conductor_radius - self.insulation.turn_ins[num] / 2 elif conductor_arrangement == ConductorArrangement.SquareFullWidth: y = bot_bound + winding.conductor_radius x = left_bound + winding.conductor_radius @@ -1348,134 +1598,286 @@ def draw_insulations(self): # Insulation between winding and core # Since an aspect ratio is given the insulation delta is calculated using the length of the longest side of the triangle, # which is always smaller than c_window. - if self.insulation.max_aspect_ratio == 0: - # If no aspect ratio is set insulations will not be drawn - return + if not self.insulation.bobbin_dimensions: + + if self.insulation.max_aspect_ratio == 0: + # If no aspect ratio is set insulations will not be drawn + return + else: + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + + self.p_iso_core = [] # Order: Left, Top, Right, Bot + self.p_iso_pri_sec = [] + + # Useful points for insulation creation + left_x = self.core.core_inner_diameter / 2 + insulation_delta + top_y = window_h / 2 - insulation_delta + right_x = self.r_inner - insulation_delta + bot_y = -window_h / 2 + insulation_delta + # # Useful lengths + # left_iso_width = iso.core_cond[2] - insulation_delta- insulation_delta + # top_iso_height = iso.core_cond[0] - insulation_delta - insulation_delta + # right_iso_width = iso.core_cond[3] - insulation_delta - insulation_delta + # bot_iso_height = iso.core_cond[1] - insulation_delta - insulation_delta + + # Useful lengths + left_iso_width = iso.core_cond[2] - insulation_delta - insulation_delta + top_iso_height = iso.core_cond[0] - insulation_delta - insulation_delta + right_iso_width = iso.core_cond[3] - insulation_delta - insulation_delta + bot_iso_height = iso.core_cond[1] - insulation_delta - insulation_delta + + # Core to Pri insulation + iso_core_left = [ + [ + left_x, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_core + ], + [ + left_x + left_iso_width, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_winding + ], + [ + left_x + left_iso_width, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_winding + ], + [ + left_x, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_core + ] + ] + iso_core_top = [ + [ + left_x, + top_y, + 0, + mesh_density_to_core + ], + [ + right_x, + top_y, + 0, + mesh_density_to_core + ], + [ + right_x, + top_y - top_iso_height, + 0, + mesh_density_to_winding + ], + [ + left_x, + top_y - top_iso_height, + 0, + mesh_density_to_winding + ] + ] + iso_core_right = [ + [ + right_x - right_iso_width, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_winding + ], + [ + right_x, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_core + ], + [ + right_x, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_core + ], + [ + right_x - right_iso_width, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_winding + ] + ] + iso_core_bot = [ + [ + left_x, + bot_y + bot_iso_height, + 0, + mesh_density_to_winding + ], + [ + right_x, + bot_y + bot_iso_height, + 0, + mesh_density_to_winding + ], + [ + right_x, + bot_y, + 0, + mesh_density_to_core + ], + [ + left_x, + bot_y, + 0, + mesh_density_to_core + ] + ] + + self.p_iso_core = [iso_core_left, iso_core_top, iso_core_right, iso_core_bot] else: - insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio - - self.p_iso_core = [] # Order: Left, Top, Right, Bot - self.p_iso_pri_sec = [] - - # Useful points for insulation creation - left_x = self.core.core_inner_diameter / 2 + insulation_delta - top_y = window_h / 2 - insulation_delta - right_x = self.r_inner - insulation_delta - bot_y = -window_h / 2 + insulation_delta - - # Useful lengths - left_iso_width = iso.core_cond[2] - insulation_delta - insulation_delta - top_iso_height = iso.core_cond[0] - insulation_delta - insulation_delta - right_iso_width = iso.core_cond[3] - insulation_delta - insulation_delta - bot_iso_height = iso.core_cond[1] - insulation_delta - insulation_delta - - # Core to Pri insulation - iso_core_left = [ - [ - left_x, - top_y - top_iso_height - insulation_delta, - 0, - mesh_density_to_core - ], - [ - left_x + left_iso_width, - top_y - top_iso_height - insulation_delta, - 0, - mesh_density_to_winding - ], - [ - left_x + left_iso_width, - bot_y + bot_iso_height + insulation_delta, - 0, - mesh_density_to_winding - ], - [ - left_x, - bot_y + bot_iso_height + insulation_delta, - 0, - mesh_density_to_core + + if self.insulation.max_aspect_ratio == 0: + # If no aspect ratio is set insulations will not be drawn + return + else: + insulation_delta = self.mesh_data.c_window / self.insulation.max_aspect_ratio + + # Handle the insulation delta for electrostatic transformer + # top - bot + bobbin_h = self.insulation.bobbin_window_h + insulation_delta_top_bot = (window_h - bobbin_h) / 2 + # left + bobbin_inner_radius = self.insulation.bobbin_inner_diameter / 2 + core_inner_radius = self.core.core_inner_diameter / 2 + insulation_delta_left = bobbin_inner_radius - core_inner_radius + # insulation_delta_left = 3e-4 + + self.p_iso_core = [] # Order: Left, Top, Right, Bot + self.p_iso_pri_sec = [] + + # Useful points for insulation creation + left_x = self.core.core_inner_diameter / 2 + insulation_delta_left + top_y = window_h / 2 - insulation_delta_top_bot + right_x = self.r_inner - insulation_delta + bot_y = -window_h / 2 + insulation_delta_top_bot + # # Useful lengths + # left_iso_width = iso.core_cond[2] - insulation_delta- insulation_delta + # top_iso_height = iso.core_cond[0] - insulation_delta - insulation_delta + # right_iso_width = iso.core_cond[3] - insulation_delta - insulation_delta + # bot_iso_height = iso.core_cond[1] - insulation_delta - insulation_delta + + # Useful lengths + left_iso_width = iso.core_cond[2] + top_iso_height = iso.core_cond[0] - insulation_delta - insulation_delta + right_iso_width = iso.core_cond[3] - insulation_delta - insulation_delta + bot_iso_height = iso.core_cond[1] - insulation_delta - insulation_delta + + # Core to Pri insulation + iso_core_left = [ + [ + left_x, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_core + ], + [ + left_x + left_iso_width, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_winding + ], + [ + left_x + left_iso_width, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_winding + ], + [ + left_x, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_core + ] ] - ] - iso_core_top = [ - [ - left_x, - top_y, - 0, - mesh_density_to_core - ], - [ - right_x, - top_y, - 0, - mesh_density_to_core - ], - [ - right_x, - top_y - top_iso_height, - 0, - mesh_density_to_winding - ], - [ - left_x, - top_y - top_iso_height, - 0, - mesh_density_to_winding + iso_core_top = [ + [ + left_x, + top_y, + 0, + mesh_density_to_core + ], + [ + right_x, + top_y, + 0, + mesh_density_to_core + ], + [ + right_x, + top_y - top_iso_height, + 0, + mesh_density_to_winding + ], + [ + left_x, + top_y - top_iso_height, + 0, + mesh_density_to_winding + ] ] - ] - iso_core_right = [ - [ - right_x - right_iso_width, - top_y - top_iso_height - insulation_delta, - 0, - mesh_density_to_winding - ], - [ - right_x, - top_y - top_iso_height - insulation_delta, - 0, - mesh_density_to_core - ], - [ - right_x, - bot_y + bot_iso_height + insulation_delta, - 0, - mesh_density_to_core - ], - [ - right_x - right_iso_width, - bot_y + bot_iso_height + insulation_delta, - 0, - mesh_density_to_winding + iso_core_right = [ + [ + right_x - right_iso_width, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_winding + ], + [ + right_x, + top_y - top_iso_height - insulation_delta, + 0, + mesh_density_to_core + ], + [ + right_x, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_core + ], + [ + right_x - right_iso_width, + bot_y + bot_iso_height + insulation_delta, + 0, + mesh_density_to_winding + ] ] - ] - iso_core_bot = [ - [ - left_x, - bot_y + bot_iso_height, - 0, - mesh_density_to_winding - ], - [ - right_x, - bot_y + bot_iso_height, - 0, - mesh_density_to_winding - ], - [ - right_x, - bot_y, - 0, - mesh_density_to_core - ], - [ - left_x, - bot_y, - 0, - mesh_density_to_core + iso_core_bot = [ + [ + left_x, + bot_y + bot_iso_height, + 0, + mesh_density_to_winding + ], + [ + right_x, + bot_y + bot_iso_height, + 0, + mesh_density_to_winding + ], + [ + right_x, + bot_y, + 0, + mesh_density_to_core + ], + [ + left_x, + bot_y, + 0, + mesh_density_to_core + ] ] - ] - self.p_iso_core = [iso_core_left, iso_core_top, iso_core_right, iso_core_bot] + self.p_iso_core = [iso_core_left, iso_core_top, iso_core_right, iso_core_bot] """ Currently there are only core to vww insulations @@ -1546,6 +1948,47 @@ def draw_insulations(self): print("No insulations for winding type {vww.winding_type.name}") """ + def draw_insulation_conductor(self): + """Draw insulation around each conductor turn.""" + if not self.insulation.add_turn_insulations: + return + + for winding_window in self.winding_windows: + for virtual_winding_window in winding_window.virtual_winding_windows: + winding = virtual_winding_window.windings[0] + conductor_type = winding.conductor_type + if conductor_type == ConductorType.RoundSolid or conductor_type == ConductorType.RoundLitz: + for winding_number, conductors in enumerate(self.p_conductor): + # Ensure conductors are treated as a numpy array + if not isinstance(conductors, np.ndarray): + conductors = np.asarray(conductors) + + insulation_thickness = self.insulation.turn_ins[winding_number] # Get insulation thickness + + # Prepare an empty array for storing insulation points + insulation_points = [] + + # Iterate through conductor turns, each defined by 5 points + for i in range(0, conductors.shape[0], 5): + center_x, center_y = conductors[i, :2] # Get conductor center point + conductor_radius = conductors[i + 3, 0] - center_x # Calculate current conductor radius + insulation_radius = conductor_radius + insulation_thickness # Add insulation thickness + + # Define 5 points for the insulation and add them to the list + insulation_points.extend([ + [center_x, center_y, 0, self.mesh_data.c_conductor[winding_number]], # Center + [center_x - insulation_radius, center_y, 0, self.mesh_data.c_conductor[winding_number]], # Left + [center_x, center_y + insulation_radius, 0, self.mesh_data.c_conductor[winding_number]], # Top + [center_x + insulation_radius, center_y, 0, self.mesh_data.c_conductor[winding_number]], # Right + [center_x, center_y - insulation_radius, 0, self.mesh_data.c_conductor[winding_number]] # Bottom + ]) + + # Convert collected points to a NumPy array and store it + self.p_iso_conductor[winding_number] = np.array(insulation_points) + # TODO Add logic for rectangularsolid. + + logger.info("Insulation around conductors drawn successfully.") + def draw_model(self): """Draw the full component. @@ -1563,7 +2006,8 @@ def draw_model(self): if self.insulation.flag_insulation: # check flag before drawing insulations self.draw_insulations() - + if self.insulation.add_turn_insulations is True: + self.draw_insulation_conductor() # TODO: Region # if self.core.core_type == CoreType.Single: # self.draw_region_single() diff --git a/femmt/dtos.py b/femmt/dtos.py index 807d473d..66aa7b77 100644 --- a/femmt/dtos.py +++ b/femmt/dtos.py @@ -22,6 +22,14 @@ class StackedCoreDimensions: window_h_top: float window_h_bot: float +@dataclass +class BobbinDimensions: + """Defines the dimensions of a default core.""" + + bobbin_inner_diameter: float + bobbin_window_w: float + bobbin_window_h: float + bobbin_h: float @dataclass class ConductorRow: diff --git a/femmt/electro_magnetic/fields_electrostatic.pro b/femmt/electro_magnetic/fields_electrostatic.pro new file mode 100644 index 00000000..d8638c15 --- /dev/null +++ b/femmt/electro_magnetic/fields_electrostatic.pro @@ -0,0 +1,25 @@ +PostOperation Map_local UsingPost EleSta { + + // Potentials for the entire domain + ExtGmsh = ".pos"; + Print[ u0, OnElementsOf Region[{Domain}], Name "Potential / V", File StrCat[DirResFields, "Potential", ExtGmsh], LastTimeStepOnly ] ; + Print[ u0, OnElementsOf Region[{Core}], Name "Average Potential on Core / V", File StrCat[DirResFields, "Voltage_Core_Map", ExtGmsh], LastTimeStepOnly ]; + + // Electric Field vector in the entire domain + Print[ e, OnElementsOf Region[{Domain}], Name "Electric Field", File StrCat[DirResFields, "Efield", ExtGmsh], LastTimeStepOnly ] ; + //Print[ Welocal, OnElementsOf Region[{Domain}], Name "Stored Energy", File StrCat[DirResFields, "We", ExtGmsh], LastTimeStepOnly ] ; + Print[ MagE, OnElementsOf Region[{Domain}], Name "Magnitude Electric Field / V/m", File StrCat[DirResFields, "MagE", ExtGmsh], LastTimeStepOnly ] ; + + // Displacement Field vector in the entire domain + Print[ d, OnElementsOf Region[{Domain}], Name "Electric Field Density", File StrCat[DirResFields, "Dfield", ExtGmsh], LastTimeStepOnly ] ; + Print[ MagD, OnElementsOf Region[{Domain}], Name "Magnitude Electric Field Density / C/m^2", File StrCat[DirResFields, "MagD", ExtGmsh], LastTimeStepOnly ] ; + + + // Settings for visualization output (optional) + Echo[ Str["View[PostProcessing.NbViews-1].Light=0; + View[PostProcessing.NbViews-1].LineWidth = 2; + View[PostProcessing.NbViews-1].RangeType=3; + View[PostProcessing.NbViews-1].IntervalsType=1; + View[PostProcessing.NbViews-1].NbIso = 25;"], + File OptionPos]; +} \ No newline at end of file diff --git a/femmt/electro_magnetic/ind_axi_python_controlled_electrostatic.pro b/femmt/electro_magnetic/ind_axi_python_controlled_electrostatic.pro new file mode 100644 index 00000000..843ab150 --- /dev/null +++ b/femmt/electro_magnetic/ind_axi_python_controlled_electrostatic.pro @@ -0,0 +1,539 @@ +// ---------------------- +Include "Parameter.pro"; +Include "postquantities.pro"; +// All Variables - remove or create in python +// ---------------------- +Nb_max_iter = 20; +relaxation_factor = 1.; +stop_criterion = 1e-8; + +// ---------------------- +// half inductor with axisymmetry +// 1 means full cylinder +SymFactor = 1. ; +CoefGeo = 2*Pi*SymFactor ; // axisymmetry +/* symmetry factor */ +n_windings = Number_of_Windings; // Number of windings + +// ---------------------- +// Physical numbers +// ---------------------- +OUTBND = 111111; +BOUND_LEFT = 111112; +AIR = 110000; +AIR_EXT = 110001; +AIR_COND = 1000000; +Bobbin_Insulation = 110010; +Layer_Insulation = 110050; +//Cond_Insulation = 2000000; +CORE_PN = 120000; +ExtGmsh = ".pos"; +po = "Output 2D/"; + + +//physical numbers of conductors in n transformer +For n In {1:n_windings} + iCOND~{n} = 130000 + 1000*(n-1); + istrandedCOND~{n} = 150000 + 1000*(n-1); +EndFor + +//physical numbers of conductor insulation in n winding +For n In {1:n_windings} + Cond_Insulation~{n} = 2000000 + 1000*(n-1); +EndFor + +// ---------------------- +// Groups (Physical Domains Setup for Electrostatics) +// ---------------------- +Group { + + Air = Region[{AIR, AIR_EXT}]; + // Insulation = Region[{Insulation}]; + + // Core + Core = Region[{}]; + // Core Domain + For n In {1:nCoreParts} + CorePart~{n} = Region[{(CORE_PN+n-1)}]; + Core += Region[{(CORE_PN+n-1)}]; + EndFor + // cond_insulation domain + /*ConductorInsulation = Region[{}]; + For n In {1:n_windings} + CondIsoPart~{n} = Region[{(Cond_Insulation+1000*(n-1))}]; + ConductorInsulation += Region[{CondIsoPart~{n}}]; + EndFor*/ + + + // Winding Domains Setup + For n In {1:n_windings} // Define regions for each winding + Winding~{n} = Region[{}]; // Initialize a region for the winding + EndFor + // Loop to Define Regions for Each Turn within Windings + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Loop over each turn in this winding to create a region for turns and add it to the winding + For turn_number In {1:nbturns~{winding_number}} + Turn~{winding_number}~{turn_number} = Region[{(iCOND~{winding_number} + turn_number - 1)}]; // Define a region for each turn + Winding~{winding_number} += Region[{(iCOND~{winding_number} + turn_number - 1)}]; // Add each turn to the respective winding region + Air += Region[{(AIR_COND + iCOND~{winding_number} + turn_number - 1)}]; // Include air surrounding each turn in the overall air domain + EndFor + EndFor + + // Domain Definitions + // Non-Conducting Domain (Air and Insulation) + DomainCC = Region[{Air}]; + DomainCC += Region[{Bobbin_Insulation}]; + DomainCC += Region[{Layer_Insulation}]; + // DomainCC += Region[{ConductorInsulation}]; + For n In {1:n_windings} + DomainCC += Region[{Cond_Insulation~{n}}]; // Initialize a region for the winding + EndFor + //DomainCC += Region[{Cond_Insulation}]; + + // Add the Core region to the appropriate domain region + /*If (Flag_ground_OutBoundary) + If (Flag_ground_core || Flag_excite_core) + // If both the core and the outer boundary are grounded, add the core to DomainC + DomainC += Region[{Core}]; + Else + // If the outer boundary is grounded but not the core, the core should be floating + DomainCC += Region[{Core}]; + EndIf + Else + // If the outer boundary is NOT grounded + If (Flag_ground_core || Flag_excite_core) + DomainC += Region[{Core}]; + Else + DomainCC += Region[{Core}]; + EndIf + EndIf*/ + + // Add the Core region to the appropriate domain region + If (Flag_ground_OutBoundary) + If (Flag_excite_core) + // If both the core and the outer boundary are grounded, add the core to DomainC + DomainC += Region[{Core}]; + Else + // If the outer boundary is grounded but not the core, the core should be floating + DomainCC += Region[{Core}]; + EndIf + Else + // If the outer boundary is NOT grounded + If (Flag_excite_core) + DomainC += Region[{Core}]; + Else + DomainCC += Region[{Core}]; + EndIf + EndIf + + // Boundary Conditions (Outer boundary to define potential) + // Neumann boundary conditions should be applied at the left du its symmetry + // Neumann boundary conditions specify the derivative of the function ( here is the derivative of the voltage(electric flux)) normal to the boundary. + // This means that there is no electric flux across a boundary + // This implies insulation or symmetry (no flow of electric charge across the boundary). + // Note that Since there are no non-homogeneous Neumann conditions in our code, Sur_Neu_Ele can be defined as empty. + // Otherwise, surface with imposed non-homogeneous Neumann boundary conditions (on n.d = -n . (epsilon grad v)), so nd[] function should be defined in our + // code as (epsilon[] * Norm[{d u0}]), look to the solver. + // non-homogeneous indicates that the derivative of the voltage (electric flux) is not equal to zero + // But as we do not have non-homogeneous boundary conditions, this will not affect our results here. + Sur_Neu_Ele = Region[{BOUND_LEFT}]; + // Dirichlet boundary conditions specify the value of the function (here is the voltage) directly on the boundary. + // For instance, setting a boundary to be at 0 volts (ground). + // OUTBND does not include the BOUND_TOP_LEFT and BOUND_BOT_LEFT. + OuterBoundary = Region[{OUTBND}]; + + // Add Winding Regions to the Core Domain + For n In {1:n_windings} + DomainC += Region[{Winding~{n}}]; + EndFor + + // Initialize the Main Domain + For n In {1:n_windings} + DomainCond~{n} += Region[{Winding~{n}}] ; + EndFor + + // Define the Conductor Domain for Each Winding (Turn-wise Capacitance Calculation) + For winding_number In {1:n_windings} + DomainCond~{winding_number} = Region[{Winding~{winding_number}}]; // Assign each winding to a conductor domain + // Define the Conductor Domain for Each Turn within the Winding + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; // Calculate the number of turns in each winding + For turn_number In {1:nbturns~{winding_number}} + DomainCond~{winding_number}~{turn_number} = Region[{Turn~{winding_number}~{turn_number}}]; // Assign each turn to a conductor domain + EndFor + EndFor + + Domain = Region[{DomainC, DomainCC}]; + // Dummy Region for post-processing with functions + DomainDummy = Region[12345]; +} +// Excitation +// ---------------------- +Function { + + // Define relative permittivity (epsilon) for all regions + AreaCell[#{Air, Bobbin_Insulation, Core}] = 1.; + For n In {1:n_windings} + AreaCell[#{Winding~{n}}] = 1.; + EndFor + // Core area: needed for finding the avg voltage in the core + SurfCore[] = SurfaceArea[]{CORE_PN} ; + // Materials + er_air = 1; + er_core = 100000; + //er_cond_insulation = 3; + For n In {1:n_windings} + epsilon[#{Cond_Insulation~{n}}] = e0 * er_turns_insulation~{n}; + EndFor + epsilon[#{Air}] = e0 * er_air; + epsilon[#{Core}] = e0 * er_core; + epsilon[#{Bobbin_Insulation}] = e0 * er_bobbin; + epsilon[#{Layer_Insulation}] = e0 * er_layer_insulation; + //epsilon[#{Cond_Insulation}] = e0 * er_cond_insulation; + // The winding permittivity is set to 1, but it does not play any role in the simulation. + For winding_number In {1:n_windings} + er_winding~{winding_number} = 1; + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + For turn_number In {1:nbturns~{winding_number}} + er_turn~{winding_number}~{turn_number} = er_winding~{winding_number}; + epsilon[#{Turn~{winding_number}~{turn_number}}] = e0 * er_turn~{winding_number}~{turn_number}; + EndFor + EndFor + + If (Flag_voltage) + // Assign a voltage to each turn of each turn + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + For turn_number In {1:nbturns~{winding_number}} + //Val_Potential_Turn~{winding_number}~{turn_number} = Voltage~{winding_number} * (turn_number / nbturns~{winding_number}); + Val_Potential_Turn~{winding_number}~{turn_number} = Voltage~{winding_number}~{turn_number}; + EndFor + EndFor + EndIf + + If (Flag_charge) + // Assign a charge to each turn of each turn + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + For turn_number In {1:nbturns~{winding_number}} + Cha_Potential_Turn~{winding_number}~{turn_number} = Charge~{winding_number}~{turn_number}; + EndFor + EndFor + EndIf +} + +// Definition of Constraints for Electrostatic Analysis +// ---------------------- +Constraint { + + { Name Electrostatic_Potential ; Type Assign; + Case { + If (Flag_voltage) + // Assigning a fixed potential to each turn within each winding + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + For turn_number In {1:nbturns~{winding_number}} + { Region Turn~{winding_number}~{turn_number} ; Type Assign ; Value Val_Potential_Turn~{winding_number}~{turn_number}; } + EndFor + EndFor + EndIf + + /*If(Flag_ground_core) + { Region Core ; Type Assign ; Value 0; } + ElseIf(Flag_excite_core) + { Region Core ; Type Assign ; Value v_core; } + EndIf*/ + If(Flag_excite_core) + { Region Core ; Type Assign ; Value v_core; } + EndIf + } + } + { Name SetArmatureCharge; Type Assign; + Case { + If (Flag_charge) + // Assigning a fixed charge to each turn within each winding + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + For turn_number In {1:nbturns~{winding_number}} + { Region Turn~{winding_number}~{turn_number} ; Type Assign ; Value Cha_Potential_Turn~{winding_number}~{turn_number}; } + EndFor + EndFor + If(Flag_excite_core) + { Region Core ; Type Assign ; Value v_core; } + EndIf + EndIf + + } + } + { Name Dirichlet_Ele; Type Assign; + Case { + If(Flag_ground_OutBoundary) + { Region OuterBoundary; Type Assign ; Value 0; } + EndIf + } + } + +} + +// ---------------------- +// Include solver file(s) +// ---------------------- +Include "solver_electrostatic.pro" +// Resolution +Resolution { + { Name EleSta_v; + System { + { Name Sys_Ele; NameOfFormulation Electrostatic_Potential; } + } + Operation { + CreateDir[DirResValsCore]; + CreateDir[DirResValsCapacitance]; + For n In {1:n_windings} + CreateDir[DirResValsWinding~{n}]; + CreateDir[DirResValsCharge~{n}]; + CreateDir[DirResValsVoltage~{n}]; + CreateDir[DirResValsCapacitanceFromQV~{n}]; + nbturns~{n} = NbrCond~{n} / SymFactor; + For turn In {1:nbturns~{n}} + CreateDir[DirResValsTurn~{turn}]; + EndFor + + EndFor + Generate[Sys_Ele]; Solve[Sys_Ele]; SaveSolution[Sys_Ele]; + PostOperation[Map_local] ; + PostOperation[Get_global] ; + } + } +} + +// Post processing +PostProcessing { + { Name EleSta; NameOfFormulation Electrostatic_Potential; NameOfSystem Sys_Ele; + PostQuantity { + // Voltage + { Name u0; Value { + Term { [ {u0} ]; In Domain; Jacobian Vol; } + } + } + // Electric field + { Name e; Value { + Term { [ -{d u0} ]; In Domain; Jacobian Vol; } + } + } + // Electric field + { Name Welocal; Value { + Term { [ epsilon[] / 2. * SquNorm[{d u0}] ]; In Domain; Jacobian Vol; } + } + } + // electric field density + { Name d; Value { + Term { [ -epsilon[] * {d u0} ]; In Domain; Jacobian Vol; } + } + } + // Magnitude of Electric Field (|E|) + { Name MagE; Value { + Term { [ Norm[{d u0}] ]; In Domain; Jacobian Vol; } + } + } + + // Magnitude of Displacement Field (|D|) + { Name MagD; Value { + Term { [ Norm[epsilon[] * {d u0}] ]; In Domain; Jacobian Vol; } + } + } + + // Real and Imaginary Components of Electric Field + { Name Real_E; Value { + Term { [ Re[{d u0}] ]; In DomainCC; Jacobian Vol; } + } + } + + { Name Imag_E; Value { + Term { [ Im[{d u0}] ]; In DomainCC; Jacobian Vol; } + } + } + + // Real and Imaginary Components of Displacement Field + { Name Real_D; Value { + Term { [ Re[epsilon[] * {d u0}] ]; In DomainCC; Jacobian Vol; } + } + } + + { Name Imag_D; Value { + Term { [ Im[epsilon[] * {d u0}] ]; In DomainCC; Jacobian Vol; } + } + } + // Global voltages + { Name U; Value { + Term { [ {U} ]; In DomainC; } + } + } + // Global charges + { Name Q; Value { + Term { [ CoefGeo * {Q} ]; In DomainC; } + } + } + // Stored Energy + { Name energy; Value { + Integral { Type Global; + [ CoefGeo * epsilon[] / 2. * SquNorm[{d u0}] ]; + In Domain; Jacobian Vol; Integration II; + } + } + } + + { Name Charge; Value { + Integral { [ CoefGeo * epsilon[] * Norm[{d u0}] ]; + In Domain; Jacobian Vol; Integration II; } + } + } + // Average voltage of the core + { Name Avg_Voltage_Core; Value {Integral { [ {u0} / SurfCore[] ]; In Region[{Core}]; Jacobian Sur; Integration II; }}} + + // Voltages of every turn to print them in different files + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Print charge for each turn in the winding + For turn_number In {1:nbturns~{winding_number}} + { Name U~{winding_number}~{turn_number}; Value { + Term { [ {U} ]; In Turn~{winding_number}~{turn_number}; } + } + } + EndFor + EndFor + // Charges of every turn to print them in different files + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Print charge for each turn in the winding + For turn_number In {1:nbturns~{winding_number}} + { Name Q~{winding_number}~{turn_number}; Value { + Term { [ CoefGeo * {Q} ]; In Turn~{winding_number}~{turn_number}; } + } + } + EndFor + EndFor + + + // Capacitance Calculation Between Turns for Each Winding ( from charges) + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Loop through each turn to define its capacitance relative to other turns + For turn_number1 In {1:nbturns~{winding_number}} + { Name Capacitance_Turn_Core~{winding_number}~{turn_number1}; Value { + Term { Type Global; + [ $Charge_Core / ($U~{winding_number}~{turn_number1} - $Avg_Voltage_Core) ]; + In Turn~{winding_number}~{turn_number1}; } + } + } + + For turn_number2 In {1:nbturns~{winding_number}} + // Calculate capacitance for every pair of turns, where turn_number1 is the reference voltage turn + If (turn_number1 == turn_number2) + // Self-capacitance (Q / V, with V being the turn voltage) + { Name Capacitance_Turn_Self~{winding_number}~{turn_number1}; Value { + Term { Type Global; + [ $Q~{winding_number}~{turn_number1} / $U~{winding_number}~{turn_number1} ]; + In Turn~{winding_number}~{turn_number1}; } + } + } + Else + // Mutual capacitance between different turns (Q_turn2 / V_turn1) + { Name Capacitance_Turn~{winding_number}~{turn_number1}~{turn_number2}; Value { + Term { Type Global; + [ $Q~{winding_number}~{turn_number2} / $U~{winding_number}~{turn_number1} ]; + In Turn~{winding_number}~{turn_number1}; } + } + } + EndIf + EndFor + EndFor + EndFor + + // Capacitance Calculation Between Turns of Different Windings + For winding_number1 In {1:n_windings} + nbturns1~{winding_number1} = NbrCond~{winding_number1} / SymFactor; + + For winding_number2 In {1:n_windings} + If (winding_number1 != winding_number2) + nbturns2~{winding_number2} = NbrCond~{winding_number2} / SymFactor; + + For turn_number1 In {1:nbturns1~{winding_number1}} + For turn_number2 In {1:nbturns2~{winding_number2}} + // Mutual capacitance between different turns in different windings (Q_turn2 / V_turn1) + { Name Capacitance_Turn_Windings~{winding_number1}~{turn_number1}~{winding_number2}~{turn_number2}; Value { + Term { Type Global; + [ $Q~{winding_number2}~{turn_number2} / $U~{winding_number1}~{turn_number1} ]; + In Turn~{winding_number1}~{turn_number1}; } + } + } + EndFor + EndFor + EndIf + EndFor + EndFor + + // Calculate capacitance through the stored energy + If (Flag_voltage) + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Loop through each turn as the reference turn + For turn_number1 In {1:nbturns~{winding_number}} + // Loop through every other turn, including the reference turn itself + // Calculate the capacitance between the turn and the core + { Name Capacitance_Between_Turns_Core~{winding_number}~{turn_number1}; Value { + Term { Type Global; + [ 2 * $energy_Component / ((Val_Potential_Turn~{winding_number}~{turn_number1} - $Avg_Voltage_Core) * (Val_Potential_Turn~{winding_number}~{turn_number1} - $Avg_Voltage_Core)) ]; + In DomainCond~{winding_number}~{turn_number1}; } + } + } + For turn_number2 In {1:nbturns~{winding_number}} + // Calculate the voltage difference between the two turns + VoltageDifference_Pair~{winding_number}~{turn_number1}~{turn_number2} = Val_Potential_Turn~{winding_number}~{turn_number2} - Val_Potential_Turn~{winding_number}~{turn_number1}; + + // Calculate the capacitance between the two turns + /*{ Name Capacitance_Between_Turns~{winding_number}~{turn_number1}~{turn_number2}; Value { + Integral { [ CoefGeo * epsilon[] * SquNorm[{d u0}] / (VoltageDifference_Pair~{winding_number}~{turn_number1}~{turn_number2} * VoltageDifference_Pair~{winding_number}~{turn_number1}~{turn_number2}) ]; + In Domain; Jacobian Vol; Integration II; } + } + }*/ + // using $... will increase the computation of our simulation + { Name Capacitance_Between_Turns~{winding_number}~{turn_number1}~{turn_number2}; Value { + Term { Type Global; + [ 2 * $energy_Component / (VoltageDifference_Pair~{winding_number}~{turn_number1}~{turn_number2} * VoltageDifference_Pair~{winding_number}~{turn_number1}~{turn_number2}) ]; + In DomainCond~{winding_number}~{turn_number1}; } + } + } + + EndFor + // Calculate capacitance between this turn and turns in other windings + For other_winding_number In {1:n_windings} + If (other_winding_number != winding_number) + nbturns~{other_winding_number} = NbrCond~{other_winding_number} / SymFactor; + + // Loop through each turn in the other winding + For turn_number2 In {1:nbturns~{other_winding_number}} + // Calculate the voltage difference between the turn in the current winding and a turn in the other winding + VoltageDifference_Cross~{winding_number}~{turn_number1}~{other_winding_number}~{turn_number2} = + Val_Potential_Turn~{other_winding_number}~{turn_number2} - Val_Potential_Turn~{winding_number}~{turn_number1}; + { Name Capacitance_Cross~{winding_number}~{turn_number1}~{other_winding_number}~{turn_number2} ; + Value { Term { Type Global; + [ 2 * $energy_Component / (VoltageDifference_Cross~{winding_number}~{turn_number1}~{other_winding_number}~{turn_number2} * + VoltageDifference_Cross~{winding_number}~{turn_number1}~{other_winding_number}~{turn_number2}) ] ; In DomainCond~{winding_number}~{turn_number1}; } } } + EndFor + EndIf + EndFor + EndFor + EndFor + EndIf + + } // Quantity Section + } // Name EleSta_v +} // PostProcessing + + +Include "fields_electrostatic.pro"; +Include "values_electrostatic.pro"; diff --git a/femmt/electro_magnetic/solver_electrostatic.pro b/femmt/electro_magnetic/solver_electrostatic.pro new file mode 100644 index 00000000..d959e0ed --- /dev/null +++ b/femmt/electro_magnetic/solver_electrostatic.pro @@ -0,0 +1,93 @@ +// Template solver file for FEMMT framework for Electrostatic Analysis +// Most parts are based on GetDP tutorials +// ---------------------- +// ---------------------- +// Functions + +Jacobian { + { Name Vol ; Case { { Region All ; Jacobian VolAxiSqu ; } } } + { Name Sur ; Case { { Region All ; Jacobian SurAxi ; } } } +} + +Integration { + { Name II ; Case { + { Type Gauss ; Case { + { GeoElement Triangle ; NumberOfPoints 4 ; } + { GeoElement Quadrangle ; NumberOfPoints 4 ; } + { GeoElement Line ; NumberOfPoints 13 ; } + } } + } } +} + +// ---------------------- +// ---------------------- +// FunctionSpace for Electrostatics +FunctionSpace { + + // Electric Scalar Potential + { Name Hgrad_v_Ele ; Type Form0 ; + BasisFunction { +// { Name s0 ; NameOfCoef u0 ; Function BF_Node ; +// Support Region[{Domain}] ; Entity NodesOf [ All ] ; } + //For n In {1:n_windings} + { Name sn; NameOfCoef vn; Function BF_Node; + Support Region[{Domain}]; Entity NodesOf[ All, Not DomainC ]; } + //EndFor + //For n In {1:n_windings} + { Name s0; NameOfCoef u0; Function BF_GroupOfNodes; + Support Region[{Domain}]; Entity GroupsOfNodesOf[ DomainC ]; } + //EndFor + // Include additional basis functions for nodes within windings to show internal voltage + /*For n In {1:n_windings} + { Name sw; NameOfCoef vw; Function BF_Node; + Support Region[{Winding~{n}}]; Entity NodesOf [ Winding~{n} ]; } + EndFor*/ + } + GlobalQuantity { + { Name GlobalPotential; Type AliasOf ; NameOfCoef u0; } + { Name ArmatureCharge ; Type AssociatedWith; NameOfCoef u0; } + } + Constraint { + { NameOfCoef vn; EntityType NodesOf; NameOfConstraint Dirichlet_Ele; } + { NameOfCoef GlobalPotential ; EntityType GroupsOfNodesOf ; NameOfConstraint Electrostatic_Potential ; } + { NameOfCoef ArmatureCharge ; EntityType GroupsOfNodesOf ; NameOfConstraint SetArmatureCharge ; } + // Dirichlet boundary condition for nodes inside the winding to reflect excitation potential + /*For n In {1:n_windings} + { NameOfCoef vw; EntityType NodesOf; NameOfConstraint Electrostatic_Potential; } + EndFor*/ + + } + } +} +Formulation { + { Name Electrostatic_Potential ; Type FemEquation ; + Quantity { + { Name u0 ; Type Local ; NameOfSpace Hgrad_v_Ele ; } // Electric potential + { Name U ; Type Global ; NameOfSpace Hgrad_v_Ele [GlobalPotential]; } + { Name Q ; Type Global; NameOfSpace Hgrad_v_Ele [ArmatureCharge]; } + } + Equation { + // Poisson's Equation for Electrostatics: -div(epsilon * grad(u)) = 0 + /*Galerkin { [ epsilon[] * Dof{d u0}, {d u0} ] ; + In Domain ; Jacobian Vol ; Integration II ; }*/ + // Laplace's equation for electrostatics +// Galerkin { [ epsilon[{d u0}] * Dof{d u0}, {d u0} ] ; +// In DomainCC ; Jacobian Vol ; Integration II ; } + Integral { [ epsilon[] * Dof{d u0} , {d u0} ]; + In Domain; Jacobian Vol; Integration II; } + //Integral { [ epsilon[] * Norm[{d u0}] , {u0} ]; In Sur_Neu_Ele; Jacobian Sur; Integration II; } + //Integral { [ 0 , {u0} ]; In Sur_Neu_Ele; Jacobian Sur; Integration II; } +// For n In {1:n_windings} +// GlobalTerm { [ -Dof{Q} , {U} ]; In Winding~{n}; } +// EndFor + // to find charge on Domain C ( core + turns) + GlobalTerm { [ -Dof{Q} , {U} ]; In DomainC; } + + } + } +} + + + + + diff --git a/femmt/electro_magnetic/values_electrostatic.pro b/femmt/electro_magnetic/values_electrostatic.pro new file mode 100644 index 00000000..38588f2e --- /dev/null +++ b/femmt/electro_magnetic/values_electrostatic.pro @@ -0,0 +1,164 @@ +PostOperation Get_global UsingPost EleSta { + // energy stored in air + Print[ energy[Domain], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"Energy_Stored_Component.dat"], LastTimeStepOnly, StoreInVariable $energy_Component]; + Print[ energy[Air], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"Energy_Stored_Air.dat"], LastTimeStepOnly, StoreInVariable $energy_Air]; + Print[ energy[Core], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"Energy_Stored_Core.dat"], LastTimeStepOnly, StoreInVariable $energy_Core]; +// Print[ energy[Insulation_bobbin], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"Energy_Stored_bobbin.dat"], LastTimeStepOnly, StoreInVariable $energy_bobbin]; +// Print[ energy[Insulation_cond], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"Energy_Stored_Cond_insulation.dat"], LastTimeStepOnly, StoreInVariable $energy_cond_insulation]; + + // average voltage of the core + Print[ Avg_Voltage_Core[Core], OnGlobal, Format TimeTable, File > StrCat[DirResCirc,"Avg_Core_voltage.dat"], LastTimeStepOnly, StoreInVariable $Avg_Voltage_Core]; + + // Charges + Print[ Charge[Air], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"charge_Air.dat"], LastTimeStepOnly, StoreInVariable $Charge_Air]; + Print[ Charge[Core], OnGlobal, Format TimeTable, File > StrCat[DirResVals,"charge_Core.dat"], LastTimeStepOnly, StoreInVariable $Charge_Core]; + // voltage and charges on windings + For n In {1:n_windings} + Print[ U, OnRegion Winding~{n}, Format TimeTable, File > Sprintf[StrCat[DirResCirc,"U_%g.dat"], n] , LastTimeStepOnly]; + EndFor + For n In {1:n_windings} + Print[ Q, OnRegion Winding~{n}, Format TimeTable, File > Sprintf[StrCat[DirResCirc,"Q_%g.dat"], n] , LastTimeStepOnly]; + EndFor + Print[ Q, OnRegion Core, Format TimeTable, File > Sprintf[StrCat[DirResCirc,"Q_Core.dat"]] , LastTimeStepOnly]; + + // print charge and voltage for each turn in separate file (Just another way) + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Print charge for each turn in the winding + For turn_number In {1:nbturns~{winding_number}} + Print[ U, OnRegion Turn~{winding_number}~{turn_number}, Format TimeTable, + File > Sprintf[StrCat[DirResCirc, "U_%g_%g.dat"], winding_number, turn_number], LastTimeStepOnly]; + Print[ Q, OnRegion Turn~{winding_number}~{turn_number}, Format TimeTable, + File > Sprintf[StrCat[DirResCirc, "Q_%g_%g.dat"], winding_number, turn_number], LastTimeStepOnly]; + EndFor + EndFor + + // print voltages for each turn + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Print voltage for each turn in the winding + For turn_number In {1:nbturns~{winding_number}} + Print[ U~{winding_number}~{turn_number}, + OnRegion Turn~{winding_number}~{turn_number}, Format Table, File > Sprintf[StrCat[DirResValsVoltage~{winding_number}, + "voltage_%g_%g.dat"], winding_number, turn_number], LastTimeStepOnly, StoreInVariable $U~{winding_number}~{turn_number}]; + EndFor + EndFor + + // print charges for each turn + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Print charge for each turn in the winding + For turn_number In {1:nbturns~{winding_number}} + Print[ Q~{winding_number}~{turn_number}, + OnRegion Turn~{winding_number}~{turn_number}, Format Table, File > Sprintf[StrCat[DirResValsCharge~{winding_number}, + "Charge_%g_%g.dat"], winding_number, turn_number], LastTimeStepOnly, StoreInVariable $Q~{winding_number}~{turn_number}]; + EndFor + EndFor + + + // Calculate and print capacitance through the stored energy + If (Flag_voltage) + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Loop through each turn as the reference turn + For turn_number1 In {1:nbturns~{winding_number}} + // Loop through every other turn, including the reference turn itself + Print[ Capacitance_Between_Turns_Core~{winding_number}~{turn_number1}, + OnRegion DomainCond~{winding_number}~{turn_number1}, Format Table, File > Sprintf[StrCat[DirResValsTurn~{turn_number1}, + "C_%g_%g_Core.dat"], winding_number, turn_number1], LastTimeStepOnly]; + For turn_number2 In {1:nbturns~{winding_number}} + // Print the calculated capacitance between each pair of turns + /*Print[ Capacitance_Between_Turns~{winding_number}~{turn_number1}~{turn_number2}[Domain], + OnGlobal, Format TimeTable, + File > Sprintf[StrCat[DirResValsTurn~{turn_number1}, "C_%g_%g_%g.dat"], winding_number, turn_number1, turn_number2] ];*/ + // Using this for $... + Print[ Capacitance_Between_Turns~{winding_number}~{turn_number1}~{turn_number2}, + OnRegion DomainCond~{winding_number}~{turn_number1}, Format Table, File > Sprintf[StrCat[DirResValsTurn~{turn_number1}, + "C_%g_%g_%g.dat"], winding_number, turn_number1, turn_number2], LastTimeStepOnly]; + EndFor + // Print capacitance between this turn and turns in other windings + For other_winding_number In {1:n_windings} + If (other_winding_number != winding_number) + nbturns~{other_winding_number} = NbrCond~{other_winding_number} / SymFactor; + + // Loop through each turn in the other winding + For turn_number2 In {1:nbturns~{other_winding_number}} + // Print the calculated capacitance between each pair of turns in different windings + Print[ Capacitance_Cross~{winding_number}~{turn_number1}~{other_winding_number}~{turn_number2}, + OnRegion DomainCond~{winding_number}~{turn_number1}, Format Table, File > Sprintf[StrCat[DirResValsTurn~{turn_number1}, + "C_Cross_%g_%g_%g_%g.dat"], winding_number, turn_number1, other_winding_number, turn_number2], LastTimeStepOnly]; + EndFor + EndIf + EndFor + EndFor + EndFor + EndIf + + + + // Capacitances from QV relation on each turn of the same winding + If (Flag_voltage) + // Capacitance Calculation Between Turns for Each Winding (from charges) + For winding_number In {1:n_windings} + nbturns~{winding_number} = NbrCond~{winding_number} / SymFactor; + + // Loop through each turn to define its capacitance relative to other turns + For turn_number1 In {1:nbturns~{winding_number}} + // Print the self-capacitance value + Print[Capacitance_Turn_Core~{winding_number}~{turn_number1}, + OnRegion Turn~{winding_number}~{turn_number1}, + Format Table, + File > Sprintf[StrCat[DirResValsCapacitanceFromQV~{winding_number}, "C_%g_%g_Core.dat"], winding_number, turn_number1], + LastTimeStepOnly]; + For turn_number2 In {1:nbturns~{winding_number}} + // Calculate capacitance for every pair of turns, where turn_number1 is the reference voltage turn + If (turn_number1 == turn_number2) + // Print the self-capacitance value + Print[Capacitance_Turn_Self~{winding_number}~{turn_number1}, + OnRegion Turn~{winding_number}~{turn_number1}, + Format Table, + File > Sprintf[StrCat[DirResValsCapacitanceFromQV~{winding_number}, "C_%g_%g_%g.dat"], winding_number, turn_number1, turn_number2], + LastTimeStepOnly]; + Else + // Print the mutual capacitance value + Print[Capacitance_Turn~{winding_number}~{turn_number1}~{turn_number2}, + OnRegion Turn~{winding_number}~{turn_number1}, + Format Table, + File > Sprintf[StrCat[DirResValsCapacitanceFromQV~{winding_number}, "C_%g_%g_%g.dat"], winding_number, turn_number1, turn_number2], + LastTimeStepOnly]; + EndIf + EndFor + EndFor + EndFor + EndIf + + // Print Capacitance Between Turns of Different Windings + For winding_number1 In {1:n_windings} + nbturns1~{winding_number1} = NbrCond~{winding_number1} / SymFactor; + + For winding_number2 In {1:n_windings} + If (winding_number1 != winding_number2) + nbturns2~{winding_number2} = NbrCond~{winding_number2} / SymFactor; + + For turn_number1 In {1:nbturns1~{winding_number1}} + For turn_number2 In {1:nbturns2~{winding_number2}} + // Print the mutual capacitance value between different windings + Print[Capacitance_Turn_Windings~{winding_number1}~{turn_number1}~{winding_number2}~{turn_number2}, + OnRegion Turn~{winding_number1}~{turn_number1}, + Format Table, + File > Sprintf[StrCat[DirResValsCapacitanceFromQV~{winding_number1}, + "C_%g_%g_%g_%g.dat"], winding_number1, turn_number1, winding_number2, turn_number2], + LastTimeStepOnly]; + EndFor + EndFor + EndIf + EndFor + EndFor + + + +} \ No newline at end of file diff --git a/femmt/enumerations.py b/femmt/enumerations.py index 783ac18d..49782066 100644 --- a/femmt/enumerations.py +++ b/femmt/enumerations.py @@ -62,6 +62,7 @@ class SimulationType(IntEnum): FreqDomain = 1 TimeDomain = 2 + ElectroStatic = 3 class CoreType(IntEnum): """Sets the core type for the whole simulation. Needs to be given to the MagneticComponent on creation.""" @@ -69,7 +70,6 @@ class CoreType(IntEnum): Single = 1 # one axisymmetric core Stacked = 2 # one and a half cores - class AirGapMethod(IntEnum): """Sets the method how the air gap position (vertical) is set. diff --git a/femmt/examples/advanced_inductor_air_gap_sweep.py b/femmt/examples/advanced_inductor_air_gap_sweep.py index e74d3ec0..0050b003 100644 --- a/femmt/examples/advanced_inductor_air_gap_sweep.py +++ b/femmt/examples/advanced_inductor_air_gap_sweep.py @@ -64,7 +64,9 @@ def basic_example_sweep(onelab_folder: str | None = None, show_visual_outputs: b insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) diff --git a/femmt/examples/advanced_inductor_sweep.py b/femmt/examples/advanced_inductor_sweep.py index 21a0051d..fd40bf77 100644 --- a/femmt/examples/advanced_inductor_sweep.py +++ b/femmt/examples/advanced_inductor_sweep.py @@ -63,7 +63,9 @@ def advanced_example_inductor_sweep(onelab_folder: str = None, show_visual_outpu # 4. set insulations insulation = fmt.Insulation(flag_insulation=False) insulation.add_core_insulations(0.001, 0.014, 0.006, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_inductor.py b/femmt/examples/basic_inductor.py index 4e9e0acf..2e430c87 100644 --- a/femmt/examples/basic_inductor.py +++ b/femmt/examples/basic_inductor.py @@ -111,7 +111,7 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in inductor_frequency = 270000 - # 2. set core parameters + # 2.1a set core parameters core_db = fmt.core_database()["PQ 40/40"] core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=core_db["core_inner_diameter"], window_w=core_db["window_w"], @@ -138,10 +138,22 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in # air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.0002, 90) geo.set_air_gaps(air_gaps) - # 4. set insulations + # 4. set insulation + # it is preferred to assign the exact dimensions of the bobbin for running electrostatic simulations or obtaining the capacitance of the inductor component + # using the function below + # bobbin_db = fmt.bobbin_database()["PQ 40/40"] + # bobbin_dimensions = fmt.dtos.BobbinDimensions(bobbin_inner_diameter=bobbin_db["bobbin_inner_diameter"], + # bobbin_window_w=bobbin_db["bobbin_window_w"], + # bobbin_window_h=bobbin_db["bobbin_window_h"], + # bobbin_h=bobbin_db["bobbin_h"]) insulation = fmt.Insulation(flag_insulation=True) insulation.add_core_insulations(0.001, 0.001, 0.003, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005, 0.0005]], per_layer_of_turns=False) + # When "add_turn_insulation" is false, this function has no effect + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + # When "add_insulation_material" is false, the material will be air by default. For now this function makes difference when the winding scheme is square and + # litz wire. + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -165,8 +177,8 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in # 6.a. start simulation geo.single_simulation(freq=inductor_frequency, current=[4.5], plot_interpolation=False, show_fem_simulation_results=show_visual_outputs) - - # geo.femm_reference(freq=inductor_frequency, current=[4.5], sign=[1], non_visualize=0) + # geo.get_inductances(I0=2, op_frequency=20000, skin_mesh_factor=0.5) + # geo.femm_reference(freq=inductor_frequency, current=[4.5], sign=[1], non_visualize=0)# # 6.b. Excitation Sweep Example # Perform a sweep using more than one frequency @@ -179,6 +191,10 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in # 7. prepare and start thermal simulation example_thermal_simulation(show_visual_outputs, flag_insulation=True) + # Extract the capacitance of inductor component + geo.get_capacitance_of_inductor_component(show_fem_simulation_results=False) + # geo.get_inductor_stray_capacitance(show_visual_outputs=True) + if __name__ == "__main__": basic_example_inductor(show_visual_outputs=True) diff --git a/femmt/examples/basic_inductor_electrostatic.py b/femmt/examples/basic_inductor_electrostatic.py new file mode 100644 index 00000000..bb29a77c --- /dev/null +++ b/femmt/examples/basic_inductor_electrostatic.py @@ -0,0 +1,124 @@ +""" +Basic example to show how to simulate an inductor with electrostatic analysis. + +After starting the program, the geometry dimensions are displayed. Verify this geometry, close the window to continue the simulation. +This example simulates the electrostatic properties of an inductor, such as capacitance and electric fields. + +Once the geometry is verified, an electrostatic simulation will be run with voltages applied to the turns of the winding. +The simulation results will include electrostatic potential distributions, electric field maps, and capacitance data. +These results can help you understand how the electric field is distributed within the component and where high-field regions may occur. + +The simulation results will be visualized. In these visual outputs, you will be able to see the distribution of electrostatic potential in different turns of +the winding and the influence of the core and other materials in the geometry. +""" +import femmt as fmt +import os + + +def basic_example_inductor_electrostatic(onelab_folder: str = None, show_visual_outputs: bool = True, is_test: bool = True): + """ + Run the example code for the inductor. + + :param onelab_folder: onelab folder path + :type onelab_folder: str + :param show_visual_outputs: True to show visual outputs (simulation results) + :type show_visual_outputs: bool + :param is_test: True for pytest usage. Defaults to False. + :type is_test: bool + """ + example_results_folder = os.path.join(os.path.dirname(__file__), "example_results") + if not os.path.exists(example_results_folder): + os.mkdir(example_results_folder) + + # Working directory can be set arbitrarily + working_directory = os.path.join(example_results_folder, os.path.splitext(os.path.basename(__file__))[0]) + if not os.path.exists(working_directory): + os.mkdir(working_directory) + + # 1. chose simulation type + geo = fmt.MagneticComponent(simulation_type=fmt.SimulationType.ElectroStatic, component_type=fmt.ComponentType.Inductor, + working_directory=working_directory, is_gui=is_test) + + # This line is for automated pytest running on GitHub only. Please ignore this line! + if onelab_folder is not None: + geo.file_data.onelab_folder_path = onelab_folder + + inductor_frequency = 2700000 + + # 2. set core parameters + core_db = fmt.core_database()["PQ 40/40"] + core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=core_db["core_inner_diameter"], + window_w=core_db["window_w"], + window_h=core_db["window_h"], + core_h=core_db["core_h"]) + + core = fmt.Core(core_type=fmt.CoreType.Single, + core_dimensions=core_dimensions, + detailed_core_model=False, + material=fmt.Material.N49, temperature=45, frequency=inductor_frequency, + # permeability_datasource="manufacturer_datasheet", + permeability_datasource=fmt.MaterialDataSource.Measurement, + permeability_datatype=fmt.MeasurementDataType.ComplexPermeability, + permeability_measurement_setup=fmt.MeasurementSetup.LEA_LK, + permittivity_datasource=fmt.MaterialDataSource.Measurement, + permittivity_datatype=fmt.MeasurementDataType.ComplexPermittivity, + permittivity_measurement_setup=fmt.MeasurementSetup.LEA_LK, mdb_verbosity=fmt.Verbosity.Silent) + + geo.set_core(core) + # 3. set air gap parameters + air_gaps = fmt.AirGaps(fmt.AirGapMethod.Percent, core) + air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.001, 50) + # air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.0002, 90) + geo.set_air_gaps(air_gaps) + + # 4. set insulations + bobbin_db = fmt.bobbin_database()["PQ 40/40"] + bobbin_dimensions = fmt.dtos.BobbinDimensions(bobbin_inner_diameter=bobbin_db["bobbin_inner_diameter"], + bobbin_window_w=bobbin_db["bobbin_window_w"], + bobbin_window_h=bobbin_db["bobbin_window_h"], + bobbin_h=bobbin_db["bobbin_h"]) + insulation = fmt.Insulation(flag_insulation=True, bobbin_dimensions=bobbin_dimensions) + bobbin_material = fmt.insulation_materials_database()["core_insulation"]["bobbins"]["Thermoset"]["Phenolic"] + insulation.add_core_insulations(0.2e-3, 0.2e-3, 0.2e-3, 0.2e-3, dielectric_constant=bobbin_material["dielectric_constant"]) + # This is the insulation of the turn itself + turn_insulation_material = fmt.insulation_materials_database()["wire_insulation"]["plastic_insulation"]["Plenum Polyvinyl Chloride (Plenum PVC)"] + insulation.add_turn_insulation([0.2e-3], dielectric_constant=[turn_insulation_material["dielectric_constant"]], add_turn_insulations=True) + # This is an air between turns if needed + insulation.add_winding_insulations([[1e-3, 1e-3]], per_layer_of_turns=True) + # Kapton material is added between every layer of turns + layer_insulation = fmt.insulation_materials_database()["film_insulation"]["Kapton"] + insulation.add_insulation_between_layers(add_insulation_material=True, thickness=0.6e-3, dielectric_constant=layer_insulation["dielectric_constant"]) + geo.set_insulation(insulation) + + # 5. create winding window and virtual winding windows (vww) + winding_window = fmt.WindingWindow(core, insulation) + vww = winding_window.split_window(fmt.WindingWindowSplit.NoSplit) + + # 6. create conductor and set parameters: use solid wires + winding = fmt.Conductor(0, fmt.Conductivity.Copper, winding_material_temperature=45) + winding.set_solid_round_conductor(conductor_radius=1.1506e-3, conductor_arrangement=fmt.ConductorArrangement.Square) + winding.parallel = False # set True to make the windings parallel, currently only for solid conductors + # 7. add conductor to vww and add winding window to MagneticComponent + vww.set_winding(winding, 7, None, fmt.Align.ToEdges, placing_strategy=fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, + zigzag=True) + geo.set_winding_windows([winding_window]) + # 8. create the model + geo.create_model(freq=inductor_frequency, pre_visualize_geometry=show_visual_outputs, save_png=False, skin_mesh_factor=0.5) + # 8. run electrostatic simulation + num_turns_w1 = 7 + # Create a linear voltage distribution along winding 1 from V_A to V_B ( the first turn to the last turn) + V_A = 1 + V_B = 0 + voltages_winding_1 = [ + V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + for i in range(num_turns_w1) + ] + geo.electrostatic_simulation(voltage=[voltages_winding_1], ground_outer_boundary=True, core_voltage=0, + show_fem_simulation_results=show_visual_outputs, save_to_excel_file=False) + # Run simulation in FEMM + # geo.femm_reference_electrostatic(voltages=[voltages_winding_1], ground_core=True, ground_outer_boundary=True, + # non_visualize=0, save_to_excel_file=False, compare_femmt_to_femm=False, mesh_size_conductor=0.0) + + +if __name__ == "__main__": + basic_example_inductor_electrostatic(show_visual_outputs=True) diff --git a/femmt/examples/basic_inductor_excitation_sweep.py b/femmt/examples/basic_inductor_excitation_sweep.py index 27c463b4..22967363 100644 --- a/femmt/examples/basic_inductor_excitation_sweep.py +++ b/femmt/examples/basic_inductor_excitation_sweep.py @@ -79,7 +79,9 @@ def basic_example_inductor_excitation_sweep(onelab_folder: str = None, show_visu # 4. set insulations insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_inductor_foil_vertical_electrostatic.py b/femmt/examples/basic_inductor_foil_vertical_electrostatic.py new file mode 100644 index 00000000..3f00272b --- /dev/null +++ b/femmt/examples/basic_inductor_foil_vertical_electrostatic.py @@ -0,0 +1,97 @@ +""" +Basic example to show how to simulate an inductor with vertical foil winding. + +After starting the program, the geometry dimensions are displayed. Verify this geometry, close the window, to continue the simulation. +After a short time, B-Field and winding losses simulation results are shown. Winding losses are shown as a colormap. +In the core, the magnitude B-Field in Tesla is shown. With the gmsh window, one can move the picture in the 3D way (not recommended). +If you close this window, the thermal simulation will be continued, if programmed. If true, the thermal heat distribution will be displayed. +To continue with the next simulation (or end the program), you need to close this window. All results are written to the result +folder .../femmt/examples/example_results/simulation_file_name/results/log_electro_magnetic.json. and .../results_thermal.json. +""" +import femmt as fmt +import os + + +def basic_example_inductor_foil_vertical_electrostatic(onelab_folder: str = None, show_visual_outputs: bool = True, is_test: bool = False): + """ + Run the example code for the inductor with vertical foil winding. + + :param onelab_folder: onelab folder path + :type onelab_folder: str + :param show_visual_outputs: True to show visual outputs (simulation results) + :type show_visual_outputs: bool + :param is_test: True for pytest usage. Defaults to False. + :type is_test: bool + """ + example_results_folder = os.path.join(os.path.dirname(__file__), "example_results") + if not os.path.exists(example_results_folder): + os.mkdir(example_results_folder) + + # Example for a transformer with multiple virtual winding windows. + working_directory = os.path.join(example_results_folder, os.path.splitext(os.path.basename(__file__))[0]) + if not os.path.exists(working_directory): + os.mkdir(working_directory) + + # Choose wrap para type + wrap_para_type = fmt.WrapParaType.FixedThickness + + # Set is_gui = True so FEMMt won't ask for the onelab path if no config is found. + geo = fmt.MagneticComponent(simulation_type=fmt.SimulationType.ElectroStatic, component_type=fmt.ComponentType.Inductor, + working_directory=working_directory, is_gui=is_test) + + # This line is for automated pytest running on GitHub only. Please ignore this line! + if onelab_folder is not None: + geo.file_data.onelab_folder_path = onelab_folder + + core_db = fmt.core_database()["PQ 40/40"] + core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=core_db["core_inner_diameter"], + window_w=core_db["window_w"], + window_h=core_db["window_h"], + core_h=core_db["core_h"]) + + core = fmt.Core(core_type=fmt.CoreType.Single, core_dimensions=core_dimensions, + mu_r_abs=3100, phi_mu_deg=12, + sigma=0.6, permeability_datasource=fmt.MaterialDataSource.Custom, + permittivity_datasource=fmt.MaterialDataSource.Custom) + geo.set_core(core) + + air_gaps = fmt.AirGaps(fmt.AirGapMethod.Center, core) + air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.0005) + geo.set_air_gaps(air_gaps) + + # 4. set insulations + bobbin_db = fmt.bobbin_database()["PQ 40/40"] + bobbin_dimensions = fmt.dtos.BobbinDimensions(bobbin_inner_diameter=bobbin_db["bobbin_inner_diameter"], + bobbin_window_w=bobbin_db["bobbin_window_w"], + bobbin_window_h=bobbin_db["bobbin_window_h"], + bobbin_h=bobbin_db["bobbin_h"]) + insulation = fmt.Insulation(flag_insulation=True, bobbin_dimensions=bobbin_dimensions) + insulation.add_core_insulations(0.001, 0.001, 0.001, 0.001) + insulation.add_winding_insulations([[0.025e-3]]) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + geo.set_insulation(insulation) + + winding_window = fmt.WindingWindow(core, insulation) + vww = winding_window.split_window(fmt.WindingWindowSplit.NoSplit) + + winding = fmt.Conductor(0, fmt.Conductivity.Copper, winding_material_temperature=25) + winding.set_rectangular_conductor(thickness=1e-3) + + vww.set_winding(winding, 3, fmt.WindingScheme.FoilHorizontal, fmt.Align.ToEdges, wrap_para_type=wrap_para_type, + foil_horizontal_placing_strategy=fmt.FoilHorizontalDistribution.VerticalUpward) + geo.set_winding_windows([winding_window]) + + geo.create_model(freq=100000, pre_visualize_geometry=show_visual_outputs, save_png=False) + + # 8. run electrostatic simulation + + geo.electrostatic_simulation(voltage=[[1, 0.5, 0]], ground_outer_boundary=False, core_voltage=0, + show_fem_simulation_results=show_visual_outputs, save_to_excel_file=False) + # Call the electrostatic FEMM simulation function + # voltages = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140] + # geo.femm_reference_electrostatic(voltages=[[5, 0]], ground_core=True, ground_outer_boundary=False, + # non_visualize=0, save_to_excel_file=True, compare_femmt_to_femm=True, mesh_size_conductor=0.0) + + +if __name__ == "__main__": + basic_example_inductor_foil_vertical_electrostatic(show_visual_outputs=True) diff --git a/femmt/examples/basic_split_windings.py b/femmt/examples/basic_split_windings.py index c4381a39..c3d64120 100644 --- a/femmt/examples/basic_split_windings.py +++ b/femmt/examples/basic_split_windings.py @@ -73,7 +73,9 @@ def setup_simulation(working_directory, horizontal_split_factors, vertical_split [iso_against, iso_against, iso_against, iso_self, iso_against, iso_against, iso_against], [iso_against, iso_against, iso_against, iso_against, iso_self, iso_against, iso_against], [iso_against, iso_against, iso_against, iso_against, iso_against, iso_self, iso_against], - [iso_against, iso_against, iso_against, iso_against, iso_against, iso_against, iso_self]]) + [iso_against, iso_against, iso_against, iso_against, iso_against, iso_against, iso_self]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0001) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) diff --git a/femmt/examples/basic_transformer.py b/femmt/examples/basic_transformer.py index 7dff8f98..a777168b 100644 --- a/femmt/examples/basic_transformer.py +++ b/femmt/examples/basic_transformer.py @@ -115,7 +115,7 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in core = fmt.Core(core_dimensions=core_dimensions, mu_r_abs=3100, phi_mu_deg=12, sigma=1.2, permeability_datasource=fmt.MaterialDataSource.Custom, permittivity_datasource=fmt.MaterialDataSource.Custom, - detailed_core_model=True) + detailed_core_model=False) geo.set_core(core) # 3. set air gap parameters @@ -126,8 +126,10 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in # 4. set insulation insulation = fmt.Insulation(flag_insulation=True) insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -159,7 +161,11 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in geo.single_simulation(freq=200000, current=[2, 2], phi_deg=[0, 180], show_fem_simulation_results=show_visual_outputs) + # thermal simulation example_thermal_simulation(show_visual_outputs, flag_insulation=True) + # geo.get_inductances(I0=10, op_frequency=100000, skin_mesh_factor=0.5) + # Extract capacitance of transformer component + geo.get_capacitance_of_transformer() if __name__ == "__main__": diff --git a/femmt/examples/basic_transformer_5_windings.py b/femmt/examples/basic_transformer_5_windings.py index 1e86d853..fb9d89d3 100644 --- a/femmt/examples/basic_transformer_5_windings.py +++ b/femmt/examples/basic_transformer_5_windings.py @@ -138,7 +138,9 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in [iso_against, iso_self, iso_against, iso_against, iso_against], [iso_against, iso_against, iso_self, iso_against, iso_against], [iso_against, iso_against, iso_self, iso_against, iso_against], - [iso_against, iso_against, iso_against, iso_against, iso_self]]) + [iso_against, iso_against, iso_against, iso_against, iso_self]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0001) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_transformer_6_windings.py b/femmt/examples/basic_transformer_6_windings.py index fb1e5f10..d58688d8 100644 --- a/femmt/examples/basic_transformer_6_windings.py +++ b/femmt/examples/basic_transformer_6_windings.py @@ -139,7 +139,10 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in [iso_against, iso_against, iso_self, iso_against, iso_against, iso_against], [iso_against, iso_against, iso_against, iso_self, iso_against, iso_against], [iso_against, iso_against, iso_against, iso_against, iso_self, iso_against], - [iso_against, iso_against, iso_against, iso_against, iso_against, iso_self]]) + [iso_against, iso_against, iso_against, iso_against, iso_against, iso_self]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0001) + geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_transformer_center_tapped.py b/femmt/examples/basic_transformer_center_tapped.py index 2e621b37..f3d83850 100644 --- a/femmt/examples/basic_transformer_center_tapped.py +++ b/femmt/examples/basic_transformer_center_tapped.py @@ -143,6 +143,9 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in wrap_para_type=fmt.WrapParaType.FixedThickness, foil_horizontal_placing_strategy=fmt.FoilHorizontalDistribution.VerticalUpward) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=1e-4) + geo.set_insulation(insulation) geo.set_winding_windows([winding_window]) diff --git a/femmt/examples/basic_transformer_electrostatic.py b/femmt/examples/basic_transformer_electrostatic.py new file mode 100644 index 00000000..3760797f --- /dev/null +++ b/femmt/examples/basic_transformer_electrostatic.py @@ -0,0 +1,242 @@ +""" +Basic example to show how to simulate an inductor with electrostatic analysis. + +After starting the program, the geometry dimensions are displayed. Verify this geometry, close the window to continue the simulation. +This example simulates the electrostatic properties of an inductor, such as capacitance and electric fields. + +Once the geometry is verified, an electrostatic simulation will be run with voltages applied to the turns of the winding. +The simulation results will include electrostatic potential distributions, electric field maps, and capacitance data. +These results can help you understand how the electric field is distributed within the component and where high-field regions may occur. + +The simulation results will be visualized. In these visual outputs, you will be able to see the distribution of electrostatic potential in different turns of +the winding and the influence of the core and other materials in the geometry. +""" +import femmt as fmt +import os + +def basic_example_transformer_electrostatic(onelab_folder: str = None, show_visual_outputs: bool = True, is_test: bool = False): + """ + Run the example code for the transformer. + + :param onelab_folder: onelab folder path + :type onelab_folder: str + :param show_visual_outputs: True to show visual outputs (simulation results) + :type show_visual_outputs: bool + :param is_test: True for pytest usage. Defaults to False. + :type is_test: bool + """ + example_results_folder = os.path.join(os.path.dirname(__file__), "example_results") + if not os.path.exists(example_results_folder): + os.mkdir(example_results_folder) + + # Example for a transformer with multiple virtual winding windows. + working_directory = os.path.join(example_results_folder, os.path.splitext(os.path.basename(__file__))[0]) + if not os.path.exists(working_directory): + os.mkdir(working_directory) + + # 1. chose simulation type + geo = fmt.MagneticComponent(simulation_type=fmt.SimulationType.ElectroStatic, component_type=fmt.ComponentType.Transformer, + working_directory=working_directory, is_gui=is_test) + + # This line is for automated pytest running on GitHub only. Please ignore this line! + if onelab_folder is not None: + geo.file_data.onelab_folder_path = onelab_folder + + # 2. set core parameters + # core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=0.02, window_w=0.01, window_h=0.03, + # core_h=0.02) + # core = fmt.Core(core_dimensions=core_dimensions, mu_r_abs=3100, phi_mu_deg=12, sigma=1.2, + # permeability_datasource=fmt.MaterialDataSource.Custom, + # permittivity_datasource=fmt.MaterialDataSource.Custom, + # detailed_core_model=False) + # geo.set_core(core) + core_db = fmt.core_database()["PQ 40/40"] + core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=core_db["core_inner_diameter"], + window_w=core_db["window_w"], + window_h=core_db["window_h"], + core_h=core_db["core_h"]) + core = fmt.Core(core_type=fmt.CoreType.Single, + core_dimensions=core_dimensions, + detailed_core_model=False, + material=fmt.Material.N49, temperature=45, frequency=0, + # permeability_datasource="manufacturer_datasheet", + permeability_datasource=fmt.MaterialDataSource.Measurement, + permeability_datatype=fmt.MeasurementDataType.ComplexPermeability, + permeability_measurement_setup=fmt.MeasurementSetup.LEA_LK, + permittivity_datasource=fmt.MaterialDataSource.Measurement, + permittivity_datatype=fmt.MeasurementDataType.ComplexPermittivity, + permittivity_measurement_setup=fmt.MeasurementSetup.LEA_LK, mdb_verbosity=fmt.Verbosity.Silent) + + geo.set_core(core) + + # 3. set air gap parameters + air_gaps = fmt.AirGaps(fmt.AirGapMethod.Percent, core) + air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.0005, 50) + geo.set_air_gaps(air_gaps) + + # 4. set insulation + + bobbin_db = fmt.bobbin_database()["PQ 40/40"] + bobbin_dimensions = fmt.dtos.BobbinDimensions(bobbin_inner_diameter=bobbin_db["bobbin_inner_diameter"], + bobbin_window_w=bobbin_db["bobbin_window_w"], + bobbin_window_h=bobbin_db["bobbin_window_h"], + bobbin_h=bobbin_db["bobbin_h"]) + insulation = fmt.Insulation(flag_insulation=True, bobbin_dimensions=bobbin_dimensions) + bobbin_material = fmt.insulation_materials_database()["core_insulation"]["bobbins"]["Thermoset"]["Phenolic"] + insulation.add_core_insulations(1.55e-3, 1.55e-3, 0.9e-3, 1.5e-4, + dielectric_constant=bobbin_material["dielectric_constant"]) + insulation.add_winding_insulations([[0.0002, 0.095e-3], + [0.095e-3, 0.0002]], per_layer_of_turns=True) + turn_insulation_material = fmt.insulation_materials_database()["wire_insulation"]["plastic_insulation"]["Plenum Polyvinyl Chloride (Plenum PVC)"] + insulation.add_turn_insulation([0.25e-5, 0.25e-5], + dielectric_constant=[turn_insulation_material["dielectric_constant"], turn_insulation_material["dielectric_constant"]], + add_turn_insulations=False) + layer_insulation = fmt.insulation_materials_database()["film_insulation"]["Kapton"] + insulation.add_insulation_between_layers(add_insulation_material=True, thickness=0.5e-3, dielectric_constant=layer_insulation["dielectric_constant"]) + geo.set_insulation(insulation) + + # 5. create winding window and virtual winding windows (vww) + winding_window = fmt.WindingWindow(core, insulation) + # bot, top = winding_window.split_window(fmt.WindingWindowSplit.HorizontalSplit, split_distance=0.001) + # 109-49 + cells = winding_window.NHorizontalAndVerticalSplit(horizontal_split_factors=[0.29], + vertical_split_factors=None) + # cells = winding_window.NHorizontalAndVerticalSplit(horizontal_split_factors=[0.], + # vertical_split_factors=None) + + # 6. create conductors and set parameters + winding1 = fmt.Conductor(0, fmt.Conductivity.Copper) + # winding1.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + # winding1.set_solid_round_conductor(0.35e-3, fmt.ConductorArrangement.Square) + winding1.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + + # winding1 = fmt.Conductor(0, fmt.Conductivity.Copper) + # winding1.set_litz_round_conductor(0.0011, 50, 0.00011, None, fmt.ConductorArrangement.Square) + + # winding2 = fmt.Conductor(1, fmt.Conductivity.Copper) + # winding2.set_solid_round_conductor(0.0011, fmt.ConductorArrangement.Square) + + winding2 = fmt.Conductor(1, fmt.Conductivity.Copper) + # winding2.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + winding2.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + winding2.parallel = False + # winding2.set_litz_round_conductor(0.0011, 50, 0.00011, None, fmt.ConductorArrangement.Square) + + # 7. add conductor to vww and add winding window to MagneticComponent + # top.set_winding(winding2, 15, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + cells[1].set_winding(winding2, 10, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + # bot.set_winding(winding2, 29, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + cells[0].set_winding(winding1, 10, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + # top.set_winding(winding1, 109, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + geo.set_winding_windows([winding_window]) + + # 8. start simulation with given frequency, currents and phases + geo.create_model(freq=0, pre_visualize_geometry=show_visual_outputs) + # Number of turns in each winding + num_turns_w1 = 10 + num_turns_w2 = 10 + + """---------------------------------The definition of the 10 simulations needed for extracting the capacitance of the transformer------------------- """ + # Note that the user can run these 10 simulations by simply calling the get_capacitance_of_transformer() function + # and capacitance extraction is shown in the terminal + + # -------------------------------------------------------------------------------------- + # Simulation 1 (V_A, V_B, V_C, V_D = 1, 0, 0 , 0) --- (V_1, V_2, V_3, V_4 = 1, 0, 0, 0) + V_A = 1 + V_B = 0 + voltages_winding_1 = [ + V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + for i in range(num_turns_w1) + ] + voltages_winding_2 = [1] * num_turns_w2 + + # -------------------------------------------------------------------------------------- + # # Simulation 2 (V_A, V_B, V_C, V_D = 0, 0, 1 , 0) --- (V_1, V_2, V_3, V_4 = 0, 1, 0, 0) + # voltages_winding_1 = [0] * num_turns_w1 + # V_C = 1.0 + # V_D = 0.0 + # voltages_winding_2 = [ + # V_C - (V_C - V_D) * i / (num_turns_w2 - 1) + # for i in range(num_turns_w2) + # ] + + # --------------------------------------------------------------------------------------------- + # # Simulation 3 (V_A, V_B, V_C, V_D = 0, 0, 1 , 1) --- (V_1, V_2, V_3, V_4 = 0, 0, 1, 0) + # voltages_winding_1 = [0] * num_turns_w1 + # voltages_winding_2 = [1] * num_turns_w2 + + # ----------------------------------------------------------------------------------------------- + # # Simulation 4 (V_1, V_2, V_3, V_4 = 1, 1, 1 , 1) --- (V_1, V_2, V_3, V_4 = 0, 0, 0, 1) + # voltages_winding_1 = [1] * num_turns_w1 + # voltages_winding_2 = [1] * num_turns_w2 + + # ----------------------------------------------------------------------------------------------- + # # Simulation 5 (V_A, V_B, V_C, V_D = 1, 0, 1 , 0) --- (V_1, V_2, V_3, V_4 = 1, 1, 0, 0) + # V_A = 1.0 + # V_B = 0.0 + # V_C = 1.0 + # V_D = 0.0 + # voltages_winding_1 = [ + # V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + # for i in range(num_turns_w1) + # ] + # voltages_winding_2 = [ + # V_C - (V_C - V_D) * i / (num_turns_w2 - 1) + # for i in range(num_turns_w2) + # ] + + # ------------------------------------------------------------------------------------------------ + # # Simulation 6 (V_A, V_B, V_C, V_D = 1, 0, 1 , 1) --- (V_1, V_2, V_3, V_4 = 1, 0, 1, 0) + # V_A = 1.0 + # V_B = 0.0 + # voltages_winding_1 = [ + # V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + # for i in range(num_turns_w1) + # ] + # voltages_winding_2 = [1] * num_turns_w2 + + # -------------------------------------------------------------------------------------------------- + # # Simulation 7 (V_A, V_B, V_C, V_D = 2, 1, 1 , 1) --- (V_1, V_2, V_3, V_4 = 1, 0, 0, 1) + # V_A = 2.0 + # V_B = 1.0 + # voltages_winding_1 = [ + # V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + # for i in range(num_turns_w1) + # ] + # voltages_winding_2 = [1] * num_turns_w2 + + # ------------------------------------------------------------------------------------------------------ + # # Simulation 8 (V_A, V_B, V_C, V_D = 0, 0, 2 , 1) --- (V_1, V_2, V_3, V_4 = 0, 1, 1, 1) + # voltages_winding_1 = [0] * num_turns_w1 + # V_C = 2.0 + # V_D = 1.0 + # voltages_winding_2 = [ + # V_C - (V_C - V_D) * i / (num_turns_w2 - 1) + # for i in range(num_turns_w2) + # ] + + # --------------------------------------------------------------- + # # Simulation 9 (V_A, V_B, V_C, V_D = 1, 1, 2 , 1) --- (V_1, V_2, V_3, V_4 = 0, 1, 0, 1) + # voltages_winding_1 = [1] * num_turns_w1 + # V_C = 2.0 + # V_D = 1.0 + # voltages_winding_2 = [ + # V_C - (V_C - V_D) * i / (num_turns_w2 - 1) + # for i in range(num_turns_w2) + # ] + + # --------------------------------------------------------------- + # # Simulation 10 (V_A, V_B, V_C, V_D = 1, 1, 2 , 2) --- (V_1, V_2, V_3, V_4 = 0, 1, 0, 1) + # # Create a fixed voltage from C to D + # voltages_winding_1 = [1] * num_turns_w1 + # voltages_winding_2 = [2] * num_turns_w2 + + geo.electrostatic_simulation(voltage=[voltages_winding_1, voltages_winding_2], core_voltage=0, ground_outer_boundary=False, + show_fem_simulation_results=show_visual_outputs, save_to_excel_file=False) + # geo.get_total_charges() + # geo.femm_reference_electrostatic(voltages=[voltages_winding_1, voltages_winding_2], ground_core=True, ground_outer_boundary=True, non_visualize=0, + # save_to_excel_file=False, compare_femmt_to_femm=False) + + +if __name__ == "__main__": + basic_example_transformer_electrostatic(show_visual_outputs=True) diff --git a/femmt/examples/basic_transformer_excitation_sweep.py b/femmt/examples/basic_transformer_excitation_sweep.py index 01540ecf..7c54919d 100644 --- a/femmt/examples/basic_transformer_excitation_sweep.py +++ b/femmt/examples/basic_transformer_excitation_sweep.py @@ -70,7 +70,9 @@ def basic_example_transformer_excitation_sweep(onelab_folder: str = None, show_v insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.0002], - [0.0002, 0.0002]]) + [0.0002, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_transformer_integrated.py b/femmt/examples/basic_transformer_integrated.py index 93ea0316..f25221bc 100644 --- a/femmt/examples/basic_transformer_integrated.py +++ b/femmt/examples/basic_transformer_integrated.py @@ -131,8 +131,10 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in # 4. set insulations insulation = fmt.Insulation(flag_insulation=False) insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.1e-3) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_transformer_interleaved.py b/femmt/examples/basic_transformer_interleaved.py index ccd4f702..5c743a97 100644 --- a/femmt/examples/basic_transformer_interleaved.py +++ b/femmt/examples/basic_transformer_interleaved.py @@ -126,7 +126,9 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_transformer_n_winding.py b/femmt/examples/basic_transformer_n_winding.py index 30f83e2e..8471b5af 100644 --- a/femmt/examples/basic_transformer_n_winding.py +++ b/femmt/examples/basic_transformer_n_winding.py @@ -136,7 +136,10 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in [0.0004, 0.0002, 0.0004, 0.0002, 0.0002, 0.0004, 0.0002, 0.0002, 0.0002, 0.0004, 0.0002, 0.0004], [0.0002, 0.0004, 0.0004, 0.0002, 0.0002, 0.0004, 0.0002, 0.0002, 0.0002, 0.0004, 0.0002, 0.0004], [0.0004, 0.0002, 0.0004, 0.0002, 0.0002, 0.0004, 0.0002, 0.0002, 0.0002, 0.0004, 0.0002, 0.0004], - [0.0004, 0.0002, 0.0004, 0.0002, 0.0002, 0.0004, 0.0002, 0.0002, 0.0002, 0.0004, 0.0002, 0.0004]]) + [0.0004, 0.0002, 0.0004, 0.0002, 0.0002, 0.0004, 0.0002, 0.0002, 0.0002, 0.0004, 0.0002, 0.0004]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5], + add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/basic_transformer_stacked.py b/femmt/examples/basic_transformer_stacked.py index 3d8a5922..69dd2a7c 100644 --- a/femmt/examples/basic_transformer_stacked.py +++ b/femmt/examples/basic_transformer_stacked.py @@ -127,7 +127,9 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in insulation = fmt.Insulation(flag_insulation=False) insulation.add_core_insulations(0.001, 0.001, 0.001, 0.001) # [bot, top, left, right] insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window_top, winding_window_bot = fmt.create_stacked_winding_windows(core, insulation) diff --git a/femmt/examples/basic_transformer_stacked_center_tapped.py b/femmt/examples/basic_transformer_stacked_center_tapped.py index 73fa57b8..41a70a15 100644 --- a/femmt/examples/basic_transformer_stacked_center_tapped.py +++ b/femmt/examples/basic_transformer_stacked_center_tapped.py @@ -146,6 +146,9 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in wrap_para_type=fmt.WrapParaType.FixedThickness, foil_horizontal_placing_strategy=fmt.FoilHorizontalDistribution.VerticalUpward) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) + geo.set_insulation(insulation) geo.set_winding_windows([coil_window, transformer_window]) diff --git a/femmt/examples/basic_transformer_three_winding.py b/femmt/examples/basic_transformer_three_winding.py index 18c9bf1d..96ad1f9a 100644 --- a/femmt/examples/basic_transformer_three_winding.py +++ b/femmt/examples/basic_transformer_three_winding.py @@ -127,7 +127,9 @@ def example_thermal_simulation(show_thermal_visual_outputs: bool = True, flag_in insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.0004, 0.0004], [0.0004, 0.0002, 0.0004], - [0.0004, 0.0004, 0.0002]]) + [0.0004, 0.0004, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/component_study/transformer_component_study.py b/femmt/examples/component_study/transformer_component_study.py index 0054deb7..443a9452 100644 --- a/femmt/examples/component_study/transformer_component_study.py +++ b/femmt/examples/component_study/transformer_component_study.py @@ -60,7 +60,9 @@ def transformer_component_study(onelab_folder: str | None = None, show_visual_ou insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/experimental_inductor_time_domain.py b/femmt/examples/experimental_inductor_time_domain.py index 193fccea..a90191e6 100644 --- a/femmt/examples/experimental_inductor_time_domain.py +++ b/femmt/examples/experimental_inductor_time_domain.py @@ -79,7 +79,9 @@ def basic_example_inductor_time_domain(onelab_folder: str = None, show_visual_ou # 4. set insulations insulation = fmt.Insulation(flag_insulation=True) insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/experimental_transformer_three_winding_time_domain.py b/femmt/examples/experimental_transformer_three_winding_time_domain.py index 811700c2..3cede58c 100644 --- a/femmt/examples/experimental_transformer_three_winding_time_domain.py +++ b/femmt/examples/experimental_transformer_three_winding_time_domain.py @@ -63,7 +63,9 @@ def basic_example_transformer_three_windings_time_domain(onelab_folder: str = No insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.0004, 0.0004], [0.0004, 0.0002, 0.0004], - [0.0004, 0.0004, 0.0002]]) + [0.0004, 0.0004, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/examples/experimental_transformer_time_domain.py b/femmt/examples/experimental_transformer_time_domain.py index 85ad13cf..84e70e6a 100644 --- a/femmt/examples/experimental_transformer_time_domain.py +++ b/femmt/examples/experimental_transformer_time_domain.py @@ -62,7 +62,9 @@ def basic_example_transformer_time_domain(onelab_folder: str = None, show_visual insulation = fmt.Insulation(flag_insulation=True) insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) diff --git a/femmt/functions.py b/femmt/functions.py index b9b081de..b0860c39 100644 --- a/femmt/functions.py +++ b/femmt/functions.py @@ -1,5 +1,6 @@ """Contains different functions, used by the whole FEMMT functions.""" # Python standard libraries +# Python standard libraries import json import pkg_resources import subprocess @@ -242,6 +243,329 @@ def core_database() -> dict: } return core_dict +def bobbin_database() -> dict: + """ + Return a dictionary containing bobbin dimensions for various core structures. + + :return: Dictionary containing bobbin dimensions from Datasheet. + :rtype: Dict + """ + bobbin_dict = {} + + # ----------------------- + # PQ Bobbins + # ----------------------- + + # bobbin_dict["PQ 16/11.6"] = { + # "bobbin_h": None, + # "bobbin_inner_diameter": None, + # "bobbin_window_h": None, + # "bobbin_window_w": None + # } + bobbin_dict["PQ 20/16"] = { + "bobbin_h": 18.3e-3, + "bobbin_inner_diameter": 10.95e-3, + "bobbin_window_h": 9.8e-3, + "bobbin_window_w": (23.3 - 9.15) / 2 * 1e-3 + } + bobbin_dict["PQ 20/20"] = { + "bobbin_h": 22.3e-3, + "bobbin_inner_diameter": 10.9e-3, + "bobbin_window_h": 13.9e-3, + "bobbin_window_w": (23.3 - 9.2) / 2 * 1e-3 + } + bobbin_dict["PQ 26/25"] = { + "bobbin_h": 29.3e-3, + "bobbin_inner_diameter": 14.2e-3, + "bobbin_window_h": 15.5e-3, + "bobbin_window_w": (26.5 - 14.2) / 2 * 1e-3 + } + bobbin_dict["PQ 32/20"] = { + "bobbin_h": 18.8e-3, + "bobbin_inner_diameter": 16.2e-3, + "bobbin_window_h": 10.7e-3, + "bobbin_window_w": (32.3 - 16.2) / 2 * 1e-3, + } + bobbin_dict["PQ 32/30"] = { + "bobbin_h": 32.8e-3, + "bobbin_inner_diameter": 15.9e-3, + "bobbin_window_h": 20.7e-3, + "bobbin_window_w": (26.4 - 15.9) / 2 * 1e-3 + } + bobbin_dict["PQ 40/40"] = { + "bobbin_h": 45.3e-3, + "bobbin_inner_diameter": 15.55e-3, + "bobbin_window_h": 28.75e-3, + "bobbin_window_w": (40.3 - 15.55) / 2 * 1e-3 + } + bobbin_dict["PQ 50/50"] = { + "bobbin_h": 51.5e-3, + "bobbin_inner_diameter": 23.2e-3, + "bobbin_window_h": 35.2e-3, + "bobbin_window_w": (51.3 - 23.2) / 2 * 1e-3 + } + bobbin_dict["PQ 65/60"] = { + "bobbin_h": 65.5e-3, + "bobbin_inner_diameter": 27.3e-3, + "bobbin_window_h": 40.7e-3, + "bobbin_window_w": (66.5 - 27.3) / 2 * 1e-3 + } + + # ----------------------- + # PM Bobbins + # ----------------------- + + bobbin_dict["PM 114/93"] = { + "bobbin_h": 91 * 1e-3, + "bobbin_inner_diameter": 44e-3, + "bobbin_window_h": 62.3e-3, + "bobbin_window_w": (91 - 44) / 2 * 1e-3, + } + bobbin_dict["PM 50/39"] = { + "bobbin_h": 33.8 * 1e-3, + "bobbin_inner_diameter": 20.4e-3, + "bobbin_window_h": 25.9e-3, + "bobbin_window_w": (38.5 - 20.4) / 2 * 1e-3, + } + bobbin_dict["PM 62/49"] = { + "bobbin_h": 48 * 1e-3, + "bobbin_inner_diameter": 25.7e-3, + "bobbin_window_h": 33.0e-3, + "bobbin_window_w": (48 - 25.7) / 2 * 1e-3, + } + bobbin_dict["PM 74/59"] = { + "bobbin_h": 57.8 * 1e-3, + "bobbin_inner_diameter": 30e-3, + "bobbin_window_h": 40.3e-3, + "bobbin_window_w": (57.3 - 30) / 2 * 1e-3, + } + bobbin_dict["PM 87/70"] = { + "bobbin_h": 68.2 * 1e-3, + "bobbin_inner_diameter": 32.5e-3, + "bobbin_window_h": 47.2e-3, + "bobbin_window_w": (66.2 - 32.5) / 2 * 1e-3, + } + return bobbin_dict + +def insulation_materials_database() -> dict: + """ + Return insulation properties for different type of materials. + + :return: Dict including insulation parameters + :rtype: dict + """ + # To see the shape and the properties of these materials of the wire insulation, review this website: + # https://www.awcwire.com/customersupport/techinfo/insulation-materials?srsltid=AfmBOoqXkVrB6ITF-R9nL_UlgGVpzX2xB2ENjMNQQHmczLRcf0-y6YwG + + insulation_materials = { + # wire_insulation materials + "wire_insulation": { + # Plastic materials + "plastic_insulation": { + # PVC + # 1.a PVC (pure) + "Polyvinyl Chloride (PVC)": {"dielectric_constant": 4.0, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Huang, J., Zhang, X., Liu, R., Ding, Y., & Guo, D. (2023). Polyvinyl chloride-based dielectric elastomer with high permittivity + # and low viscoelasticity for actuation and sensing. Nature communications, 14(1), 1483.""" + + # 1.b Semi-Rigid PVC (SR-PVC). it is from 2.7 to 6.5 + "Semi-Rigid PVC (SR-PVC)": {"dielectric_constant": 3.6, "thermal_conductivity": None, "max_temperature": None}, + # 1.c Plenum Polyvinyl Chloride (Plenum PVC) + "Plenum Polyvinyl Chloride (Plenum PVC)": {"dielectric_constant": 3.5, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://www.anixter.com/content/dam/Anixter/Guide/7H0011X0_W&C_Tech_Handbook_Sec_03.pdf""" + + # 2. Polyethylene (PE): it ranges from 2.7 to 2.8 (30% glass fiber) + "Polyethylene (PE)": {"dielectric_constant": 2.7, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://passive-components.eu/what-is-dielectric-constant-of-plastic-materials/""" + # """Reference: + # https://www.awcwire.com/customersupport/techinfo/insulation-materials#:~:text=Semi%2DRigid%20PVC%20(SR%2DPVC)%20is%20mainly%20used, + # degrees%20Celsius%2C%20300%20volts).""" + + # 3.Polypropylene (PP) 10-20 glass fiber 2.2 - 2.3 + "Polypropylene (PP)": {"dielectric_constant": 2.3, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: + # https://www.awcwire.com/customersupport/techinfo/insulation-materials#:~:text=Semi%2DRigid%20PVC%20(SR%2DPVC)%20is%20mainly%20used, + # degrees%20Celsius%2C%20300%20volts).""" + + # 4.Polyurethane (PUR): the permittivity differs from 1.065 to 3.35 based on the density (kg/m^3). (need to be reviewed) + "Polyurethane (PUR)": {"dielectric_constant": 3.35, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Beverte, I. (2025). + # Investigation of the Partial Permittivity of Rigid Polyurethane Foams by a Circular One-Side-Access Capacitive Sensor. + # Polymers, 17(5), 602.""" + # """https://www.anixter.com/content/dam/Anixter/Guide/7H0011X0_W&C_Tech_Handbook_Sec_03.pdf""" + + # 5. Chlorinated Polyethylene (CPE) + "Chlorinated Polyethylene (CPE)": {"dielectric_constant": 2.3, "thermal_conductivity": None, "max_temperature": None}, + # """https://www.anixter.com/content/dam/Anixter/Guide/7H0011X0_W&C_Tech_Handbook_Sec_03.pdf""" + + # 6. Nylon : 3.2 - 5 + "Nylon": {"dielectric_constant": 5, "thermal_conductivity": None, "max_temperature": None}, + # """https://www.anixter.com/content/dam/Anixter/Guide/7H0011X0_W&C_Tech_Handbook_Sec_03.pdf""" + }, + # Rubber Materials + "rubber_insulation": { + # 1. Thermoplastic Rubber (TPR) 3.30 - 5.10. It can be called Thermoplastic Elastomer (TPE) + "Thermoplastic Rubber(TPR)": {"dielectric_constant": 5.10, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://www.matweb.com/search/datasheet.aspx?matguid=0619837e5f584a1f8c5e6f692952898a""" + + # 2. Neoprene (Polychloroprene): 4-6.7 + "Neoprene (Polychloroprene)": {"dielectric_constant": 6.7, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://hep.physics.illinois.edu/home/serrede/p435/lecture_notes/dielectric_constants.pdf""" + + # 3. Styrene-Butadiene Rubber (SBR): 2.5 - 3 + "Styrene-Butadiene Rubber (SBR)": {"dielectric_constant": 3.0, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://www.azom.com/properties.aspx?ArticleID=1844""" + + # 4. Silicone: 2.9 - 4 + "Silicone": {"dielectric_constant": 4.0, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://en.wikipedia.org/wiki/Relative_permittivity""" + + # 5. Fiberglass 3.0 - 4.0 + "Fiberglass": {"dielectric_constant": 4.0, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://passive-components.eu/what-is-dielectric-constant-of-plastic-materials/""" + + # 6. Ethylene Propylene Rubber (EPR): 2.4 and can reach 4 + "Ethylene Propylene Rubber (EPR)": {"dielectric_constant": 2.4, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://passive-components.eu/what-is-dielectric-constant-of-plastic-materials/""" + + # 7. Rubber ( refers to natural rubber and SBR compounds.) + # 7.a: natural rubber: 2.7 – 4.0 (low freq); 2.4–2.7 (GHz range) + "Natural Rubber": {"dielectric_constant": 2.7, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Al-Hartomy, O. A., Al-Ghamdi, A., Dishovsky, N., Shtarkova, R., Iliev, V., Mutlay, I., & El-Tantawy, F. (2012). + # Dielectric and microwave properties of natural rubber based nanocomposites containing graphene.""" + + # 7.b: SBR: 2.5 to 3 (low freq); up to 6.6 (Ghz freq) + "Rubber (SBR)": {"dielectric_constant": 3, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Gunasekaran,S.,Natarajan, R. K., Kala, A., & Jagannathan, R. (2008). + # Dielectric studies of some rubber materials at microwave frequencies.""" + + # 8. Chlorosulfonated Polyethylene (CSPE): Measured dielectric constant: 8-10 + "Chlorosulfonated Polyethylene (CSPE)": {"dielectric_constant": 8.5, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Ganguly, S., & Das, N. C. (2015). Chlorosulphonated polyethylene and its composites for electronic applications. + # In Flexible and stretchable electronic composites (pp. 229-259). Cham: Springer International Publishing.""" + }, + # Fluoropolymer Insulation Types + "fluoropolymer_insulation": { + # 1. Perfluoroalkoxy (PFA): dielectric constant: 2.06 to 2.10: + "Perfluoroalkoxy (PFA)": {"dielectric_constant": 2.06, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://adtech.co.uk/application/files/1816/0500/0871/Adtech_PFA_General_Properties_2020.pdf""" + # """Reference: https://www.fluorotherm.com/technical-information/materials-overview/pfa-properties/""" + + # 2. Polytetrafluoroethylene (PTFE): dielectric constant: 2.12 to 2.01 + "Polytetrafluoroethylene (PTFE)": {"dielectric_constant": 2.12, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Li, L., Bowler, N., Kessler, M. R., & Yoon, S. H. (2010). + # Dielectric response of PTFE and ETFE wiring insulation to thermal exposure. + # IEEE Transactions on Dielectrics and Electrical Insulation, 17(4), 1234-1241.""" + + # 3. Fluorinated Ethylene Propylene (FEP): dielectric constant is about 2.2 + "Fluorinated Ethylene Propylene (FEP)": {"dielectric_constant": 2.2, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Lv, X., Yv, J., Wang, X., & Huang, P. (2022). + # Flexible low dielectric polyimide/fluorinated ethylene propylene composite films for + # flexible integrated circuits. Polymer Science, Series B, 64(2), 219-228..""" + + # 4. Ethylene Tetrafluoroethylene (ETFE) : dielectric constant is about 2.2 to 2.6 + "Ethylene Tetrafluoroethylene (ETFE)": {"dielectric_constant": 2.6, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: Wang, M., He, Y., Yang, X., Hou, X., Li, W., Tan, S., & Zhang, Z. (2024). Optimizing thermal and dielectric properties of + # ethylene-tetrafluoroethylene (ETFE)/h-BN composites via interface engineering: activation of C–F bonds on ETFE for surface grafting. + # Journal of Materials Chemistry A, 12(45), 31424-31431.""" + + # 5. Ethylenechlorotrifluoroethylene (ECTFE) : dielectric constant is 2.5 + # Note 4. and 5. have approximately the same properties + "Ethylenechlorotrifluoroethylene (ECTFE)": {"dielectric_constant": 2.5, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://www.polyfluor.nl/assets/files/datasheet-ectfe-uk.pdf""" + + # 6. Polyvinylidene Fluoride (PVDF) : dielectric constant is 8.5 at 1MHz + "Polyvinylidene Fluoride (PVDF)": {"dielectric_constant": 8.5, "thermal_conductivity": None, "max_temperature": None}, + # """Reference: https://www.ipolymer.com/pdf/PVDF.pdf""" + + # 7.Thermoplastic Elastomers (TPE) : dielectric constant is 3.3 to 5.1 + "Thermoplastic Elastomers (TPE)": {"dielectric_constant": 4.5, "thermal_conductivity": None, "max_temperature": None}, + # """https://www.matweb.com/search/datasheet.aspx?matguid=0619837e5f584a1f8c5e6f692952898a&""" + + } + }, + + # Bobbin materials is based on these references: + # 1. https://www.cosmocorp.com/docs/en/cosmo-bcat-en-mdres.pdf + # 2. https://pearl-hifi.com/06_Lit_Archive/06_Mchy_Methods_Catalogues/Coil_Winding/Transformer_Bobbin_and_Core_Selection.pdf + + "core_insulation": { + "bobbins": { + "Thermoplastic": { + "Polyamide (Nylon 66)": { + "dielectric_constant": 3.8, + "thermal_conductivity": 0.25, + "max_temperature": 130 + }, + "Polybutylene Terephthalate (PBT)": { + "dielectric_constant": 3.7, + "thermal_conductivity": 0.25, + "max_temperature": 155 + }, + "Polyphenylene Sulfide (PPS)": { + "dielectric_constant": 3.8, + "thermal_conductivity": 0.30, + "max_temperature": 200 + }, + "Liquid Crystal Polymer (LCP)": { + "dielectric_constant": 3.6, + "thermal_conductivity": 0.50, + "max_temperature": 240 + }, + "Polyethylene Terephthalate (PET)": { + "dielectric_constant": 3.6, + "thermal_conductivity": 0.15, + "max_temperature": 150 + } + }, + "Thermoset": { + "Diallyl Phthalate (DAP)": { + "dielectric_constant": 4.4, + "thermal_conductivity": 0.20, + "max_temperature": 200 + }, + "Phenolic": { + "dielectric_constant": 4.5, + "thermal_conductivity": 0.20, + "max_temperature": 220 + } + } + } + }, + + # The kapton has different types, but the dielectric constant is approximately around 3.4-3.5 + # Reference: https://www.dupont.com/content/dam/electronics/amer/us/en/electronics/public/documents/en/EI-10167-Kapton-General-Specifications.pdf + "film_insulation": { + "Kapton": { + "dielectric_constant": 3.5, + "thermal_conductivity": None, + "max_temperature": None, + }, + "Nomex 410": { + "dielectric_constant": 1.6, + "thermal_conductivity": None, + "max_temperature": None, + }, + "Mylar": { + "dielectric_constant": 3.2, + "thermal_conductivity": None, + "max_temperature": None, + }, + "PET / bOPET": { + "dielectric_constant": 3.3, + "thermal_conductivity": None, + "max_temperature": None, + }, + "PVC Tape": { + "dielectric_constant": 4.0, + "max_temperature": None, + "thermal_conductivity": None + } + } + } + + return insulation_materials + def litz_database() -> dict: """ @@ -1812,6 +2136,513 @@ def hysteresis_current_excitation(input_time_current_vectors: list[list[list[flo fr.phases_deg_from_time_current(time_current_vector[0], time_current_vector[1])[0]) return hyst_frequency, hyst_current_amplitudes, hyst_phases_deg +def get_defined_potentials(component_type: str) -> list[list[float]]: + """ + Define of different potentials to be applied in the simulation. + + It is needed to save the energies with every simulation based on the number of the capacitors in the equivalent + circuit and to calculate the voltage matrix. + + :param component_type : the type of the component to be solved + :type component_type: str + """ + if component_type == 'inductor': + return [[1, 0, 0], # Scenario 1 + [1, 1, 0], # Scenario 2 + [1, 0, 1]] # Scenario 3 + if component_type == 'transformer': + return [[1, 0, 0, 0], # Simulation 1 + [0, 0, 1, 0], # Simulation 2 + [0, 0, 1, 1], # Simulation 3 + [1, 1, 1, 1], # Simulation 4 + [1, 0, 1, 0], # Simulation 5 + [1, 0, 1, 1], # Simulation 6 + [2, 1, 1, 1], # Simulation 7 + [0, 0, 2, 1], # Simulation 8 + [1, 1, 2, 1], # Simulation 9 + [1, 1, 2, 2]] # Simulation 10 + else: + raise ValueError(f"Unknown component_type: {component_type}") + +def generate_voltage_matrix(component_type: str, potentials: list[list[float]], flip_the_sec_terminal: bool = False) -> np.ndarray: + r""" + Generate the voltage matrix from the potentials. + + W = 0.5 (M^2) * C. M represents the voltage matrix derived from the applied potentials. + Equivalent circuit of inductor: + A--.------.---A + | | + | | + v_1 | c1 + | | + | | + B--|------.---B + | | + v_2 c2 c3 + | | + E--.-----.----E + Equivalent circuit of transformer: + A--.------.-----c4--------.------.------C + | | \ / | | + | | c5 c6 | | + v1 | c1 \/ c2 | v2 + | | / \ | | + | | / \ | | + B--|------.-----c3--------.------|------D + | | v3 | | + c7 c8 v4 c10 c9 + | | | | + E--.-----.-----------------------.------E + + :param component_type : the type of the component to be solved. + :type component_type: str + :param potentials: list of potentials + :type potentials: list[list[float]] + :param flip_the_sec_terminal: flip the sec voltage. v2 will be negative. + :type flip_the_sec_terminal: bool + """ + diffs = [] + for vs in potentials: + if component_type == 'inductor': + a, b, e = vs + # Potentials are a, b, e, where the voltage matrix is derived from these potentials. + diffs.append([a - b, + a - e, + b - e]) + elif component_type == 'transformer': + # Core is always grounded + # a, b, c, d are potentials (terminals). The voltage matrix is derived from these potentials. + # see the equivalent circuit of transformer in the description + e = 0 + a, b, c, d = vs + if not flip_the_sec_terminal: + diffs.append([a - b, + c - d, + d - b, + a - b - (d - b) - (c - d), + d - b + c - d, + a - b - (d - b), + a - b + b - e, + b - e, + c - d + d - b + b - e, + d - b + b - e + ]) + elif flip_the_sec_terminal: + diffs.append([a - b, + d - c, + d - b, + a - b - (d - b) - (d - c), + d - b + d - c, + a - b - (d - b), + a - b + b - e, + b - e, + d - c + d - b + b - e, + d - b + b - e + ]) + + return np.array(diffs) + +def solve_capacitance(m: np.ndarray, energies: np.ndarray) -> np.ndarray: + """ + Solve the capacitance from the voltage matrix and the energy matrix saved from the simulation. + + :param m: voltage matrix + :type: array + :param energies: energies solved from the simulations + :type energies: byte array + """ + m_squared = m ** 2 + if np.isclose(np.linalg.det(m_squared), 0): + raise ValueError("Singular matrix!") + return (2 * np.linalg.inv(m_squared) @ energies.reshape(-1, 1)).flatten() + +def distribute_potential_linearly(v_start: float, v_end: float, num_turns: int) -> list[float]: + """ + Linearly distribute potentials between v_start and v_end over the turns. + + :param v_start: the voltage on the first turn + :type v_start: float + :param v_end: the voltage on the last turn + :type v_end: float + :param num_turns: number of turns + :type num_turns: int + """ + if v_start == v_end or num_turns == 1: + return [v_start] * num_turns + return [v_start + (v_end - v_start) * j / (num_turns - 1) for j in range(num_turns)] + +def get_open_circuit_capacitance(c_vec: np.ndarray, num_turns_w1: int, num_turns_w2: int) -> float: + """ + Get the capacitance when the secondary is open. + + :param c_vec: the calculated capacitance from the simulation. + :type c_vec: byte array + :param num_turns_w1: number of turns of the first winding + :type num_turns_w1: int + :param num_turns_w2: number of turns of the second winding + :type num_turns_w2: int + """ + c_1, c_2, c_3, c_4, c_5, c_6, c_7, c_8, c_9, c_10 = c_vec + n_sym = num_turns_w1 / num_turns_w2 + den = c_3 * c_7 + c_3 * c_8 + c_4 * c_7 + c_3 * c_9 + c_4 * c_8 + c_5 * c_7 + c_3 * c_10 + c_4 * c_9 + \ + c_5 * c_8 + c_6 * c_7 + c_4 * c_10 + c_5 * c_9 + c_6 * c_8 + c_5 * c_10 + c_6 * c_9 + c_6 * c_10 + \ + c_7 * c_9 + c_7 * c_10 + c_8 * c_9 + c_8 * c_10 + + num1 = (c_2 * c_3 * c_7 + c_2 * c_3 * c_8 + c_2 * c_4 * c_7 + c_2 * c_3 * c_9 + c_2 * c_4 * c_8 + c_2 * c_5 * c_7 + c_3 * c_4 * c_7 + c_2 * c_3 * c_10 + \ + c_2 * c_4 * c_9 + c_2 * c_5 * c_8 + c_2 * c_6 * c_7 + c_3 * c_4 * c_8 + c_3 * c_5 * c_7 + c_2 * c_4 * c_10 + c_2 * c_5 * c_9 + c_2 * c_6 * c_8 + \ + c_3 * c_4 * c_9 + c_3 * c_5 * c_8 + c_2 * c_5 * c_10 + c_2 * c_6 * c_9 + c_3 * c_4 * c_10 + c_3 * c_5 * c_9 + c_4 * c_6 * c_7 + \ + c_2 * c_6 * c_10 + c_2 * c_7 * c_9 + c_3 * c_5 * c_10 + c_4 * c_6 * c_8 + c_5 * c_6 * c_7 + c_2 * c_7 * c_10 + c_2 * c_8 * c_9 + \ + c_3 * c_7 * c_9 + c_4 * c_6 * c_9 + c_5 * c_6 * c_8 + c_2 * c_8 * c_10 + c_3 * c_8 * c_9 + c_4 * c_6 * c_10 + c_5 * c_6 * c_9 + \ + c_4 * c_7 * c_10 + c_5 * c_6 * c_10 + c_3 * c_9 * c_10 + c_4 * c_8 * c_10 + c_5 * c_7 * c_10 + c_6 * c_7 * c_9 + c_4 * c_9 * c_10 + \ + c_5 * c_8 * c_10 + c_6 * c_8 * c_9 + c_5 * c_9 * c_10 + c_6 * c_9 * c_10 + c_7 * c_9 * c_10 + c_8 * c_9 * c_10) * n_sym ** 2 + + num2 = (-2 * c_3 * c_4 * c_7 - 2 * c_3 * c_4 * c_8 - 2 * c_3 * c_4 * c_9 - 2 * c_3 * c_4 * c_10 + 2 * c_5 * c_6 * c_7 + 2 * c_3 * c_7 * c_9 - \ + 2 * c_5 * c_6 * c_8 - 2 * c_5 * c_6 * c_9 - 2 * c_5 * c_6 * c_10 + 2 * c_4 * c_8 * c_10 - 2 * c_5 * c_7 * c_10 - 2 * c_6 * c_8 * c_9) * n_sym + + num3 = (c_1 * c_3 * c_7 + c_1 * c_3 * c_8 + c_1 * c_4 * c_7 + c_1 * c_3 * c_9 + c_1 * c_4 * c_8 + c_1 * c_5 * c_7 + c_1 * c_3 * c_10 + c_1 * c_4 * c_9 + \ + c_1 * c_5 * c_8 + c_1 * c_6 * c_7 + c_3 * c_4 * c_7 + c_1 * c_4 * c_10 + c_1 * c_5 * c_9 + c_1 * c_6 * c_8 + c_3 * c_4 * c_8 + c_1 * c_5 * c_10 + \ + c_1 * c_6 * c_9 + c_3 * c_4 * c_9 + c_3 * c_6 * c_7 + c_4 * c_5 * c_7 + c_1 * c_6 * c_10 + c_1 * c_7 * c_9 + c_3 * c_4 * c_10 + c_3 * c_6 * c_8 + \ + c_4 * c_5 * c_8 + c_1 * c_7 * c_10 + c_1 * c_8 * c_9 + c_3 * c_6 * c_9 + c_3 * c_7 * c_8 + c_4 * c_5 * c_9 + c_5 * c_6 * c_7 + c_1 * c_8 * c_10 + \ + c_3 * c_6 * c_10 + c_3 * c_7 * c_9 + c_4 * c_5 * c_10 + c_4 * c_7 * c_8 + c_5 * c_6 * c_8 + c_3 * c_7 * c_10 + c_5 * c_6 * c_9 + c_5 * c_7 * c_8 + \ + c_4 * c_8 * c_9 + c_5 * c_6 * c_10 + c_5 * c_7 * c_9 + c_6 * c_7 * c_8 + c_4 * c_8 * c_10 + c_5 * c_7 * c_10 + c_6 * c_8 * c_9 + \ + c_6 * c_8 * c_10 + c_7 * c_8 * c_9 + c_7 * c_8 * c_10) + return (num1 - num2 + num3) / den + +def get_short_circuit_capacitance(c_vec: np.ndarray) -> float: + """ + Get the capacitance when the secondary is shorted. + + :param c_vec: the calculated capacitance from the simulation. + :type c_vec: byte array + """ + c_1, c_2, c_3, c_4, c_5, c_6, c_7, c_8, c_9, c_10 = c_vec + num = c_3 * c_4 * c_7 + c_3 * c_4 * c_8 + c_3 * c_4 * c_9 + c_3 * c_6 * c_7 + c_4 * c_5 * c_7 + c_3 * c_4 * c_10 + \ + c_3 * c_6 * c_8 + c_4 * c_5 * c_8 + c_3 * c_6 * c_9 + c_3 * c_7 * c_8 + c_4 * c_5 * c_9 + c_5 * c_6 * c_7 + \ + c_3 * c_6 * c_10 + c_3 * c_7 * c_9 + c_4 * c_5 * c_10 + c_4 * c_7 * c_8 + c_5 * c_6 * c_8 + c_3 * c_7 * c_10 + \ + c_5 * c_6 * c_9 + c_5 * c_7 * c_8 + c_4 * c_8 * c_9 + c_5 * c_6 * c_10 + c_5 * c_7 * c_9 + c_6 * c_7 * c_8 + \ + c_4 * c_8 * c_10 + c_5 * c_7 * c_10 + c_6 * c_8 * c_9 + c_6 * c_8 * c_10 + c_7 * c_8 * c_9 + c_7 * c_8 * c_10 + den = c_3 * c_7 + c_3 * c_8 + c_4 * c_7 + c_3 * c_9 + c_4 * c_8 + c_5 * c_7 + c_3 * c_10 + c_4 * c_9 + c_5 * c_8 + \ + c_6 * c_7 + c_4 * c_10 + c_5 * c_9 + c_6 * c_8 + c_5 * c_10 + c_6 * c_9 + c_6 * c_10 + c_7 * c_9 + c_7 * c_10 + \ + c_8 * c_9 + c_8 * c_10 + c_sim_short = (c_1 + (num / den)) + return c_sim_short + +def compare_and_plot_connection_capacitance_of_transformer(c_vec: np.ndarray, measured_capacitance: list[float | None] | None = None, show_plot: bool = True): + """ + Compare the connection capacitance applied in the measurement. The capacitors C1,...C10 can not be compared directly to the measurement results. + + For every connection, we look to the behavior of the equivalent circuit. For example; AB vs CDE will result in C3 + C4 + C5 + C6 + C7 + C8 + :param c_vec: the calculated capacitance from the simulation. + :type c_vec: byte array + :param measured_capacitance: represent the measured capacitance of all the connections + :type measured_capacitance: list[float | None] + :param show_plot: to show the comparison between the simulation and measurement results in a figure. + :type show_plot: bool + """ + connection_keys = [ + 'C_ABvsCDE', 'C_ABCDvsE', 'C_ABEvsCD', 'C_AvsBCDE', 'C_BvsACDE', + 'C_CvsABDE', 'C_DvsABCE', 'C_ACvsBDE', 'C_ADvsBCE', 'C_BC_ADE' + ] + connection_sums = { + 'C_ABvsCDE': lambda C: C[2] + C[3] + C[4] + C[5] + C[6] + C[7], + 'C_ABCDvsE': lambda C: C[6] + C[7] + C[8] + C[9], + 'C_ABEvsCD': lambda C: C[2] + C[3] + C[4] + C[5] + C[8] + C[9], + 'C_AvsBCDE': lambda C: C[0] + C[3] + C[5] + C[6], + 'C_BvsACDE': lambda C: C[0] + C[2] + C[4] + C[7], + 'C_CvsABDE': lambda C: C[1] + C[3] + C[4] + C[8], + 'C_DvsABCE': lambda C: C[1] + C[2] + C[5] + C[9], + 'C_ACvsBDE': lambda C: C[0] + C[1] + C[4] + C[5] + C[6] + C[8], + 'C_ADvsBCE': lambda C: C[0] + C[1] + C[2] + C[3] + C[6] + C[9], + 'C_BC_ADE': lambda C: C[0] + C[1] + C[2] + C[3] + C[7] + C[8], + } + + if measured_capacitance is not None: + if len(measured_capacitance) != 10: + raise ValueError("measured_capacitances must be a sequence of 10 numbers (float or None).") + + # build simulated connection sums + sim_sums = [connection_sums[k](c_vec) for k in connection_keys] + + logger.info("\n--- Simulated vs Measured Capacitance (pF) -------------") + logger.info(f"{'Connection':<12}{'Measured':>12}{'Simulated':>12}{'Error %':>10}") + logger.info("-" * 46) + + # prepare data for optional plot + idx_used, meas_pf_used, calc_pf_used, ratio_used = [], [], [], [] + + for i, (k, meas, sim) in enumerate(zip(connection_keys, + measured_capacitance, + sim_sums)): + sim_pf = sim * 1e12 + if meas is None or (isinstance(meas, float) and np.isnan(meas)): + logger.info(f"{k:<12}{'---':>12}{sim_pf:12.2f}{'---':>10}") + # still plot simulated value + idx_used.append(i) + meas_pf_used.append(None) + calc_pf_used.append(sim_pf) + ratio_used.append(None) + else: + meas_pf = meas * 1e12 + error_percentage = 100 * (sim - meas) / meas + logger.info(f"{k:<12}{meas_pf:12.2f}{sim_pf:12.2f}{error_percentage:10.2f}") + + idx_used.append(i) + meas_pf_used.append(meas_pf) + calc_pf_used.append(sim_pf) + ratio_used.append(sim_pf / meas_pf) + + if show_plot: + + idx = np.arange(10) + plt.figure(figsize=(14, 6)) + + # plot all simulated points + plt.scatter(idx, [s for s in calc_pf_used], + label="Simulated", color="C0", marker="o") + + # plot only measured values that exist + idx_meas = [i for i, m in zip(idx_used, meas_pf_used) if m is not None] + meas_pf_ok = [m for m in meas_pf_used if m is not None] + plt.scatter(idx_meas, meas_pf_ok, + label="Measured", color="C3", marker="x") + + # annotate ratio where both values exist + for i, sim_pf, ratio in zip(idx_used, calc_pf_used, ratio_used): + if ratio is not None: + plt.text(i, sim_pf, f"{ratio:.2f}×", + ha="center", va="bottom", fontsize=8) + + label_txt = [k.replace('vs', ' vs ') for k in connection_keys] + plt.xticks(idx, label_txt, rotation=45, ha="right") + plt.xlabel("Capacitance Connections") + plt.ylabel("Capacitance / pF]") + plt.title("Simulated vs Measured Capacitance") + plt.grid(True, linestyle=":") + plt.legend() + plt.tight_layout() + plt.show() + +def plot_open_and_short_comparison(c_sim_open: float, c_sim_short: float, c_meas_open: float | None, c_meas_short: float | None): + """ + Plot horizontal bar comparison for open-circuit capacitance and short-circuit capacitance. + + :param c_sim_open: simulated open‑circuit capacitance (F) + :param c_sim_short: simulated short‑circuit capacitance (F) + :param c_meas_open: measured open‑circuit capacitance (F) or None + :param c_meas_short: measured short‑circuit capacitance (F) or None + """ + labels, sim_bar, meas_bar = [], [], [] + if c_meas_open is not None: + labels.append("A‑B (CD open)") + sim_bar.append(c_sim_open * 1e12) + meas_bar.append(c_meas_open * 1e12) + if c_meas_short is not None: + labels.append("A‑B (CD short)") + sim_bar.append(c_sim_short * 1e12) + meas_bar.append(c_meas_short * 1e12) + + if not labels: + return # nothing to plot + + y = np.arange(len(labels)) + h = 0.3 + plt.figure(figsize=(10, 3.5)) + plt.barh(y - h/2, sim_bar, height=h, color='tab:blue', label="Simulated") + plt.barh(y + h/2, meas_bar, height=h, color='tab:red', label="Measured") + + for i, (s, m) in enumerate(zip(sim_bar, meas_bar)): + if m != 0: + plt.text(s * 1.01, i - h/2, f"{s/m:.2f}×", va='center', fontsize=9, color='blue') + + plt.yticks(y, labels) + plt.xlabel("Capacitance / pF") + plt.grid(axis='x', linestyle='--', alpha=0.6) + plt.title("Simulated vs Measured Open / Short Capacitance") + plt.legend() + plt.tight_layout() + plt.show() + + +def close_excel_file_if_open(filepath): + """ + Close the specified Excel file if it is currently open. + + :param filepath: The path to the Excel file to close. + :type filepath: str + """ + # Get the absolute path + filepath = os.path.abspath(filepath) + + try: + excel = win32com.client.Dispatch("Excel.Application") + for workbook in excel.Workbooks: + if workbook.FullName.lower() == filepath.lower(): + workbook.Close(SaveChanges=False) + return + excel.Quit() + except Exception as e: + print(f"Unable to close Excel. Error: {e}") + +def json_to_excel(json_file_path: str, output_excel_path: str) -> None: + """ + Extract data from the electrostatic simulation and write it into a log file. + + :param json_file_path: Path to the JSON input file containing simulation results. + :type json_file_path: str + :param output_excel_path: Path where the Excel (.xlsx) file will be saved. + :type output_excel_path: str + """ + # Trying to close the Excel file if it's open + close_excel_file_if_open(output_excel_path) + # Load the JSON data from the file + with open(json_file_path, 'r') as json_file: + data = json.load(json_file) + + # Prepare the different data sections + charges_data = [] + energy_data = [] + average_voltages_data = [] + capacitances_within_data = [] + capacitances_between_data = [] + capacitances_between_turns_core_data = [] + + # Extract charges + charge_value = data.get("charges", None) + if charge_value is not None: + charges_data.append({"Charge Type": "Total Charge", "Value (Coulombs)": charge_value}) + + # Extract energy + for key, value in data.get("energy", {}).items(): + energy_data.append({"Energy Type": key, "Value (Joules)": value}) + + # Extract average voltages + for region, voltage in data.get("average_voltages", {}).items(): + average_voltages_data.append({"Region": region, "Average Voltage (V)": voltage}) + + # Extract capacitance within windings + for winding, turns in data.get("capacitances", {}).get("within_winding", {}).items(): + for turn, connections in turns.items(): + for target_turn, capacitance_value in connections.items(): + capacitances_within_data.append({ + "Winding": winding, + "Turn": turn, + "To Turn": target_turn, + "Capacitance (F)": capacitance_value + }) + + # Extract capacitance between windings + for winding1, windings in data.get("capacitances", {}).get("between_windings", {}).items(): + for winding2, turns in windings.items(): + for turn1, connections in turns.items(): + for turn2, capacitance_value in connections.items(): + capacitances_between_data.append({ + "Winding 1": winding1, + "Turn 1": turn1, + "Winding 2": winding2, + "Turn 2": turn2, + "Capacitance (F)": capacitance_value + }) + # Extract capacitance between turns and core + for winding, turns in data.get("capacitances", {}).get("between_turns_core", {}).items(): + for turn, capacitance_value in turns.items(): + capacitances_between_turns_core_data.append({ + "Winding": winding, + "Turn": turn, + "Capacitance to Core (F)": capacitance_value + }) + + # Create DataFrames for each section + charges_df = pd.DataFrame(charges_data) + energy_df = pd.DataFrame(energy_data) + average_voltages_df = pd.DataFrame(average_voltages_data) + capacitances_within_df = pd.DataFrame(capacitances_within_data) + capacitances_between_df = pd.DataFrame(capacitances_between_data) + capacitances_between_turns_core_df = pd.DataFrame(capacitances_between_turns_core_data) + + # Write to Excel file with multiple sheets + with pd.ExcelWriter(output_excel_path) as writer: + if not charges_df.empty: + charges_df.to_excel(writer, sheet_name='Charges', index=False) + worksheet = writer.sheets['Charges'] + worksheet.set_column('A:B', 30) + if not energy_df.empty: + energy_df.to_excel(writer, sheet_name='Energy', index=False) + worksheet = writer.sheets['Energy'] + worksheet.set_column('A:B', 30) + if not average_voltages_df.empty: + average_voltages_df.to_excel(writer, sheet_name='Average_Voltages', index=False) + worksheet = writer.sheets['Average_Voltages'] + worksheet.set_column('A:E', 30) + if not capacitances_within_df.empty: + capacitances_within_df.to_excel(writer, sheet_name='Capacitances_Within', index=False) + worksheet = writer.sheets['Capacitances_Within'] + worksheet.set_column('A:D', 30) + if not capacitances_between_df.empty: + capacitances_between_df.to_excel(writer, sheet_name='Capacitances_Between', index=False) + worksheet = writer.sheets['Capacitances_Between'] + worksheet.set_column('A:E', 30) + if not capacitances_between_turns_core_df.empty: + capacitances_between_turns_core_df.to_excel(writer, sheet_name='Turns_Core', index=False) + worksheet = writer.sheets['Turns_Core'] + worksheet.set_column('A:C', 30) + +def compare_excel_files(femmt_excel_path: str, femm_excel_path: str, comparison_output_path: str) -> None: + """ + Compare two Excel files (FEMMT and FEMM) and generate a new Excel file with comparison results. + + This function loads two Excel files, one generated by FEMMT and the other by FEMM, compares the data across + all common sheets, and calculates the differences between corresponding values. The results include: + - Absolute Difference + - Relative Error + - Relative Error Percentage + + The comparison is saved into a new Excel file, with each comparison in a separate sheet named after + the original sheet with the "_Comparison" suffix. + + :param femmt_excel_path: Path to the Excel file generated by FEMMT. + :type femmt_excel_path: str + :param femm_excel_path: Path to the Excel file generated by FEMM. + :type femm_excel_path: str + :param comparison_output_path: Path to save the resulting comparison Excel file. + :type comparison_output_path: str + """ + # Trying to close the Excel file if it's open + close_excel_file_if_open(comparison_output_path) + # Load both Excel files, get all sheets + femmt_sheets = pd.read_excel(femmt_excel_path, sheet_name=None) + femm_sheets = pd.read_excel(femm_excel_path, sheet_name=None) + + # Define sheets to compare + sheets_to_compare = femmt_sheets.keys() + + # Create an Excel writer for the output + with pd.ExcelWriter(comparison_output_path, engine='xlsxwriter') as writer: + # Iterate through each sheet to compare + for sheet_name in sheets_to_compare: + if sheet_name in femm_sheets: + # Load DataFrames for the current sheet + femmt_df = femmt_sheets[sheet_name] + femm_df = femm_sheets[sheet_name] + # Rename columns (FEMMT and FEMM) + femmt_df.columns = [f"{col}_FEMMT" for col in femmt_df.columns] + femm_df.columns = [f"{col}_FEMM" for col in femm_df.columns] + + # Concatenate both DataFrames side by side + comparison_df = pd.concat([femmt_df, femm_df], axis=1) + + # Calculating difference, relative error, and relative error in percentage for columns + for femmt_col, femm_col in zip(femmt_df.columns, femm_df.columns): + col_name = femmt_col.replace("_FEMMT", "") + if np.issubdtype(comparison_df[femmt_col].dtype, np.number) and np.issubdtype(comparison_df[femm_col].dtype, np.number): + comparison_df[f"{col_name}_Difference"] = comparison_df[femmt_col] - comparison_df[femm_col] + comparison_df[f"{col_name}_Relative_Error"] = comparison_df[f"{col_name}_Difference"] / comparison_df[femmt_col].replace(0, np.nan) + comparison_df[f"{col_name}_Error_Percent"] = comparison_df[f"{col_name}_Relative_Error"] * 100 + + # Writing to the Excel output file + comparison_df.to_excel(writer, sheet_name=f"{sheet_name}_Comparison", index=False) + worksheet = writer.sheets[f"{sheet_name}_Comparison"] + worksheet.set_column('A:Z', 35) + if __name__ == '__main__': pass diff --git a/femmt/mesh.py b/femmt/mesh.py index 3f372721..7b36ea5e 100644 --- a/femmt/mesh.py +++ b/femmt/mesh.py @@ -18,7 +18,7 @@ # Local libraries import femmt.functions as ff -from femmt.enumerations import ComponentType, ConductorType, WindingType, CoreType, Verbosity +from femmt.enumerations import SimulationType, ComponentType, ConductorType, WindingType, CoreType, Verbosity from femmt.data import FileData from femmt.model import Conductor, Core, StrayPath, AirGaps, Insulation, WindingWindow from femmt.drawing import TwoDaxiSymmetric @@ -32,6 +32,7 @@ class Mesh: core: Core stray_path: StrayPath insulation: Insulation + simulation_type: SimulationType component_type: ComponentType windings: list[Conductor] winding_windows: list[WindingWindow] @@ -81,6 +82,7 @@ def __init__(self, model: TwoDaxiSymmetric, windings: list[Conductor], winding_w self.core = model.core self.stray_path = model.stray_path self.insulation = model.insulation + self.simulation_type = model.simulation_type self.component_type = model.component_type self.windings = windings self.air_gaps = model.air_gaps @@ -106,7 +108,11 @@ def set_empty_point_lists(self): p_cond.append([]) p_region = [] p_iso_core = [] - return p_core, p_island, p_cond, p_region, p_iso_core + p_iso_layer = [] + p_iso_cond = [] + for _ in range(len(self.windings)): + p_iso_cond.append([]) + return p_core, p_island, p_cond, p_region, p_iso_core, p_iso_cond, p_iso_layer def set_empty_line_lists(self): """Initialize line lists. For internal overview as a mirrored gmsh information set.""" @@ -121,21 +127,34 @@ def set_empty_line_lists(self): l_region = [] l_air_gaps_air = [] l_iso_core = [] - return l_bound_core, l_bound_air, l_core_air, l_cond, l_region, l_air_gaps_air, l_iso_core, l_core_core + l_iso_layer = [] + l_iso_cond = [] + for _ in range(len(self.windings)): + l_iso_cond.append([]) + return l_bound_core, l_bound_air, l_core_air, l_cond, l_region, l_air_gaps_air, l_iso_core, l_core_core, l_iso_cond, l_iso_layer def set_empty_curve_loop_lists(self): """Initialize curve loop lists. For internal overview as a mirrored gmsh information set.""" # Curve Loops curve_loop_cond = [] + self.curve_loop_cond = [] for _ in range(len(self.windings)): curve_loop_cond.append([]) + self.curve_loop_cond.append([]) + #### + self.inner_cond_loop = [] + #### curve_loop_island = [] curve_loop_air = [] curve_loop_air_gaps = [] curve_loop_iso_core = [] + curve_loop_iso_layer = [] + curve_loop_iso_cond = [] + for _ in range(len(self.windings)): + curve_loop_iso_cond.append([]) # curve_loop_outer_air = [] # curve_loop_bound = [] - return curve_loop_cond, curve_loop_island, curve_loop_air, curve_loop_air_gaps, curve_loop_iso_core + return curve_loop_cond, curve_loop_island, curve_loop_air, curve_loop_air_gaps, curve_loop_iso_core, curve_loop_iso_cond, curve_loop_iso_layer def set_empty_plane_lists(self): """Initialize plane lists. For internal overview as a mirrored gmsh information set.""" @@ -154,7 +173,11 @@ def set_empty_plane_lists(self): self.plane_surface_outer_air = [] self.plane_surface_air_gaps = [] self.plane_surface_iso_core = [] + self.plane_surface_iso_layer = [] self.plane_surface_iso_pri_sec = [] + self.plane_surface_iso_cond = [] + for _ in range(len(self.windings)): + self.plane_surface_iso_cond.append([]) def single_core(self, p_core: list, p_island: list, @@ -697,7 +720,7 @@ def conductors(self, p_cond: list, l_cond: list, curve_loop_cond: list): self.model.p_conductor[num][i][1], 0, self.model.p_conductor[num][i][3]) - + if self.windings[num].conductor_type in [ConductorType.RoundLitz, ConductorType.RoundSolid]: p_cond[num].append(point) elif self.windings[num].conductor_type == ConductorType.RectangularSolid: @@ -707,7 +730,7 @@ def conductors(self, p_cond: list, l_cond: list, curve_loop_cond: list): current_center_points.append(point) else: raise Exception(f"ConductorType {self.windings[num].conductor_type} is not implemented") - + p_cond_center.append(current_center_points) # Curves of Conductors @@ -738,6 +761,7 @@ def conductors(self, p_cond: list, l_cond: list, curve_loop_cond: list): l_cond[num][i * 4 + 3]])) self.plane_surface_cond[num].append( gmsh.model.geo.addPlaneSurface([curve_loop_cond[num][i]])) + # self.store_curve_conds.append(curve_loop_cond[num]) elif self.windings[num].conductor_type == ConductorType.RectangularSolid: # Rectangle conductor cut for i in range(int(len(p_cond[num]) / 4)): @@ -756,7 +780,8 @@ def conductors(self, p_cond: list, l_cond: list, curve_loop_cond: list): l_cond[num][i * 4 + 3]])) self.plane_surface_cond[num].append( gmsh.model.geo.addPlaneSurface([curve_loop_cond[num][i]])) - + self.curve_loop_cond = curve_loop_cond + gmsh.model.geo.synchronize() # Embed center points so the mesh will adapt to it @@ -768,10 +793,10 @@ def insulations_core_cond(self, p_iso_core: list) -> list: """ Set the rectangular electrical insulation between conductors and core. - :param p_iso_core: + :param p_iso_core: Insulation points of the bobbin. :type p_iso_core: list - :return: + :return: List of curve-loop identifiers for each insulation rectangle. :rtype: list """ # Insulations @@ -798,7 +823,113 @@ def insulations_core_cond(self, p_iso_core: list) -> list: self.plane_surface_iso_core.append(gmsh.model.geo.addPlaneSurface([cl])) return curve_loop_iso_core - def air_single(self, l_core_air: list, l_air_gaps_air: list, curve_loop_air: list, curve_loop_cond: list, curve_loop_iso_core: list): + def insulation_between_layers(self, p_iso_layer: list): + """ + Set the rectangular electrical insulation between layers. + + :param p_iso_layer: Points of the insulation between the layers of turns. + :type p_iso_layer: List + + :return: List of curve-loop. + :rtype: list + """ + for iso in self.model.p_iso_layer: + p_iso = [] + for i in iso: + p_iso.append(gmsh.model.geo.addPoint(i[0], i[1], i[2], i[3])) + p_iso_layer.append(p_iso) + + # Lines + l_iso_layer = [[gmsh.model.geo.addLine(iso[i], iso[(i + 1) % 4]) for i in range(4)] for iso in p_iso_layer] + + # Curve loop and surface + curve_loop_iso_layer = [] + self.plane_surface_iso_layer = [] + for iso in l_iso_layer: + cl = gmsh.model.geo.addCurveLoop(iso) + curve_loop_iso_layer.append(cl) + self.plane_surface_iso_core.append(gmsh.model.geo.addPlaneSurface([cl])) + return curve_loop_iso_layer + + def conductor_insulation(self, p_iso_cond: list, l_iso_cond: list, curve_loop_iso_cond: list): + """ + Generate the insulation around each conductor and subtract it from the conductor. + + :param p_iso_cond: List of insulation points (Grouped by turns) + :type p_iso_cond: list + :param l_iso_cond: List of insulation curves + :type l_iso_cond: list + :param curve_loop_iso_cond: List of insulation curve loops + :type curve_loop_iso_cond: list + """ + if not self.insulation.add_turn_insulations: + logger.info("Turn insulation drawing skipped.") + return [] + p_iso_cond_center = [] + # points of conductor insulation + for num in range(len(self.windings)): + if self.windings[num].conductor_type in [ConductorType.RoundLitz, ConductorType.RoundSolid]: + current_center_points = [] # Center points to be embedded later + # Process insulation points for each conductor turn + for i in range(self.model.p_iso_conductor[num].shape[0]): + point = gmsh.model.geo.addPoint( + self.model.p_iso_conductor[num][i][0], + self.model.p_iso_conductor[num][i][1], + 0, + self.model.p_iso_conductor[num][i][3]) + + if self.windings[num].conductor_type in [ConductorType.RoundLitz, ConductorType.RoundSolid]: + p_iso_cond[num].append(point) + + p_iso_cond_center.append(current_center_points) + + # Curves of Conductor insulation + # if self.windings[num].conductor_type in [ConductorType.RoundLitz, ConductorType.RoundSolid]: + # Round conductor + for i in range(int(len(p_iso_cond[num]) / 5)): + l_iso_cond[num].append(gmsh.model.geo.addCircleArc( + p_iso_cond[num][5 * i + 1], + p_iso_cond[num][5 * i + 0], + p_iso_cond[num][5 * i + 2])) + l_iso_cond[num].append(gmsh.model.geo.addCircleArc( + p_iso_cond[num][5 * i + 2], + p_iso_cond[num][5 * i + 0], + p_iso_cond[num][5 * i + 3])) + l_iso_cond[num].append(gmsh.model.geo.addCircleArc( + p_iso_cond[num][5 * i + 3], + p_iso_cond[num][5 * i + 0], + p_iso_cond[num][5 * i + 4])) + l_iso_cond[num].append(gmsh.model.geo.addCircleArc( + p_iso_cond[num][5 * i + 4], + p_iso_cond[num][5 * i + 0], + p_iso_cond[num][5 * i + 1])) + # Iterative plane creation + curve_loop_iso_cond[num].append(gmsh.model.geo.addCurveLoop([ + l_iso_cond[num][i * 4 + 0], + l_iso_cond[num][i * 4 + 1], + l_iso_cond[num][i * 4 + 2], + l_iso_cond[num][i * 4 + 3]])) + # self.plane_surface_iso_cond[num].append( + # gmsh.model.geo.addPlaneSurface([curve_loop_iso_cond[num][i]])) + self.plane_surface_iso_cond[num].append( + gmsh.model.geo.addPlaneSurface([ + curve_loop_iso_cond[num][i], # Outer loop: insulation + self.curve_loop_cond[num][i]] # Inner loop: conductor + ) + ) + self.curve_loop_iso_cond = curve_loop_iso_cond + # TODO Add logic for rectanguölarsolid. + # else: + # raise Exception(f"ConductorType {self.windings[num].conductor_type} is not implemented") + # Synchronize the GMSH model + gmsh.model.geo.synchronize() + + # Print success message + logger.info("Insulation around conductors drawn successfully.") + return curve_loop_iso_cond + + def air_single(self, l_core_air: list, l_air_gaps_air: list, curve_loop_air: list, curve_loop_cond: list, curve_loop_iso_core: list, + curve_loop_iso_cond: list, curve_loop_iso_layer: list): """ Generate gmsh entities (points, lines, closed loops and planes) and draw the air gaps for the single core. @@ -812,6 +943,10 @@ def air_single(self, l_core_air: list, l_air_gaps_air: list, curve_loop_air: lis :type curve_loop_air: list :param curve_loop_iso_core: closed loop for core :type curve_loop_iso_core: list + :param curve_loop_iso_cond: insulation of conductors + :type curve_loop_iso_cond: list + :param curve_loop_iso_layer: insulation between the layer of turns + :type curve_loop_iso_layer: list """ # Air # Points are partwise double designated @@ -855,10 +990,13 @@ def air_single(self, l_core_air: list, l_air_gaps_air: list, curve_loop_air: lis # Need flatten list of all! conductors flatten_curve_loop_cond = [j for sub in curve_loop_cond for j in sub] + # Need flatten list of all! conductor insulation + flatten_curve_loop_iso_cond = [j for sub in curve_loop_iso_cond for j in sub] # The first curve loop represents the outer bounds: self.curve_loop_air (should only contain one element) # The other curve loops represent holes in the surface -> For each conductor as well as each insulation - self.plane_surface_air.append(gmsh.model.geo.addPlaneSurface(curve_loop_air + flatten_curve_loop_cond + curve_loop_iso_core)) + self.plane_surface_air.append(gmsh.model.geo.addPlaneSurface(curve_loop_air + flatten_curve_loop_cond + curve_loop_iso_core + \ + curve_loop_iso_layer + flatten_curve_loop_iso_cond)) # if curve_loop_iso_core is not None: # self.plane_surface_air.append( @@ -1159,9 +1297,10 @@ def generate_hybrid_mesh(self, color_scheme: dict = ff.colors_femmt_default, col gmsh.clear() # Initialization self.set_empty_plane_lists() - p_core, p_island, p_cond, p_region, p_iso_core = self.set_empty_point_lists() - l_bound_core, l_bound_air, l_core_air, l_cond, l_region, l_air_gaps_air, l_iso_core, l_core_core = self.set_empty_line_lists() - curve_loop_cond, curve_loop_island, curve_loop_air, curve_loop_air_gaps, curve_loop_iso_core = self.set_empty_curve_loop_lists() + p_core, p_island, p_cond, p_region, p_iso_core, p_iso_cond, p_iso_layer = self.set_empty_point_lists() + l_bound_core, l_bound_air, l_core_air, l_cond, l_region, l_air_gaps_air, l_iso_core, l_core_core, l_iso_cond, l_iso_layer = self.set_empty_line_lists() + curve_loop_cond, curve_loop_island, curve_loop_air, curve_loop_air_gaps, curve_loop_iso_core, curve_loop_iso_cond, curve_loop_iso_layer = ( + self.set_empty_curve_loop_lists()) # Set path for storing the mesh file gmsh.model.add(os.path.join(self.e_m_mesh_file, "geometry")) @@ -1188,10 +1327,15 @@ def generate_hybrid_mesh(self, color_scheme: dict = ff.colors_femmt_default, col if model_insulation: curve_loop_iso_core = self.insulations_core_cond(p_iso_core) + # layer insulation + curve_loop_iso_layer = self.insulation_between_layers(p_iso_layer) + # insulation of conductor + # if self.insulation.add_turn_insulations: + curve_loop_iso_cond = self.conductor_insulation(p_iso_cond, l_iso_cond, curve_loop_iso_cond) # Define mesh for air if self.core.core_type == CoreType.Single: - self.air_single(l_core_air, l_air_gaps_air, curve_loop_air, curve_loop_cond, curve_loop_iso_core) + self.air_single(l_core_air, l_air_gaps_air, curve_loop_air, curve_loop_cond, curve_loop_iso_core, curve_loop_iso_cond, curve_loop_iso_layer) if self.core.core_type == CoreType.Stacked: self.air_stacked(l_core_air, l_bound_air, curve_loop_cond) @@ -1230,10 +1374,15 @@ def generate_electro_magnetic_mesh(self, refine: int = 0): logger.info("Electro Magnetic Mesh Generation in Gmsh (write physical entities)") self.PN_BOUND = 111111 + # Needed for electrostatic simulation + self.PN_BOUND_LEFT = 111112 self.PN_AIR = 110000 + self.PN_Insulation_Bobbin = 110010 + self.PN_Insulation_Layer = 110050 self.PN_CORE = 120000 self.PN_COND_SOLID = 130000 self.PN_ROUND_LITZ = 150000 + self.PN_Insulation_Cond = 2000000 gmsh.open(self.model_geo_file) @@ -1284,21 +1433,67 @@ def set_physical_surface_conductor(): set_physical_surface_conductor() + def set_physical_surface_cond_insulation(): + if self.simulation_type == SimulationType.ElectroStatic: + self.ps_insulation_cond = [] + # + # # Add insulation planes to the physical group + # tags = [] + # for num in range(len(self.windings)): + # tags.extend(self.plane_surface_iso_cond[num]) + for winding_number in range(len(self.windings)): + tags = self.plane_surface_iso_cond[winding_number] + # # Create a physical group for all insulation planes + self.ps_insulation_cond = gmsh.model.geo.addPhysicalGroup(2, tags, tag=self.PN_Insulation_Cond + 1000 * winding_number) + set_physical_surface_cond_insulation() + def set_physical_surface_air(): - if self.model.core.core_type == CoreType.Single: - # These three areas self.plane_surface_air + self.plane_surface_air_gaps + self.plane_surface_iso_core - # must be - air_and_air_gaps = self.plane_surface_air + self.plane_surface_air_gaps + self.plane_surface_iso_core - self.ps_air = gmsh.model.geo.addPhysicalGroup(2, air_and_air_gaps, tag=self.PN_AIR) - # ps_air_ext = gmsh.model.geo.addPhysicalGroup(2, plane_surface_outer_air, tag=1001) - elif self.model.core.core_type == CoreType.Stacked: - air_total = self.plane_surface_air_bot + self.plane_surface_air_top - self.ps_air = gmsh.model.geo.addPhysicalGroup(2, air_total, tag=self.PN_AIR) + if self.simulation_type == SimulationType.ElectroStatic: + if self.model.core.core_type == CoreType.Single: + # These three areas self.plane_surface_air + self.plane_surface_air_gaps + self.plane_surface_iso_core + # must be + air_and_air_gaps = self.plane_surface_air + self.plane_surface_air_gaps + self.ps_air = gmsh.model.geo.addPhysicalGroup(2, air_and_air_gaps, tag=self.PN_AIR) + bobbin = self.plane_surface_iso_core + self.ps_insulation = gmsh.model.geo.addPhysicalGroup(2, bobbin, tag=self.PN_Insulation_Bobbin) + self.ps_layer_insulation = gmsh.model.geo.addPhysicalGroup(2, self.plane_surface_iso_layer, tag=self.PN_Insulation_Layer) + + # ps_air_ext = gmsh.model.geo.addPhysicalGroup(2, plane_surface_outer_air, tag=1001) + elif self.model.core.core_type == CoreType.Stacked: + air_total = self.plane_surface_air_bot + self.plane_surface_air_top + self.ps_air = gmsh.model.geo.addPhysicalGroup(2, air_total, tag=self.PN_AIR) + # to do, after merging the branch into main, the insulation here should be defined + else: + if self.model.core.core_type == CoreType.Single: + # These three areas self.plane_surface_air + self.plane_surface_air_gaps + self.plane_surface_iso_core + # must be + self.ps_insulation_cond = [] + tags = [] + for num in range(len(self.windings)): + tags.extend(self.plane_surface_iso_cond[num]) + air_and_air_gaps = self.plane_surface_air + self.plane_surface_air_gaps + self.plane_surface_iso_core + tags + self.plane_surface_iso_layer + self.ps_air = gmsh.model.geo.addPhysicalGroup(2, air_and_air_gaps, tag=self.PN_AIR) + # ps_air_ext = gmsh.model.geo.addPhysicalGroup(2, plane_surface_outer_air, tag=1001) + elif self.model.core.core_type == CoreType.Stacked: + air_total = self.plane_surface_air_bot + self.plane_surface_air_top + self.ps_air = gmsh.model.geo.addPhysicalGroup(2, air_total, tag=self.PN_AIR) set_physical_surface_air() def set_physical_line_bound(): - self.pc_bound = gmsh.model.geo.addPhysicalGroup(1, self.l_bound_tmp, tag=self.PN_BOUND) + if self.simulation_type == SimulationType.ElectroStatic: + if self.core.core_type == CoreType.Single: + values_to_exclude = [3, 4, 5, 6, 7] + elif self.core.core_type == CoreType.Stacked: + values_to_exclude = [2, 3, 4, 5, 6] + + rest_of_values = [value for value in self.l_bound_tmp if value not in values_to_exclude] + self.pc_bound_left = gmsh.model.geo.addPhysicalGroup(1, rest_of_values, tag=self.PN_BOUND_LEFT) + + # Set the rest of the boundary as a separate physical group (excluding axisymmetric boundaries) + self.pc_bound = gmsh.model.geo.addPhysicalGroup(1, values_to_exclude, tag=self.PN_BOUND) + else: + self.pc_bound = gmsh.model.geo.addPhysicalGroup(1, self.l_bound_tmp, tag=self.PN_BOUND) set_physical_line_bound() diff --git a/femmt/model.py b/femmt/model.py index 170f60ae..f6d59e01 100644 --- a/femmt/model.py +++ b/femmt/model.py @@ -309,7 +309,6 @@ def __init__(self, # set r_outer, so cross-section of outer leg has same cross-section as inner leg # this is the default-case self.r_outer = fr.calculate_r_outer(self.core_inner_diameter, self.window_w) - # Material Parameters # General # Initialize database @@ -664,15 +663,23 @@ class Insulation: In general, it is not necessary to add an insulation object at all when no insulation is needed. """ - cond_cond: list[list[ - float]] # two-dimensional list with size NxN, where N is the number of windings (symmetrical isolation matrix) - core_cond: list[ - float] # list with size 4x1, with respectively isolation of cond_n -> [top_core, bot_core, left_core, right_core] - + conductor_type: ConductorType # it is needed here tempoarily + cond_cond: list[list[float]] # two-dimensional list with size NxN, where N is the number of windings (symmetrical isolation matrix) + core_cond: list[float] # list with size 4x1, with respectively isolation of cond_n -> [top_core, bot_core, left_core, right_core] + turn_ins: list[float] # list of turn insulation of every winding -> [turn_ins_of_winding_1, turn_ins_of_winding_2, ...] + cond_air_cond: list[list[float]] # two-dimensional list with size NxN, where N is the number of windings (symmetrical isolation matrix) + er_turn_insulation: list[float] + er_layer_insulation: float = None + er_bobbin: float = None + bobbin_dimensions: None + thickness_of_insulation: float + consistent_ins: bool = True + draw_insulation_between_layers: bool = True flag_insulation: bool = True + add_turn_insulations: bool = True max_aspect_ratio: float - def __init__(self, max_aspect_ratio: float = 10, flag_insulation: bool = True): + def __init__(self, max_aspect_ratio: float = 10, flag_insulation: bool = True, bobbin_dimensions: None = None): """Create an insulation object. Sets an insulation_delta value. In order to simplify the drawing of the isolations between core and winding window the isolation rectangles @@ -683,6 +690,13 @@ def __init__(self, max_aspect_ratio: float = 10, flag_insulation: bool = True): # If the gaps between insulations and core (or windings) are to big/small just change this value self.flag_insulation = flag_insulation self.max_aspect_ratio = max_aspect_ratio + self.bobbin_dimensions = bobbin_dimensions + # As there is a gap between the core and the bobbin, the definition of bobbin parameters is needed in electrostatic simulation + if bobbin_dimensions is not None: + self.bobbin_inner_diameter = bobbin_dimensions.bobbin_inner_diameter + self.bobbin_window_w = bobbin_dimensions.bobbin_window_w + self.bobbin_window_h = bobbin_dimensions.bobbin_window_h + self.bobbin_h = bobbin_dimensions.bobbin_h def set_flag_insulation(self, flag: bool): # to differentiate between the simulation with and without insulation """ @@ -693,21 +707,75 @@ def set_flag_insulation(self, flag: bool): # to differentiate between the simul """ self.flag_insulation = flag - def add_winding_insulations(self, inner_winding_insulation: list[list[float]]): - """Add insulations between turns of one winding and insulation between virtual winding windows. + def add_winding_insulations(self, inner_winding_insulation: list[list[float]], per_layer_of_turns: bool = False): + """Add a consistent insulation between turns of one winding and insulation between virtual winding windows. Insulation between virtual winding windows is not always needed. :param inner_winding_insulation: List of floats which represent the insulations between turns of the same winding. This does not correspond to the order conductors are added to the winding! Instead, the winding number is important. The conductors are sorted by ascending winding number. The lowest winding number therefore is combined with index 0. The second lowest with index 1 and so on. :type inner_winding_insulation: list[list[float]] + :param per_layer_of_turns: If it is enabled, the insulation will be added between turns for every layer in every winding. + :type per_layer_of_turns: bool. """ if inner_winding_insulation == [[]]: raise Exception("Inner winding insulations list cannot be empty.") self.cond_cond = inner_winding_insulation - def add_core_insulations(self, top_core: float, bot_core: float, left_core: float, right_core: float): + if per_layer_of_turns: + self.consistent_ins = False + else: + self.consistent_ins = True + + def add_turn_insulation(self, insulation_thickness: list[float], dielectric_constant: list[float] = None, add_turn_insulations: bool = False): + """Add insulation for turns in every winding. + + :param insulation_thickness: List of floats which represent the insulation around every winding. + :type insulation_thickness: list[[float]] + :param dielectric_constant: relative permittivity of the insulation of the winding + :type dielectric_constant list[[float]] + :param add_turn_insulations: bool to draw the insulation around turns + :type add_turn_insulations: bool + """ + self.add_turn_insulations = add_turn_insulations + # self.er_turn_insulation = dielectric_constant + + if dielectric_constant is not None: + self.er_turn_insulation = dielectric_constant + else: + self.er_turn_insulation = [1.0 for _ in insulation_thickness] + + if add_turn_insulations: + self.turn_ins = insulation_thickness + else: + # self.turn_ins = [] + self.turn_ins = [0.0 for _ in insulation_thickness] + + def add_insulation_between_layers(self, add_insulation_material: bool = True, thickness: float = 0.0, dielectric_constant: float = None): + """ + Add an insulation (thickness_of_insulation or tape insulation) between layers. + + :param add_insulation_material: show the drawing of the insulation between the layers of turns. If false, it is not drawn and it is an air by default + :type add_insulation_material: bool + :param thickness: the thickness of the insulation between the layers of turns + :type thickness: float + :param dielectric_constant: relative permittivity of the insulation between the layers + :type dielectric_constant: float + """ + if thickness <= 0: + raise ValueError("insulation thickness must be greater than zero.") + else: + self.thickness_of_insulation = thickness + + self.er_layer_insulation = dielectric_constant if dielectric_constant is not None else 1.0 + + if add_insulation_material: + self.draw_insulation_between_layers = True + else: + self.draw_insulation_between_layers = False + + def add_core_insulations(self, top_core: float, bot_core: float, left_core: float, right_core: float, dielectric_constant: float = None): """Add insulations between the core and the winding window. Creating those will draw real rectangles in the model. :param top_core: Insulation between winding window and top core @@ -718,6 +786,8 @@ def add_core_insulations(self, top_core: float, bot_core: float, left_core: floa :type left_core: float :param right_core: Insulation between winding window and right core :type right_core: float + :param dielectric_constant: relative permittivity of the core insulation + :type dielectric_constant: float """ if top_core is None: top_core = 0 @@ -728,6 +798,7 @@ def add_core_insulations(self, top_core: float, bot_core: float, left_core: floa if right_core is None: right_core = 0 + self.er_bobbin = dielectric_constant if dielectric_constant is not None else 1.0 self.core_cond = [top_core, bot_core, left_core, right_core] self.core_cond = [top_core, bot_core, left_core, right_core] @@ -1013,12 +1084,28 @@ def __init__(self, core: Core, insulations: Insulation, stray_path: StrayPath = self.core: Core = core self.stray_path: StrayPath = stray_path self.air_gaps: AirGaps = air_gaps + self.insulations: Insulation = insulations if self.core.core_type == CoreType.Single: - self.max_bot_bound = -core.window_h / 2 + insulations.core_cond[1] - self.max_top_bound = core.window_h / 2 - insulations.core_cond[0] - self.max_left_bound = core.core_inner_diameter / 2 + insulations.core_cond[2] - self.max_right_bound = core.r_inner - insulations.core_cond[3] + if self.insulations.bobbin_dimensions: + # top - bot + bobbin_height = self.insulations.bobbin_window_h + insulation_delta_top_bot = (self.core.window_h - bobbin_height) / 2 + # left + bobbin_inner_radius = self.insulations.bobbin_inner_diameter / 2 + core_inner_radius = self.core.core_inner_diameter / 2 + insulation_delta_left = bobbin_inner_radius - core_inner_radius + # insulation_delta_left = 3e-4 + # dimensions + self.max_bot_bound = -core.window_h / 2 + insulations.core_cond[1] + insulation_delta_top_bot + self.max_top_bound = core.window_h / 2 - insulations.core_cond[0] - insulation_delta_top_bot + self.max_left_bound = (core.core_inner_diameter / 2 + insulations.core_cond[2] + insulation_delta_left + 1.5e-5) + self.max_right_bound = core.r_inner - insulations.core_cond[3] + else: + self.max_bot_bound = -core.window_h / 2 + insulations.core_cond[1] + self.max_top_bound = core.window_h / 2 - insulations.core_cond[0] + self.max_left_bound = core.core_inner_diameter / 2 + insulations.core_cond[2] + self.max_right_bound = core.r_inner - insulations.core_cond[3] elif self.core.core_type == CoreType.Stacked: # top, bot, left, right self.max_bot_bound = -core.window_h_bot / 2 + insulations.core_cond[1] self.max_top_bound = core.window_h_bot / 2 + core.window_h_top + core.core_thickness - insulations.core_cond[0] diff --git a/requirements.txt b/requirements.txt index 17e25185..d638a110 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pandas numpy matplotlib -gmsh>=4.13.1 +gmsh==4.13.1 onelab>=1.0 scipy pytest diff --git a/tests/integration/fixtures/geometry_inductor_electrostatic.json b/tests/integration/fixtures/geometry_inductor_electrostatic.json new file mode 100644 index 00000000..12562532 --- /dev/null +++ b/tests/integration/fixtures/geometry_inductor_electrostatic.json @@ -0,0 +1,93 @@ +{ + "core_type": "Single", + "p_outer": [ + [ + 0, + -0.018475 + ], + [ + 0.019943733351606967, + -0.018475 + ], + [ + 0, + 0.018475 + ], + [ + 0.019943733351606967, + 0.018475 + ] + ], + "p_ww": [ + [ + 0.00745, + -0.01475 + ], + [ + 0.018500000000000003, + -0.01475 + ], + [ + 0.00745, + 0.01475 + ], + [ + 0.018500000000000003, + 0.01475 + ] + ], + "p_air_gap_center": [ + [ + 0.003725, + 0.0 + ] + ], + "lengths_air_gap": [ + 0.001 + ], + "p_cond_center_1": [ + [ + 0.0093406, + -0.0128244, + 0.0, + 0.000143825 + ], + [ + 0.0093406, + -0.0091232, + 0.0, + 0.000143825 + ], + [ + 0.0093406, + -0.005422, + 0.0, + 0.000143825 + ], + [ + 0.0093406, + -0.0017207999999999993, + 0.0, + 0.000143825 + ], + [ + 0.0093406, + 0.001980400000000001, + 0.0, + 0.000143825 + ], + [ + 0.0093406, + 0.005681600000000002, + 0.0, + 0.000143825 + ], + [ + 0.0093406, + 0.009382800000000002, + 0.0, + 0.000143825 + ] + ], + "radius_cond_1": 0.0011505999999999999 +} diff --git a/tests/integration/fixtures/geometry_transformer_electrostatic.json b/tests/integration/fixtures/geometry_transformer_electrostatic.json new file mode 100644 index 00000000..28f79aea --- /dev/null +++ b/tests/integration/fixtures/geometry_transformer_electrostatic.json @@ -0,0 +1,174 @@ +{ + "core_type": "Single", + "p_outer": [ + [ + 0, + -0.018475 + ], + [ + 0.019943733351606967, + -0.018475 + ], + [ + 0, + 0.018475 + ], + [ + 0.019943733351606967, + 0.018475 + ] + ], + "p_ww": [ + [ + 0.00745, + -0.01475 + ], + [ + 0.018500000000000003, + -0.01475 + ], + [ + 0.00745, + 0.01475 + ], + [ + 0.018500000000000003, + 0.01475 + ] + ], + "p_air_gap_center": [ + [ + 0.003725, + 0.0 + ] + ], + "lengths_air_gap": [ + 0.0005 + ], + "p_cond_center_1": [ + [ + 0.009840599999999998, + -0.011674400000000001, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + -0.009173200000000001, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + -0.006672000000000001, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + -0.0041708000000000005, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + -0.0016696000000000003, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + 0.0008316, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + 0.0033328000000000003, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + 0.005834000000000001, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + 0.008335200000000001, + 0.0, + 0.000143825 + ], + [ + 0.009840599999999998, + 0.010836400000000001, + 0.0, + 0.000143825 + ] + ], + "radius_cond_1": 0.0011505999999999999, + "p_cond_center_2": [ + [ + 0.012641999999999999, + -0.011674400000000001, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + -0.0092782, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + -0.006882, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + -0.0044858, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + -0.0020895999999999996, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + 0.0003066000000000006, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + 0.002702800000000001, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + 0.005099000000000001, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + 0.007495200000000001, + 0.0, + 0.000143825 + ], + [ + 0.012641999999999999, + 0.009891400000000002, + 0.0, + 0.000143825 + ] + ], + "radius_cond_2": 0.0011505999999999999 +} diff --git a/tests/integration/fixtures/inductor_electrostatic.json b/tests/integration/fixtures/inductor_electrostatic.json new file mode 100644 index 00000000..9e935958 --- /dev/null +++ b/tests/integration/fixtures/inductor_electrostatic.json @@ -0,0 +1,88 @@ +{ + "charges": 1.845803214046562e-14, + "energy": { + "stored_air": 3.332584950199621e-12, + "stored_core": 0.0, + "stored_component": 3.332584950199621e-12 + }, + "average_voltages": { + "core": 0.0 + }, + "capacitances": { + "within_winding": { + "Winding_1": { + "Turn_1": { + "to_Turn_2": 2.399461164143728e-10, + "to_Turn_3": 5.99865291035932e-11, + "to_Turn_4": 2.666067960159697e-11, + "to_Turn_5": 1.499663227589829e-11, + "to_Turn_6": 9.597844656574906e-12, + "to_Turn_7": 6.665169900399241e-12 + }, + "Turn_2": { + "to_Turn_1": 2.399461164143728e-10, + "to_Turn_3": 2.399461164143728e-10, + "to_Turn_4": 5.998652910359316e-11, + "to_Turn_5": 2.666067960159697e-11, + "to_Turn_6": 1.499663227589829e-11, + "to_Turn_7": 9.597844656574906e-12 + }, + "Turn_3": { + "to_Turn_1": 5.99865291035932e-11, + "to_Turn_2": 2.399461164143728e-10, + "to_Turn_4": 2.399461164143724e-10, + "to_Turn_5": 5.998652910359316e-11, + "to_Turn_6": 2.666067960159695e-11, + "to_Turn_7": 1.499663227589829e-11 + }, + "Turn_4": { + "to_Turn_1": 2.666067960159697e-11, + "to_Turn_2": 5.998652910359316e-11, + "to_Turn_3": 2.399461164143724e-10, + "to_Turn_5": 2.399461164143728e-10, + "to_Turn_6": 5.998652910359316e-11, + "to_Turn_7": 2.666067960159697e-11 + }, + "Turn_5": { + "to_Turn_1": 1.499663227589829e-11, + "to_Turn_2": 2.666067960159697e-11, + "to_Turn_3": 5.998652910359316e-11, + "to_Turn_4": 2.399461164143728e-10, + "to_Turn_6": 2.399461164143724e-10, + "to_Turn_7": 5.998652910359316e-11 + }, + "Turn_6": { + "to_Turn_1": 9.597844656574906e-12, + "to_Turn_2": 1.499663227589829e-11, + "to_Turn_3": 2.666067960159695e-11, + "to_Turn_4": 5.998652910359316e-11, + "to_Turn_5": 2.399461164143724e-10, + "to_Turn_7": 2.399461164143728e-10 + }, + "Turn_7": { + "to_Turn_1": 6.665169900399241e-12, + "to_Turn_2": 9.597844656574906e-12, + "to_Turn_3": 1.499663227589829e-11, + "to_Turn_4": 2.666067960159697e-11, + "to_Turn_5": 5.998652910359316e-11, + "to_Turn_6": 2.399461164143728e-10 + } + } + }, + "between_windings": { + "Winding_1": {} + }, + "between_turns_core": { + "Winding_1": { + "Turn_1": 6.665169900399241e-12, + "Turn_2": 9.597844656574906e-12, + "Turn_3": 1.499663227589829e-11, + "Turn_4": 2.666067960159697e-11, + "Turn_5": 5.998652910359316e-11, + "Turn_6": 2.399461164143728e-10, + "Turn_7": Infinity + } + } + } +} + diff --git a/tests/integration/fixtures/transformer_electrostatic.json b/tests/integration/fixtures/transformer_electrostatic.json new file mode 100644 index 00000000..0547a9cf --- /dev/null +++ b/tests/integration/fixtures/transformer_electrostatic.json @@ -0,0 +1,515 @@ +{ + "charges": 4.437279912138427e-14, + "energy": { + "stored_air": 8.444114868823223e-12, + "stored_core": 0.0, + "stored_component": 8.444114868823223e-12 + }, + "average_voltages": { + "core": 0.0 + }, + "capacitances": { + "within_winding": { + "Winding_1": { + "Turn_1": { + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_2": { + "to_Turn_1": 1.367946608749361e-09, + "to_Turn_3": 1.367946608749364e-09, + "to_Turn_4": 3.419866521873409e-10, + "to_Turn_5": 1.519940676388181e-10, + "to_Turn_6": 8.549666304683513e-11, + "to_Turn_7": 5.47178643499745e-11, + "to_Turn_8": 3.799851690970451e-11, + "to_Turn_9": 2.791727772957883e-11, + "to_Turn_10": 2.137416576170878e-11 + }, + "Turn_3": { + "to_Turn_1": 3.419866521873405e-10, + "to_Turn_2": 1.367946608749364e-09, + "to_Turn_4": 1.367946608749364e-09, + "to_Turn_5": 3.419866521873405e-10, + "to_Turn_6": 1.51994067638818e-10, + "to_Turn_7": 8.549666304683513e-11, + "to_Turn_8": 5.471786434997448e-11, + "to_Turn_9": 3.799851690970451e-11, + "to_Turn_10": 2.791727772957882e-11 + }, + "Turn_4": { + "to_Turn_1": 1.519940676388181e-10, + "to_Turn_2": 3.419866521873409e-10, + "to_Turn_3": 1.367946608749364e-09, + "to_Turn_5": 1.367946608749361e-09, + "to_Turn_6": 3.419866521873402e-10, + "to_Turn_7": 1.51994067638818e-10, + "to_Turn_8": 8.54966630468351e-11, + "to_Turn_9": 5.471786434997448e-11, + "to_Turn_10": 3.799851690970449e-11 + }, + "Turn_5": { + "to_Turn_1": 8.549666304683513e-11, + "to_Turn_2": 1.519940676388181e-10, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.367946608749361e-09, + "to_Turn_6": 1.367946608749361e-09, + "to_Turn_7": 3.419866521873405e-10, + "to_Turn_8": 1.51994067638818e-10, + "to_Turn_9": 8.549666304683513e-11, + "to_Turn_10": 5.471786434997448e-11 + }, + "Turn_6": { + "to_Turn_1": 5.471786434997448e-11, + "to_Turn_2": 8.549666304683513e-11, + "to_Turn_3": 1.51994067638818e-10, + "to_Turn_4": 3.419866521873402e-10, + "to_Turn_5": 1.367946608749361e-09, + "to_Turn_7": 1.367946608749364e-09, + "to_Turn_8": 3.419866521873405e-10, + "to_Turn_9": 1.519940676388181e-10, + "to_Turn_10": 8.549666304683513e-11 + }, + "Turn_7": { + "to_Turn_1": 3.799851690970451e-11, + "to_Turn_2": 5.47178643499745e-11, + "to_Turn_3": 8.549666304683513e-11, + "to_Turn_4": 1.51994067638818e-10, + "to_Turn_5": 3.419866521873405e-10, + "to_Turn_6": 1.367946608749364e-09, + "to_Turn_8": 1.367946608749361e-09, + "to_Turn_9": 3.419866521873405e-10, + "to_Turn_10": 1.51994067638818e-10 + }, + "Turn_8": { + "to_Turn_1": 2.791727772957882e-11, + "to_Turn_2": 3.799851690970451e-11, + "to_Turn_3": 5.471786434997448e-11, + "to_Turn_4": 8.54966630468351e-11, + "to_Turn_5": 1.51994067638818e-10, + "to_Turn_6": 3.419866521873405e-10, + "to_Turn_7": 1.367946608749361e-09, + "to_Turn_9": 1.367946608749364e-09, + "to_Turn_10": 3.419866521873405e-10 + }, + "Turn_9": { + "to_Turn_1": 2.137416576170878e-11, + "to_Turn_2": 2.791727772957883e-11, + "to_Turn_3": 3.799851690970451e-11, + "to_Turn_4": 5.471786434997448e-11, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 1.519940676388181e-10, + "to_Turn_7": 3.419866521873405e-10, + "to_Turn_8": 1.367946608749364e-09, + "to_Turn_10": 1.367946608749361e-09 + }, + "Turn_10": { + "to_Turn_1": 1.688822973764645e-11, + "to_Turn_2": 2.137416576170878e-11, + "to_Turn_3": 2.791727772957882e-11, + "to_Turn_4": 3.799851690970449e-11, + "to_Turn_5": 5.471786434997448e-11, + "to_Turn_6": 8.549666304683513e-11, + "to_Turn_7": 1.51994067638818e-10, + "to_Turn_8": 3.419866521873405e-10, + "to_Turn_9": 1.367946608749361e-09 + } + }, + "Winding_2": { + "Turn_1": { + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_2": { + "to_Turn_1": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_3": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_4": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_5": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_6": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_7": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_8": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_9": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_10": Infinity + }, + "Turn_10": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity + } + } + }, + "between_windings": { + "Winding_1": { + "Winding_2": { + "Turn_1": { + "to_Turn_1": Infinity, + "to_Turn_2": Infinity, + "to_Turn_3": Infinity, + "to_Turn_4": Infinity, + "to_Turn_5": Infinity, + "to_Turn_6": Infinity, + "to_Turn_7": Infinity, + "to_Turn_8": Infinity, + "to_Turn_9": Infinity, + "to_Turn_10": Infinity + }, + "Turn_2": { + "to_Turn_1": 1.367946608749361e-09, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 1.367946608749361e-09, + "to_Turn_4": 1.367946608749361e-09, + "to_Turn_5": 1.367946608749361e-09, + "to_Turn_6": 1.367946608749361e-09, + "to_Turn_7": 1.367946608749361e-09, + "to_Turn_8": 1.367946608749361e-09, + "to_Turn_9": 1.367946608749361e-09, + "to_Turn_10": 1.367946608749361e-09 + }, + "Turn_3": { + "to_Turn_1": 3.419866521873405e-10, + "to_Turn_2": 3.419866521873405e-10, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 3.419866521873405e-10, + "to_Turn_5": 3.419866521873405e-10, + "to_Turn_6": 3.419866521873405e-10, + "to_Turn_7": 3.419866521873405e-10, + "to_Turn_8": 3.419866521873405e-10, + "to_Turn_9": 3.419866521873405e-10, + "to_Turn_10": 3.419866521873405e-10 + }, + "Turn_4": { + "to_Turn_1": 1.519940676388181e-10, + "to_Turn_2": 1.519940676388181e-10, + "to_Turn_3": 1.519940676388181e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 1.519940676388181e-10, + "to_Turn_6": 1.519940676388181e-10, + "to_Turn_7": 1.519940676388181e-10, + "to_Turn_8": 1.519940676388181e-10, + "to_Turn_9": 1.519940676388181e-10, + "to_Turn_10": 1.519940676388181e-10 + }, + "Turn_5": { + "to_Turn_1": 8.549666304683513e-11, + "to_Turn_2": 8.549666304683513e-11, + "to_Turn_3": 8.549666304683513e-11, + "to_Turn_4": 8.549666304683513e-11, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 8.549666304683513e-11, + "to_Turn_7": 8.549666304683513e-11, + "to_Turn_8": 8.549666304683513e-11, + "to_Turn_9": 8.549666304683513e-11, + "to_Turn_10": 8.549666304683513e-11 + }, + "Turn_6": { + "to_Turn_1": 5.471786434997448e-11, + "to_Turn_2": 5.471786434997448e-11, + "to_Turn_3": 5.471786434997448e-11, + "to_Turn_4": 5.471786434997448e-11, + "to_Turn_5": 5.471786434997448e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 5.471786434997448e-11, + "to_Turn_8": 5.471786434997448e-11, + "to_Turn_9": 5.471786434997448e-11, + "to_Turn_10": 5.471786434997448e-11 + }, + "Turn_7": { + "to_Turn_1": 3.799851690970451e-11, + "to_Turn_2": 3.799851690970451e-11, + "to_Turn_3": 3.799851690970451e-11, + "to_Turn_4": 3.799851690970451e-11, + "to_Turn_5": 3.799851690970451e-11, + "to_Turn_6": 3.799851690970451e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 3.799851690970451e-11, + "to_Turn_9": 3.799851690970451e-11, + "to_Turn_10": 3.799851690970451e-11 + }, + "Turn_8": { + "to_Turn_1": 2.791727772957882e-11, + "to_Turn_2": 2.791727772957882e-11, + "to_Turn_3": 2.791727772957882e-11, + "to_Turn_4": 2.791727772957882e-11, + "to_Turn_5": 2.791727772957882e-11, + "to_Turn_6": 2.791727772957882e-11, + "to_Turn_7": 2.791727772957882e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.791727772957882e-11, + "to_Turn_10": 2.791727772957882e-11 + }, + "Turn_9": { + "to_Turn_1": 2.137416576170878e-11, + "to_Turn_2": 2.137416576170878e-11, + "to_Turn_3": 2.137416576170878e-11, + "to_Turn_4": 2.137416576170878e-11, + "to_Turn_5": 2.137416576170878e-11, + "to_Turn_6": 2.137416576170878e-11, + "to_Turn_7": 2.137416576170878e-11, + "to_Turn_8": 2.137416576170878e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 2.137416576170878e-11 + }, + "Turn_10": { + "to_Turn_1": 1.688822973764645e-11, + "to_Turn_2": 1.688822973764645e-11, + "to_Turn_3": 1.688822973764645e-11, + "to_Turn_4": 1.688822973764645e-11, + "to_Turn_5": 1.688822973764645e-11, + "to_Turn_6": 1.688822973764645e-11, + "to_Turn_7": 1.688822973764645e-11, + "to_Turn_8": 1.688822973764645e-11, + "to_Turn_9": 1.688822973764645e-11, + "to_Turn_10": 1.688822973764645e-11 + } + } + }, + "Winding_2": { + "Winding_1": { + "Turn_1": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_2": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_3": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_4": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_5": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_6": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_7": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_8": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_9": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + }, + "Turn_10": { + "to_Turn_1": Infinity, + "to_Turn_2": 1.367946608749361e-09, + "to_Turn_3": 3.419866521873405e-10, + "to_Turn_4": 1.519940676388181e-10, + "to_Turn_5": 8.549666304683513e-11, + "to_Turn_6": 5.471786434997448e-11, + "to_Turn_7": 3.799851690970451e-11, + "to_Turn_8": 2.791727772957882e-11, + "to_Turn_9": 2.137416576170878e-11, + "to_Turn_10": 1.688822973764645e-11 + } + } + } + }, + "between_turns_core": { + "Winding_1": { + "Turn_1": 1.688822973764645e-11, + "Turn_2": 2.137416576170878e-11, + "Turn_3": 2.791727772957882e-11, + "Turn_4": 3.799851690970449e-11, + "Turn_5": 5.471786434997448e-11, + "Turn_6": 8.549666304683513e-11, + "Turn_7": 1.51994067638818e-10, + "Turn_8": 3.419866521873405e-10, + "Turn_9": 1.367946608749361e-09, + "Turn_10": Infinity + }, + "Winding_2": { + "Turn_1": 1.688822973764645e-11, + "Turn_2": 1.688822973764645e-11, + "Turn_3": 1.688822973764645e-11, + "Turn_4": 1.688822973764645e-11, + "Turn_5": 1.688822973764645e-11, + "Turn_6": 1.688822973764645e-11, + "Turn_7": 1.688822973764645e-11, + "Turn_8": 1.688822973764645e-11, + "Turn_9": 1.688822973764645e-11, + "Turn_10": 1.688822973764645e-11 + } + } + } +} diff --git a/tests/integration/test_femmt.py b/tests/integration/test_femmt.py index 1eb5c06e..acf39d45 100644 --- a/tests/integration/test_femmt.py +++ b/tests/integration/test_femmt.py @@ -22,6 +22,8 @@ import femmt.examples.experimental_inductor_time_domain import femmt.examples.experimental_transformer_time_domain import femmt.examples.experimental_transformer_three_winding_time_domain +import femmt.examples.basic_inductor_electrostatic +import femmt.examples.basic_transformer_electrostatic import femmt.examples.advanced_inductor_air_gap_sweep import femmt.examples.component_study.transformer_component_study import femmt.examples.basic_transformer_excitation_sweep @@ -171,7 +173,9 @@ def fixture_inductor_core_material_database(temp_folder: pytest.fixture): insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) @@ -296,7 +300,9 @@ def fixture_inductor_core_material_database_measurement(temp_folder: pytest.fixt insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) @@ -417,7 +423,9 @@ def fixture_inductor_core_fixed_loss_angle(temp_folder: pytest.fixture): insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) @@ -537,7 +545,9 @@ def fixture_inductor_core_fixed_loss_angle_dc(temp_folder: pytest.fixture): insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) @@ -655,7 +665,9 @@ def fixture_inductor_core_fixed_loss_angle_litz_wire(temp_folder: pytest.fixture insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) winding_window = fmt.WindingWindow(core, insulation) @@ -1025,7 +1037,9 @@ def fixture_transformer_core_fixed_loss_angle(temp_folder: pytest.fixture): # 4. set insulation insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) - insulation.add_winding_insulations([[0.0002, 0.0002], [0.0002, 0.0002]]) + insulation.add_winding_insulations([[0.0002, 0.0002], [0.0002, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1154,7 +1168,9 @@ def fixture_transformer_interleaved_core_fixed_loss_angle(temp_folder: pytest.fi # 4. set insulations insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) - insulation.add_winding_insulations([[0.0002, 0.0005], [0.0005, 0.0002]]) + insulation.add_winding_insulations([[0.0002, 0.0005], [0.0005, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1287,7 +1303,9 @@ def fixture_transformer_integrated_core_fixed_loss_angle(temp_folder: pytest.fix # 4. set insulations insulation = fmt.Insulation() insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) - insulation.add_winding_insulations([[0.0002, 0.0005], [0.0005, 0.0002]]) + insulation.add_winding_insulations([[0.0002, 0.0005], [0.0005, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1438,6 +1456,8 @@ def fixture_transformer_stacked_center_tapped(temp_folder: pytest.fixture): center_foil_additional_bobbin=0e-3, wrap_para_type=fmt.WrapParaType.FixedThickness, foil_horizontal_placing_strategy=fmt.FoilHorizontalDistribution.VerticalUpward) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=2e-4) geo.set_insulation(insulation) geo.set_winding_windows([coil_window, transformer_window]) @@ -1580,7 +1600,9 @@ def fixture_transformer_5_windings(temp_folder: pytest.fixture): [iso_against, iso_self, iso_against, iso_against, iso_against], [iso_against, iso_against, iso_self, iso_against, iso_against], [iso_against, iso_against, iso_self, iso_against, iso_against], - [iso_against, iso_against, iso_against, iso_against, iso_self]]) + [iso_against, iso_against, iso_against, iso_against, iso_self]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0001) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1741,7 +1763,9 @@ def fixture_inductor_time_domain(temp_folder: pytest.fixture): # 4. set insulations insulation = fmt.Insulation(flag_insulation=True) insulation.add_core_insulations(0.001, 0.001, 0.004, 0.001) - insulation.add_winding_insulations([[0.0005]]) + insulation.add_winding_insulations([[0.0005]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0005) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1828,7 +1852,9 @@ def fixture_transformer_time_domain(temp_folder: pytest.fixture): insulation = fmt.Insulation(flag_insulation=True) insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.001], - [0.001, 0.0002]]) + [0.001, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1921,7 +1947,9 @@ def fixture_transformer_3_windings_time_domain(temp_folder: pytest.fixture): insulation.add_core_insulations(0.001, 0.001, 0.002, 0.001) insulation.add_winding_insulations([[0.0002, 0.0004, 0.0004], [0.0004, 0.0002, 0.0004], - [0.0004, 0.0004, 0.0002]]) + [0.0004, 0.0004, 0.0002]], per_layer_of_turns=False) + insulation.add_turn_insulation([0.25e-5, 0.25e-5, 0.25e-5], add_turn_insulations=False) + insulation.add_insulation_between_layers(add_insulation_material=False, thickness=0.0002) geo.set_insulation(insulation) # 5. create winding window and virtual winding windows (vww) @@ -1977,6 +2005,225 @@ def fixture_transformer_3_windings_time_domain(temp_folder: pytest.fixture): return electromagnetoquasistatic_result, material_result, geometry_result +@pytest.fixture +def fixture_inductor_electrostatic(temp_folder: pytest.fixture): + """ + Integration test to the basic example file. + + :param temp_folder: temporary folder path and onelab filepath + :type temp_folder: pytest.fixture + """ + temp_folder_path, onelab_folder = temp_folder + + # Create new temp folder, build model and simulate + try: + working_directory = temp_folder_path + if not os.path.exists(working_directory): + os.mkdir(working_directory) + + # 1. chose simulation type + geo = fmt.MagneticComponent(simulation_type=fmt.SimulationType.ElectroStatic, component_type=fmt.ComponentType.Inductor, + working_directory=working_directory, onelab_verbosity=fmt.Verbosity.Silent, is_gui=True) + # Set onelab path manually + geo.file_data.onelab_folder_path = onelab_folder + + # 2. set core parameters + core_db = fmt.core_database()["PQ 40/40"] + core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=core_db["core_inner_diameter"], + window_w=core_db["window_w"], + window_h=core_db["window_h"], + core_h=core_db["core_h"]) + core = fmt.Core(core_type=fmt.CoreType.Single, + core_dimensions=core_dimensions, + detailed_core_model=False, + material=fmt.Material.N49, temperature=45, frequency=2700000, + # permeability_datasource="manufacturer_datasheet", + permeability_datasource=fmt.MaterialDataSource.Measurement, + permeability_datatype=fmt.MeasurementDataType.ComplexPermeability, + permeability_measurement_setup=fmt.MeasurementSetup.LEA_LK, + permittivity_datasource=fmt.MaterialDataSource.Measurement, + permittivity_datatype=fmt.MeasurementDataType.ComplexPermittivity, + permittivity_measurement_setup=fmt.MeasurementSetup.LEA_LK, mdb_verbosity=fmt.Verbosity.Silent) + + geo.set_core(core) + + # 3. set air gap parameters + air_gaps = fmt.AirGaps(fmt.AirGapMethod.Percent, core) + air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.001, 50) + # air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.0002, 90) + geo.set_air_gaps(air_gaps) + + # 4. set insulation and the materials of insulation (optional) + bobbin_db = fmt.bobbin_database()["PQ 40/40"] + bobbin_dimensions = fmt.dtos.BobbinDimensions(bobbin_inner_diameter=bobbin_db["bobbin_inner_diameter"], + bobbin_window_w=bobbin_db["bobbin_window_w"], + bobbin_window_h=bobbin_db["bobbin_window_h"], + bobbin_h=bobbin_db["bobbin_h"]) + insulation = fmt.Insulation(flag_insulation=True, bobbin_dimensions=bobbin_dimensions) + bobbin_material = fmt.insulation_materials_database()["core_insulation"]["bobbins"]["Thermoset"]["Phenolic"] + insulation.add_core_insulations(0.2e-3, 0.2e-3, 0.2e-3, 0.2e-3, + dielectric_constant=bobbin_material["dielectric_constant"]) + turn_insulation_material = fmt.insulation_materials_database()["wire_insulation"]["plastic_insulation"]["Plenum Polyvinyl Chloride (Plenum PVC)"] + insulation.add_turn_insulation([0.2e-3], dielectric_constant=[turn_insulation_material["dielectric_constant"]], + add_turn_insulations=True) + # This is an air between turns if needed + insulation.add_winding_insulations([[1e-3, 1e-3]], per_layer_of_turns=True) + # Kapton material is added between every layer of turns + layer_insulation = fmt.insulation_materials_database()["film_insulation"]["Kapton"] + insulation.add_insulation_between_layers(add_insulation_material=True, thickness=0.6e-3, dielectric_constant=layer_insulation["dielectric_constant"]) + geo.set_insulation(insulation) + + # 5. create winding window and virtual winding windows (vww) + winding_window = fmt.WindingWindow(core, insulation) + vww = winding_window.split_window(fmt.WindingWindowSplit.NoSplit) + + # 6. create conductor and set parameters: use solid wires + winding = fmt.Conductor(0, fmt.Conductivity.Copper, winding_material_temperature=45) + winding.set_solid_round_conductor(conductor_radius=1.1506e-3, conductor_arrangement=fmt.ConductorArrangement.Square) + winding.parallel = False # set True to make the windings parallel, currently only for solid conductors + # 7. add conductor to vww and add winding window to MagneticComponent + vww.set_winding(winding, 7, None, fmt.Align.ToEdges, placing_strategy=fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, + zigzag=True) + geo.set_winding_windows([winding_window]) + + # 8. create the model + geo.create_model(freq=2700000, pre_visualize_geometry=False, save_png=False, skin_mesh_factor=0.5) + + # 8. run electrostatic simulation + num_turns_w1 = 7 + V_A = 1 + V_B = 0 + voltages_winding_1 = [ + V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + for i in range(num_turns_w1) + ] + geo.electrostatic_simulation(voltage=[voltages_winding_1], ground_outer_boundary=False, core_voltage=0, + show_fem_simulation_results=False, save_to_excel_file=False) + + except Exception as e: + print("An error occurred while creating the femmt mesh files:", e) + except KeyboardInterrupt: + print("Keyboard interrupt..") + + electrostaticquasistatic_result = os.path.join(temp_folder_path, "results", "log_electro_static.json") + geometry_result = os.path.join(temp_folder_path, "results", "log_coordinates_description.json") + material_result = os.path.join(temp_folder_path, "results", "log_material.json") + + return electrostaticquasistatic_result, geometry_result, material_result + +@pytest.fixture +def fixture_transformer_electrostatic(temp_folder: pytest.fixture): + """ + Integration test to the basic example file. + + :param temp_folder: temporary folder path and onelab filepath + :type temp_folder: pytest.fixture + """ + temp_folder_path, onelab_folder = temp_folder + + # Create new temp folder, build model and simulate + try: + working_directory = temp_folder_path + if not os.path.exists(working_directory): + os.mkdir(working_directory) + + # 1. chose simulation type + geo = fmt.MagneticComponent(simulation_type=fmt.SimulationType.ElectroStatic, component_type=fmt.ComponentType.Transformer, + working_directory=working_directory, onelab_verbosity=fmt.Verbosity.Silent, is_gui=True) + # Set onelab path manually + geo.file_data.onelab_folder_path = onelab_folder + + # geo.set_core(core) + core_db = fmt.core_database()["PQ 40/40"] + core_dimensions = fmt.dtos.SingleCoreDimensions(core_inner_diameter=core_db["core_inner_diameter"], + window_w=core_db["window_w"], + window_h=core_db["window_h"], + core_h=core_db["core_h"]) + core = fmt.Core(core_type=fmt.CoreType.Single, + core_dimensions=core_dimensions, + detailed_core_model=False, + material=fmt.Material.N49, temperature=45, frequency=0, + # permeability_datasource="manufacturer_datasheet", + permeability_datasource=fmt.MaterialDataSource.Measurement, + permeability_datatype=fmt.MeasurementDataType.ComplexPermeability, + permeability_measurement_setup=fmt.MeasurementSetup.LEA_LK, + permittivity_datasource=fmt.MaterialDataSource.Measurement, + permittivity_datatype=fmt.MeasurementDataType.ComplexPermittivity, + permittivity_measurement_setup=fmt.MeasurementSetup.LEA_LK, mdb_verbosity=fmt.Verbosity.Silent) + + geo.set_core(core) + + # 3. set air gap parameters + air_gaps = fmt.AirGaps(fmt.AirGapMethod.Percent, core) + air_gaps.add_air_gap(fmt.AirGapLegPosition.CenterLeg, 0.0005, 50) + geo.set_air_gaps(air_gaps) + + # 4. set insulation + bobbin_db = fmt.bobbin_database()["PQ 40/40"] + bobbin_dimensions = fmt.dtos.BobbinDimensions(bobbin_inner_diameter=bobbin_db["bobbin_inner_diameter"], + bobbin_window_w=bobbin_db["bobbin_window_w"], + bobbin_window_h=bobbin_db["bobbin_window_h"], + bobbin_h=bobbin_db["bobbin_h"]) + insulation = fmt.Insulation(flag_insulation=True, bobbin_dimensions=bobbin_dimensions) + bobbin_material = fmt.insulation_materials_database()["core_insulation"]["bobbins"]["Thermoset"]["Phenolic"] + insulation.add_core_insulations(1.55e-3, 1.55e-3, 0.9e-3, 1.5e-4, + dielectric_constant=bobbin_material["dielectric_constant"]) + insulation.add_winding_insulations([[0.0002, 0.095e-3], + [0.095e-3, 0.0002]], per_layer_of_turns=True) + turn_insulation_material = fmt.insulation_materials_database()["wire_insulation"]["plastic_insulation"]["Plenum Polyvinyl Chloride (Plenum PVC)"] + insulation.add_turn_insulation([0.25e-5, 0.25e-5], + dielectric_constant=[turn_insulation_material["dielectric_constant"], turn_insulation_material["dielectric_constant"]], + add_turn_insulations=False) + layer_insulation = fmt.insulation_materials_database()["film_insulation"]["Kapton"] + insulation.add_insulation_between_layers(add_insulation_material=True, thickness=0.5e-3, dielectric_constant=layer_insulation["dielectric_constant"]) + geo.set_insulation(insulation) + + # 5. create winding window and virtual winding windows (vww) + winding_window = fmt.WindingWindow(core, insulation) + cells = winding_window.NHorizontalAndVerticalSplit(horizontal_split_factors=[0.29], + vertical_split_factors=None) + # 6. create conductors and set parameters + winding1 = fmt.Conductor(0, fmt.Conductivity.Copper) + winding1.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + + winding2 = fmt.Conductor(1, fmt.Conductivity.Copper) + # winding2.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + winding2.set_solid_round_conductor(1.1506e-3, fmt.ConductorArrangement.Square) + winding2.parallel = False + + # 7. add conductor to vww and add winding window to MagneticComponent + cells[1].set_winding(winding2, 10, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + cells[0].set_winding(winding1, 10, None, fmt.Align.ToEdges, fmt.ConductorDistribution.VerticalUpward_HorizontalRightward, zigzag=True) + geo.set_winding_windows([winding_window]) + + # 8. create model and run the simulation + geo.create_model(freq=0, pre_visualize_geometry=False) + # Number of turns in each winding + num_turns_w1 = 10 + num_turns_w2 = 10 + + V_A = 1 + V_B = 0 + voltages_winding_1 = [ + V_A - (V_A - V_B) * i / (num_turns_w1 - 1) + for i in range(num_turns_w1) + ] + voltages_winding_2 = [1] * num_turns_w2 + + geo.electrostatic_simulation(voltage=[voltages_winding_1, voltages_winding_2], core_voltage=0, ground_outer_boundary=False, + show_fem_simulation_results=False, save_to_excel_file=False) + + except Exception as e: + print("An error occurred while creating the femmt mesh files:", e) + except KeyboardInterrupt: + print("Keyboard interrupt..") + + electrostaticquasistatic_result = os.path.join(temp_folder_path, "results", "log_electro_static.json") + geometry_result = os.path.join(temp_folder_path, "results", "log_coordinates_description.json") + material_result = os.path.join(temp_folder_path, "results", "log_material.json") + + return electrostaticquasistatic_result, geometry_result, material_result + def test_inductor_core_material_database(fixture_inductor_core_material_database: pytest.fixture): """ @@ -2415,6 +2662,50 @@ def test_transformer_3_windings_time_domain(fixture_transformer_3_windings_time_ "transformer_3_windings_time_domain.json") compare_result_logs(test_result_log, fixture_result_log, significant_digits=4) +def test_simulation_inductor_electrostatic(fixture_inductor_electrostatic: pytest.fixture): + """ + Integration test to validate the electrostatic simulation. + + :param fixture_inductor_electrostatic: fixture for the inductor electrostatic simulation + :type fixture_inductor_electrostatic: pytest.fixture + """ + test_result_log, geometry_result_log, material_result_log = fixture_inductor_electrostatic + + assert os.path.exists(geometry_result_log), "Geometry creation did not work!" + + fixture_geometry_log = os.path.join(os.path.dirname(__file__), "fixtures", + "geometry_inductor_electrostatic.json") + compare_result_logs(geometry_result_log, fixture_geometry_log, significant_digits=10) + + assert os.path.exists(test_result_log), "Electro static simulation did not work!" + + # e_s mesh + fixture_result_log = os.path.join(os.path.dirname(__file__), "fixtures", + "inductor_electrostatic.json") + compare_result_logs(test_result_log, fixture_result_log, significant_digits=4) + +def test_simulation_transformer_electrostatic(fixture_transformer_electrostatic: pytest.fixture): + """ + Integration test to validate the electrostatic simulation. + + :param fixture_transformer_electrostatic: fixture for the transformer electrostatic simulation + :type fixture_transformer_electrostatic: pytest.fixture + """ + test_result_log, geometry_result_log, material_result_log = fixture_transformer_electrostatic + + assert os.path.exists(geometry_result_log), "Geometry creation did not work!" + + fixture_geometry_log = os.path.join(os.path.dirname(__file__), "fixtures", + "geometry_transformer_electrostatic.json") + compare_result_logs(geometry_result_log, fixture_geometry_log, significant_digits=10) + + assert os.path.exists(test_result_log), "Electro static simulation did not work!" + + # e_s mesh + fixture_result_log = os.path.join(os.path.dirname(__file__), "fixtures", + "transformer_electrostatic.json") + compare_result_logs(test_result_log, fixture_result_log, significant_digits=4) + def test_load_files(temp_folder: pytest.fixture, fixture_inductor_core_material_database: pytest.fixture, fixture_inductor_core_fixed_loss_angle: pytest.fixture, fixture_inductor_core_fixed_loss_angle_dc: pytest.fixture, @@ -2693,6 +2984,31 @@ def test_basic_transformer_time_domain(temp_folder: pytest.fixture): show_visual_outputs=False, is_test=True) +def test_basic_inductor_electrostatic(temp_folder: pytest.fixture): + """ + Integration test to the basic example file. + + :param temp_folder: temporary folder path and onelab filepath + :type temp_folder: pytest.fixture + """ + temp_folder_path, onelab_folder = temp_folder + femmt.examples.basic_inductor_electrostatic.basic_example_inductor_electrostatic(onelab_folder=onelab_folder, + show_visual_outputs=False, + is_test=True) + +def test_basic_transformer_electrostatic(temp_folder: pytest.fixture): + """ + Integration test to the basic example file. + + :param temp_folder: temporary folder path and onelab filepath + :type temp_folder: pytest.fixture + """ + temp_folder_path, onelab_folder = temp_folder + femmt.examples.basic_transformer_electrostatic.basic_example_transformer_electrostatic(onelab_folder=onelab_folder, + show_visual_outputs=False, + is_test=True) + + def test_basic_transformer_3_windings_time_domain(temp_folder: pytest.fixture): """ Integration test to the basic example file.