From d4ac1c9b02d7fe55ddeb81ad532f0d545829634e Mon Sep 17 00:00:00 2001 From: promerogomb Date: Fri, 24 Jan 2025 14:26:27 +0100 Subject: [PATCH] v1.1 --- VERSION | 2 +- requirements.txt | 3 +- src/tabs/tab_coil.py | 263 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 236 insertions(+), 32 deletions(-) diff --git a/VERSION b/VERSION index b123147..ea710ab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1 \ No newline at end of file +1.2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f134a34..4cdc1a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ matplotlib numpy -pyinstaller \ No newline at end of file +pyinstaller +scipy \ No newline at end of file diff --git a/src/tabs/tab_coil.py b/src/tabs/tab_coil.py index e1aaab2..574da03 100644 --- a/src/tabs/tab_coil.py +++ b/src/tabs/tab_coil.py @@ -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 abs(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.04) # 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")