Inductance calculation
This commit is contained in:
		
							parent
							
								
									96d6cbd125
								
							
						
					
					
						commit
						618058e3b7
					
				| @ -1,3 +1,4 @@ | ||||
| matplotlib | ||||
| numpy | ||||
| pyinstaller | ||||
| pyinstaller | ||||
| scipy | ||||
| @ -2,6 +2,7 @@ | ||||
| 
 | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
| from tkinter.scrolledtext import ScrolledText | ||||
| 
 | ||||
| import matplotlib | ||||
| matplotlib.use("TkAgg") | ||||
| @ -10,11 +11,135 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg | ||||
| 
 | ||||
| import numpy as np | ||||
| import math as m | ||||
| import decimal as dec | ||||
| 
 | ||||
| # Importamos la función para dibujar la bobina 2D | ||||
| # Se importan las integrales elípticas de SciPy para el cálculo preciso de la inductancia | ||||
| from scipy.special import ellipk, ellipe | ||||
| 
 | ||||
| # ------------------------------------------------------------------- | ||||
| # Constantes y funciones para el cálculo de inductancia | ||||
| # ------------------------------------------------------------------- | ||||
| 
 | ||||
| MU0 = 4.0e-7 * m.pi  # Permeabilidad magnética del vacío (H/m) | ||||
| 
 | ||||
| def self_inductance_loop(r, a_eff=1e-4): | ||||
|     """ | ||||
|     Autoinductancia aproximada de un anillo fino de radio r. | ||||
|     Emplea la fórmula: L ~ mu0*r * [ln(8r/a_eff) - 2]. | ||||
|     """ | ||||
|     if r <= 0: | ||||
|         return 0.0 | ||||
|     return MU0 * r * (m.log(8.0 * r / a_eff) - 2.0) | ||||
| 
 | ||||
| def mutual_inductance_coaxial(r1, r2, z_dist): | ||||
|     """ | ||||
|     Inductancia mutua entre dos anillos coaxiales de radios r1 y r2, | ||||
|     separados una distancia axial z_dist. | ||||
|     Emplea las integrales elípticas de primera y segunda especie. | ||||
|     """ | ||||
|     if r1 < 1e-12 or r2 < 1e-12: | ||||
|         return 0.0 | ||||
| 
 | ||||
|     k_sq = 4.0 * r1 * r2 / ((r1 + r2)**2 + z_dist**2) | ||||
|     if k_sq > 1.0: | ||||
|         # Caso no físico (puede ocurrir por redondeos), retornamos 0 | ||||
|         return 0.0 | ||||
| 
 | ||||
|     k = np.sqrt(k_sq) | ||||
| 
 | ||||
|     # Caso casi degenerado (r1 ~ r2, z_dist ~ 0) | ||||
|     if (1.0 - k_sq) < 1e-12: | ||||
|         # Aproximación cuando r1 ≈ r2 y z ≈ 0 | ||||
|         return MU0 * min(r1, r2) * m.pi | ||||
|     else: | ||||
|         Kk = ellipk(k_sq) | ||||
|         Ek = ellipe(k_sq) | ||||
|         factor = MU0 * np.sqrt(r1 * r2) | ||||
|         # Expresión: M = mu0 * sqrt(r1*r2)*((2-k)*K - 2E) / k | ||||
|         return factor * ((2.0 - k) * Kk - 2.0 * Ek) / k | ||||
| 
 | ||||
| def compute_coil_geometry(N_total, R_int, R_ext, espesor_plast, H, wire_d): | ||||
|     """ | ||||
|     Genera la lista de (r_i, z_i) para las N_total espiras, asumiendo | ||||
|     que se llenan capas radiales y, en cada capa, vueltas a lo largo de la altura. | ||||
|     Todos los parámetros deben estar en metros. | ||||
|     """ | ||||
|     radial_thickness = R_ext - (R_int + 2 * espesor_plast) | ||||
|     if radial_thickness <= 0: | ||||
|         raise ValueError("No hay espacio radial para el bobinado.") | ||||
| 
 | ||||
|     max_layers = int(np.floor(radial_thickness / wire_d)) | ||||
|     if max_layers < 1: | ||||
|         raise ValueError("No cabe ni una capa con el grosor de hilo dado.") | ||||
| 
 | ||||
|     max_turns_per_layer = int(np.floor(H / wire_d)) | ||||
|     if max_turns_per_layer < 1: | ||||
|         raise ValueError("No cabe ni una vuelta en la altura dada.") | ||||
| 
 | ||||
|     max_turns_possible = max_layers * max_turns_per_layer | ||||
|     if max_turns_possible < N_total: | ||||
|         raise ValueError( | ||||
|             f"Con {max_layers} capas y {max_turns_per_layer} vueltas/capa " | ||||
|             f"sólo se pueden alojar {max_turns_possible} espiras, " | ||||
|             f"pero se solicitan {N_total}." | ||||
|         ) | ||||
| 
 | ||||
|     espiras = [] | ||||
|     vueltas_restantes = N_total | ||||
|     layer_index = 0 | ||||
| 
 | ||||
|     while vueltas_restantes > 0 and layer_index < max_layers: | ||||
|         r_layer = R_int + espesor_plast + layer_index * wire_d + wire_d / 2.0 | ||||
| 
 | ||||
|         if vueltas_restantes >= max_turns_per_layer: | ||||
|             n_vueltas = max_turns_per_layer | ||||
|         else: | ||||
|             n_vueltas = vueltas_restantes | ||||
| 
 | ||||
|         for turn in range(n_vueltas): | ||||
|             z_i = (turn + 0.5) * wire_d | ||||
|             espiras.append((r_layer, z_i)) | ||||
| 
 | ||||
|         vueltas_restantes -= n_vueltas | ||||
|         layer_index += 1 | ||||
| 
 | ||||
|     if vueltas_restantes > 0: | ||||
|         raise ValueError( | ||||
|             f"No se pudo colocar toda la bobina. " | ||||
|             f"Faltan {vueltas_restantes} espiras por ubicar." | ||||
|         ) | ||||
| 
 | ||||
|     return espiras | ||||
| 
 | ||||
| def inductance_from_spiralist(espiras, wire_radius): | ||||
|     """ | ||||
|     Calcula la inductancia total (en henrios) a partir de la lista (r_i, z_i), | ||||
|     sumando la autoinductancia de cada espira y la mutua entre pares (O(N^2)). | ||||
|     """ | ||||
|     N = len(espiras) | ||||
|     L_total = 0.0 | ||||
| 
 | ||||
|     for i in range(N): | ||||
|         r_i, z_i = espiras[i] | ||||
|         # Autoinductancia | ||||
|         L_total += self_inductance_loop(r_i, wire_radius) | ||||
|         # Mutua con las espiras j > i | ||||
|         for j in range(i + 1, N): | ||||
|             r_j, z_j = espiras[j] | ||||
|             dist_z = abs(z_j - z_i) | ||||
|             M_ij = mutual_inductance_coaxial(r_i, r_j, dist_z) | ||||
|             L_total += 2.0 * M_ij | ||||
| 
 | ||||
|     return L_total | ||||
| 
 | ||||
| # ------------------------------------------------------------------- | ||||
| # Importamos la función para dibujar la bobina 2D (tal y como estaba). | ||||
| # ------------------------------------------------------------------- | ||||
| from tabs.geometry_viewer import plot_coil_in_frame | ||||
| 
 | ||||
| # ------------------------------------------------------------------- | ||||
| # Definición de la clase TabCoil, añadiendo la ventana de logs y | ||||
| # el botón de cálculo de inductancia con mensajes de estado. | ||||
| # ------------------------------------------------------------------- | ||||
| class TabCoil: | ||||
|     def __init__(self, notebook, tab_simulator): | ||||
|         """ | ||||
| @ -30,9 +155,6 @@ class TabCoil: | ||||
|         self.frame = tk.Frame(notebook) | ||||
|         self.frame.pack(fill="both", expand=True) | ||||
| 
 | ||||
|         # (Este add lo hace habitualmente main.py, pero se podría hacer aquí) | ||||
|         # notebook.add(self.frame, text="Bobinas") | ||||
| 
 | ||||
|         # Layout principal: lado izq (parámetros) y lado der (dibujo) | ||||
|         frame_left = tk.Frame(self.frame) | ||||
|         frame_left.pack(side="left", fill="y", padx=5, pady=5) | ||||
| @ -50,18 +172,15 @@ class TabCoil: | ||||
|         tk.Label(frame_left, text="Parámetros de la Bobina:").pack(anchor="w") | ||||
| 
 | ||||
|         # Variables | ||||
|         self.var_N     = tk.DoubleVar(value=500) | ||||
|         self.var_e_pvc = tk.DoubleVar(value=4.04) | ||||
|         self.var_r_int = tk.DoubleVar(value=12.07) | ||||
|         self.var_r_ext = tk.DoubleVar(value=21.27) | ||||
|         self.var_h_c   = tk.DoubleVar(value=53.12) | ||||
|         self.var_d_cu  = tk.DoubleVar(value=0.8) | ||||
|          | ||||
|         mu_0 = 4 * m.pi * 1e-7 | ||||
|         mu_r = 1 | ||||
|          | ||||
|         inductance = (self.var_N.get()**2 * mu_0 * mu_r * (2 * m.pi * (self.var_r_ext.get() * 1e-3)**2)) / (self.var_h_c.get() * 1e-3) | ||||
|         self.var_L  = tk.DoubleVar(value=inductance) | ||||
|         self.var_N     = tk.DoubleVar(value=500)    # Número de espiras | ||||
|         self.var_e_pvc = tk.DoubleVar(value=2)      # Espesor del plástico [mm] | ||||
|         self.var_r_int = tk.DoubleVar(value=4.035)  # Radio interior [mm] | ||||
|         self.var_r_ext = tk.DoubleVar(value=10.64)  # Radio exterior [mm] | ||||
|         self.var_h_c   = tk.DoubleVar(value=53.12)  # Altura de la bobina [mm] | ||||
|         self.var_d_cu  = tk.DoubleVar(value=0.5)    # Diámetro conductor [mm] | ||||
| 
 | ||||
|         # Variable para mostrar la inductancia (se mostrará en µH) | ||||
|         self.var_L = tk.StringVar(value="") | ||||
| 
 | ||||
|         # Frame para agrupar los Entries | ||||
|         self.frame_params = tk.Frame(frame_left, bd=1, relief="sunken") | ||||
| @ -74,26 +193,48 @@ class TabCoil: | ||||
|             ent = tk.Entry(self.frame_params, textvariable=var, width=width) | ||||
|             ent.grid(row=row, column=1, padx=5, pady=2) | ||||
| 
 | ||||
|         # Creamos las filas | ||||
|         rowcount = 0 | ||||
|         add_param_row("N (vueltas):",      self.var_N,     rowcount); rowcount += 1 | ||||
|         add_param_row("e_pvc (mm):",       self.var_e_pvc, rowcount); rowcount += 1 | ||||
|         add_param_row("r_int (mm):",       self.var_r_int, rowcount); rowcount += 1 | ||||
|         add_param_row("r_ext (mm):",       self.var_r_ext, rowcount); rowcount += 1 | ||||
|         add_param_row("h_c (mm):",         self.var_h_c,   rowcount); rowcount += 1 | ||||
|         add_param_row("d_cu (mm):",        self.var_d_cu,  rowcount); rowcount += 1 | ||||
|         add_param_row("L (H):",            self.var_L,     rowcount); rowcount += 1 | ||||
|         add_param_row("N (vueltas):",   self.var_N,     rowcount); rowcount += 1 | ||||
|         add_param_row("e_pvc (mm):",    self.var_e_pvc, rowcount); rowcount += 1 | ||||
|         add_param_row("r_int (mm):",    self.var_r_int, rowcount); rowcount += 1 | ||||
|         add_param_row("r_ext (mm):",    self.var_r_ext, rowcount); rowcount += 1 | ||||
|         add_param_row("h_c (mm):",      self.var_h_c,   rowcount); rowcount += 1 | ||||
|         add_param_row("d_cu (mm):",     self.var_d_cu,  rowcount); rowcount += 1 | ||||
| 
 | ||||
|         # Botón para actualizar la vista 2D | ||||
|         # Campo para ver la inductancia en microHenrios (µH). | ||||
|         lblL = tk.Label(self.frame_params, text="Induct. (µH):") | ||||
|         lblL.grid(row=rowcount, column=0, sticky="w", padx=5, pady=2) | ||||
|         entL = tk.Entry(self.frame_params, textvariable=self.var_L, width=10, state="readonly") | ||||
|         entL.grid(row=rowcount, column=1, padx=5, pady=2) | ||||
|         rowcount += 1 | ||||
| 
 | ||||
|         # Botones | ||||
|         btn_refrescar = tk.Button( | ||||
|             frame_left, text="Refrescar Vista 2D", | ||||
|             command=self.refrescar_2d | ||||
|         ) | ||||
|         btn_refrescar.pack(pady=10) | ||||
| 
 | ||||
|         btn_calc_L = tk.Button( | ||||
|             frame_left, text="Calcular Inductancia", | ||||
|             command=self.calcular_inductancia | ||||
|         ) | ||||
|         btn_calc_L.pack(pady=5) | ||||
| 
 | ||||
|         # | ||||
|         # Creamos la figura y canvas para la bobina en 2D (opcional,  | ||||
|         # pues 'plot_coil_in_frame' creará su propia Figure). | ||||
|         # Sección de Logs | ||||
|         # | ||||
|         tk.Label(frame_left, text="Logs:").pack(anchor="w") | ||||
|         self.log_area = ScrolledText(frame_left, wrap="word", height=8, width=35) | ||||
|         self.log_area.pack(fill="x", padx=5, pady=5) | ||||
|         self.log_area.configure(state="disabled") | ||||
| 
 | ||||
|         # Definimos etiquetas de color | ||||
|         self.log_area.tag_config("info", foreground="green") | ||||
|         self.log_area.tag_config("error", foreground="red") | ||||
| 
 | ||||
|         # | ||||
|         # Figura y canvas para la bobina en 2D | ||||
|         # | ||||
|         self.fig = plt.Figure(figsize=(4,3), dpi=100) | ||||
|         self.ax = self.fig.add_subplot(111) | ||||
| @ -104,12 +245,21 @@ class TabCoil: | ||||
|         # Dibujamos la bobina inicial | ||||
|         self.refrescar_2d() | ||||
| 
 | ||||
|     def log_message(self, text, mode="info"): | ||||
|         """ | ||||
|         Inserta un mensaje en el log_area, en color definido por 'mode'. | ||||
|         mode puede ser 'info' (verde) o 'error' (rojo). | ||||
|         """ | ||||
|         self.log_area.configure(state="normal") | ||||
|         self.log_area.insert("end", text + "\n", mode) | ||||
|         self.log_area.see("end") | ||||
|         self.log_area.configure(state="disabled") | ||||
| 
 | ||||
|     def refrescar_2d(self): | ||||
|         """ | ||||
|         Lee los valores de la bobina y llama a 'plot_coil_in_frame' | ||||
|         para actualizar el dibujo en 2D. | ||||
|         """ | ||||
|         # Obtenemos valores | ||||
|         N_val     = self.var_N.get() | ||||
|         e_pvc_val = self.var_e_pvc.get() | ||||
|         r_int_val = self.var_r_int.get() | ||||
| @ -117,7 +267,7 @@ class TabCoil: | ||||
|         h_c_val   = self.var_h_c.get() | ||||
|         d_cu_val  = self.var_d_cu.get() | ||||
| 
 | ||||
|         # Limpiamos el Axes actual (si lo usáramos) | ||||
|         # Limpiar el Axes | ||||
|         self.ax.clear() | ||||
| 
 | ||||
|         # Llamada a la función que inserta la figura en self.frame_2d | ||||
| @ -130,3 +280,56 @@ class TabCoil: | ||||
|             h_c=h_c_val, | ||||
|             d_cu=d_cu_val | ||||
|         ) | ||||
| 
 | ||||
|     def calcular_inductancia(self): | ||||
|         """ | ||||
|         Cálculo de la inductancia a partir de los parámetros introducidos. | ||||
|         Muestra el resultado en µH en el campo correspondiente y en la log_area. | ||||
|         """ | ||||
|         try: | ||||
|             # Lectura de parámetros | ||||
|             N_espiras = int(self.var_N.get()) | ||||
|             e_pvc_val = self.var_e_pvc.get() | ||||
|             r_int_val = self.var_r_int.get() | ||||
|             r_ext_val = self.var_r_ext.get() | ||||
|             h_c_val   = self.var_h_c.get() | ||||
|             d_cu_val  = self.var_d_cu.get() | ||||
| 
 | ||||
|             # Conversión mm -> m | ||||
|             R_in = r_int_val / 1000.0 | ||||
|             R_out = r_ext_val / 1000.0 | ||||
|             plast = e_pvc_val / 1000.0 | ||||
|             altura = h_c_val / 1000.0 | ||||
|             diam_hilo = d_cu_val / 1000.0 | ||||
| 
 | ||||
|             # Construir la geometría de la bobina | ||||
|             espiras_xyz = compute_coil_geometry( | ||||
|                 N_total=N_espiras, | ||||
|                 R_int=R_in, | ||||
|                 R_ext=R_out, | ||||
|                 espesor_plast=plast, | ||||
|                 H=altura, | ||||
|                 wire_d=diam_hilo | ||||
|             ) | ||||
| 
 | ||||
|             # Radio efectivo para autoinductancia | ||||
|             wire_radius = diam_hilo / 2.0 | ||||
| 
 | ||||
|             # Calcular la inductancia total (en henrios) | ||||
|             L_h = inductance_from_spiralist(espiras_xyz, wire_radius) | ||||
|             L_uH = L_h * 1e6 | ||||
| 
 | ||||
|             # Actualizar variable de resultado | ||||
|             self.var_L.set(f"{L_uH:.3f}") | ||||
| 
 | ||||
|             # Log de éxito | ||||
|             self.log_message("Inductancia calculada correctamente", "info") | ||||
| 
 | ||||
|         except ValueError as e: | ||||
|             # Si hay algún problema con la geometría, etc. | ||||
|             self.var_L.set("Error") | ||||
|             self.log_message(f"Error en cálculo: {str(e)}", "error") | ||||
|         except Exception as ex: | ||||
|             # Errores generales | ||||
|             self.var_L.set("Error") | ||||
|             self.log_message(f"Error inesperado: {str(ex)}", "error") | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Pedro Jose Romero Gombau
						Pedro Jose Romero Gombau