import tkinter as tk from tkinter import ttk, messagebox, simpledialog, filedialog import json import numpy as np import math import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import pandas as pd import os from parametric_sweep import ParametricSweep # ==================== CONSTANTES DE MATERIAL ==================== DENS = 7850 # kg/m3 FY = 355 # MPa GAMMAS = 1.05 # Factor seguridad acero EYOUNG = 210000 # MPa NU = 0.3 # Coef Poisson FYD = FY * 10**6 / GAMMAS # Pa EYOUNG_PA = EYOUNG * 10**6 # Pa G = EYOUNG_PA / (2 * (1 + NU)) # Pa # ==================== FUNCIONES DE CÁLCULO ==================== def generate_ipe_points(H, b, tf, tw): """Genera los puntos de una sección IPE a partir de parámetros normalizados""" # H: altura total, b: ancho flange, tf: espesor flange, tw: espesor alma h_alma = H - 2*tf x_alma_ini = (b - tw) / 2 x_alma_fin = (b + tw) / 2 # Puntos de la sección (sentido horario) points = np.array([ [0, 0], # Esquina inferior izquierda flange [b, 0], # Esquina inferior derecha flange [b, tf], # Unión alma-flange derecha [x_alma_fin, tf], # Esquina superior alma derecha [x_alma_fin, H - tf], # Esquina inferior alma derecha (arriba) [b, H - tf], # Unión alma-flange arriba [b, H], # Esquina superior derecha flange [0, H], # Esquina superior izquierda flange [0, H - tf], # Unión alma-flange arriba izquierda [x_alma_ini, H - tf], # Esquina inferior alma izquierda (arriba) [x_alma_ini, tf], # Esquina superior alma izquierda [0, tf], # Unión alma-flange izquierda [0, 0], # Cerrar la sección ]) return points def generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta): """Genera los puntos de la sección tipo puente nuevo a partir de parámetros normalizados""" # h: altura exterior, b: ancho total, tf: espesor ala, tw: espesor alma, hr = (b-tw) / 2 * math.tan(math.radians(theta)) hl = tr / math.cos(math.radians(theta)) hr_ = (b/2 - ta - tw/2)* math.tan(math.radians(theta)) # Puntos de la sección (sentido horario) points = np.array([ [0, 0], [b, 0], [b, tf + ha], [(b + tw)/2, tf + ha + hr], [(b + tw)/2, tf + ha + hr - hl], [b - ta, tf + ha + hr - hl - hr_], [b - ta, tf], [(b + tw)/2, tf], [(b + tw)/2, h - tf], [b - ta, h - tf], [b - ta, h - tf - ha - hr + hl + hr_], [(b + tw)/2, h - tf - ha - hr + hl], [(b + tw)/2, h - tf - ha - hr], [b, h - tf - ha], [b, h], [0, h], [0, h - tf - ha], [(b - tw)/2, h - tf - ha - hr], [(b - tw)/2, h - tf - ha - hr + hl], [ta, h - tf - ha - hr + hl + hr_], [ta, h - tf], [(b - tw)/2, h - tf], [(b - tw)/2, tf], [ta, tf], [ta, tf + ha + hr - hl - hr_], [(b - tw)/2, tf + ha + hr - hl], [(b - tw)/2, tf + ha + hr], [0, tf + ha], [0, 0], ]) return points def calculate_section_properties(points): """Calcula todas las propiedades de la sección - CÓDIGO ORIGINAL SIN MODIFICAR""" npuntos = len(points) puntos = points px = puntos[:, 0] py = puntos[:, 1] # ANCHO Y CANTO MÁXIMO bmax = np.amax(px) - np.amin(px) hmax = np.amax(py) - np.amin(py) # PERÍMETRO long_i = np.zeros(npuntos - 1) for i in range(npuntos - 1): long_i[i] = ((puntos[i+1, 0] - puntos[i, 0])**2 + (puntos[i+1, 1] - puntos[i, 1])**2)**(1/2) perimetro = abs(sum(long_i)) # ÁREA area_i = np.zeros(npuntos - 1) for i in range(npuntos - 1): area_i[i] = (puntos[i+1, 0] - puntos[i, 0]) * (puntos[i+1, 1] + puntos[i, 1]) / 2 area = abs(sum(area_i)) # PESO POR METRO peso = area * DENS # CENTRO DE GRAVEDAD cdg_i = np.zeros([npuntos, 2]) for i in range(npuntos - 1): h1 = puntos[i, 1] h2 = puntos[i+1, 1] b = puntos[i+1, 0] - puntos[i, 0] d = puntos[i, 0] if h1 + h2 == 0: cdg_i[i, 1] = 0 else: cdg_i[i, 1] = 1/3 * (h1*h1 + h1*h2 + h2*h2) / (h1 + h2) if h1 + h2 == 0: cdg_i[i, 0] = d + b/2 else: cdg_i[i, 0] = d + b/3 * (h1 + 2*h2) / (h1 + h2) # MOMENTO ESTÁTICO statico_i = np.zeros([npuntos, 2]) for i in range(npuntos - 1): statico_i[i, 1] = area_i[i] * cdg_i[i, 1] statico_i[i, 0] = area_i[i] * cdg_i[i, 0] cdg = sum(statico_i) / sum(area_i) xg = cdg[0] yg = cdg[1] # FIBRAS MÁS ALEJADAS v1y = np.amax(py) - yg v2y = np.amin(py) - yg v1x = np.amax(px) - xg v2x = np.amin(px) - xg # MOMENTOS DE INERCIA inercia_i = np.zeros([npuntos, 3]) for i in range(npuntos - 1): h1 = puntos[i, 1] h2 = puntos[i+1, 1] b = puntos[i+1, 0] - puntos[i, 0] d = puntos[i, 0] xgi = cdg_i[i, 0] ygi = cdg_i[i, 1] ai = area_i[i] # Ixg if h2 >= h1: ixcuad_G_local = 1/12 * b * (h1**3) + b * h1 * (h1/2 - ygi)**2 ixtriang_G_loc = 1/36 * b * (h2-h1)**3 + 1/2 * b * (h2-h1) * ((2*h1+h2)/3 - ygi)**2 else: ixcuad_G_local = 1/12 * b * (h2**3) + b * h2 * (h2/2 - ygi)**2 ixtriang_G_loc = 1/36 * b * (h1-h2)**3 + 1/2 * b * (h1-h2) * ((2*h2+h1)/3 - ygi)**2 inercia_i[i, 0] = ixcuad_G_local + ixtriang_G_loc + ai * (yg - ygi)**2 # Iyg if h2 >= h1: iycuad = 1/12 * h1 * b**3 + h1 * b * (b/2 + d - xgi)**2 iytrian = 1/36 * (h2-h1) * b**3 + 1/2 * b * (h2-h1) * (2/3*b + d - xgi)**2 else: iycuad = 1/12 * h2 * b**3 + h2 * b * (b/2 + d - xgi)**2 iytrian = 1/36 * (h1-h2) * b**3 + 1/2 * b * (h1-h2) * (1/3*b + d - xgi)**2 inercia_i[i, 1] = iycuad + iytrian + ai * (xg - xgi)**2 # Pxyg if h2 >= h1: pxygcuadrado = b * h1 * (-h1/2 + ygi) * (-d - b/2 + xgi) pxytriangulo = b*b * (h2-h1)**2 / 72 + b * (h2-h1) / 2 * (-(h2-h1)/3 - h1 + ygi) * (-d - 2/3*b + xgi) else: pxygcuadrado = b * h2 * (-h2/2 + ygi) * (-d - b/2 + xgi) pxytriangulo = -b*b * (h1-h2)**2 / 72 + b * (h1-h2) / 2 * (-(h1-h2)/3 - h2 + ygi) * (-d - 1/3*b + xgi) inercia_i[i, 2] = pxygcuadrado + pxytriangulo + ai * (-xg + xgi) * (-yg + ygi) ig = sum(inercia_i) ixg = abs(ig[0]) iyg = abs(ig[1]) pxyg = ig[2] if sum(area_i) >= 0: pxyg = pxyg else: pxyg = -pxyg # RADIOS DE GIRO rx = (ixg / area)**0.5 ry = (iyg / area)**0.5 # EJES PRINCIPALES DE INERCIA ic = (ixg + iyg) / 2 ir = (((ixg - iyg)/2)**2 + pxyg**2)**0.5 imax = ic + ir imin = ic - ir rmax = (imax / area)**0.5 rmin = (imin / area)**0.5 # ORIENTACIÓN DE EJES PRINCIPALES rest = ixg - iyg if abs(rest) < 10**-12: tetha = 45 else: tetha = 0.5 * math.atan((pxyg*2) / (ixg - iyg)) tetha = abs(tetha) * 180 / math.pi if pxyg > 0: if ixg > iyg: tetha = -tetha else: tetha = tetha else: if ixg > iyg: tetha = tetha else: tetha = -tetha # MÓDULO RESISTENTE ELÁSTICO wel1x = abs(ixg / v1y) wel2x = abs(ixg / v2y) wel1y = abs(iyg / v1x) wel2y = abs(iyg / v2x) # AXIL Y MOMENTO ELÁSTICO nel = area * FYD melx = min(wel1x, wel2x) * FYD mely = min(wel1y, wel2y) * FYD # CONVERSIÓN A UNIDADES PRÁCTICAS (cm, cm2, cm3, cm4, kN) pot = 2 bmax_cm = bmax * 10**pot hmax_cm = hmax * 10**pot perimetro_cm = perimetro * 10**pot xg_cm = xg * 10**pot yg_cm = yg * 10**pot v1y_cm = v1y * 10**pot v2y_cm = v2y * 10**pot v1x_cm = v1x * 10**pot v2x_cm = v2x * 10**pot rx_cm = rx * 10**pot ry_cm = ry * 10**pot rmax_cm = rmax * 10**pot rmin_cm = rmin * 10**pot area_cm2 = area * 10**(pot*2) wel1x_cm3 = wel1x * 10**(pot*3) wel2x_cm3 = wel2x * 10**(pot*3) wel1y_cm3 = wel1y * 10**(pot*3) wel2y_cm3 = wel2y * 10**(pot*3) ixg_cm4 = ixg * 10**(pot*4) iyg_cm4 = iyg * 10**(pot*4) pxyg_cm4 = pxyg * 10**(pot*4) imax_cm4 = imax * 10**(pot*4) imin_cm4 = imin * 10**(pot*4) nel_kn = nel / 1000 melx_kn = melx / 1000 mely_kn = mely / 1000 return { 'area': area_cm2, 'perimetro': perimetro_cm, 'xg': xg_cm, 'yg': yg_cm, 'v1y': v1y_cm, 'v2y': v2y_cm, 'v1x': v1x_cm, 'v2x': v2x_cm, 'ixg': ixg_cm4, 'iyg': iyg_cm4, 'pxyg': pxyg_cm4, 'imax': imax_cm4, 'imin': imin_cm4, 'rx': rx_cm, 'ry': ry_cm, 'rmax': rmax_cm, 'rmin': rmin_cm, 'wel1x': wel1x_cm3, 'wel2x': wel2x_cm3, 'wel1y': wel1y_cm3, 'wel2y': wel2y_cm3, 'tetha': tetha, 'peso': peso, 'nel': nel_kn, 'melx': melx_kn, 'mely': mely_kn, 'xg_orig': xg, 'yg_orig': yg, 'bmax': bmax, 'hmax': hmax, 'points': points } # ==================== APLICACIÓN TKINTER ==================== class SectionDesignerApp(tk.Tk): def __init__(self): super().__init__() self.title("Diseñador de Secciones de Acero") self.geometry("1200x800") self.json_path = r"c:\Users\Daniel.p\Documents\Automatizaciones\Propiedades seccion\secciones_config.json" self.load_json() self.current_section_name = None self.current_points = None self.current_properties = None # Variables para barrido paramétrico self.sweep_result = None # DataFrame de resultados self.sweep_pareto = None # DataFrame con Pareto front self.sweep_figure = None # Figura con gráficas self.sweep_canvas = None # Canvas para mostrar gráficas self.create_widgets() def load_json(self): """Carga el archivo JSON de configuración""" if os.path.exists(self.json_path): with open(self.json_path, 'r') as f: self.secciones_data = json.load(f) else: self.secciones_data = {"secciones": []} def save_json(self): """Guarda los cambios al JSON""" with open(self.json_path, 'w') as f: json.dump(self.secciones_data, f, indent=2) def create_widgets(self): """Crea la interfaz gráfica""" # ========== PANEL IZQUIERDO (CONTROLES) ========== left_panel = ttk.Frame(self, width=250) left_panel.pack(side=tk.LEFT, fill=tk.BOTH, padx=10, pady=10) # Selector de sección ttk.Label(left_panel, text="Sección:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) self.combo_seccion = ttk.Combobox( left_panel, values=[s['nombre'] for s in self.secciones_data['secciones']], state='readonly', width=20 ) self.combo_seccion.pack(anchor=tk.W) self.combo_seccion.bind('<>', self.on_section_changed) # Separador ttk.Separator(left_panel, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) # Selector de tipo de sección ttk.Label(left_panel, text="Tipo de sección:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) self.combo_tipo = ttk.Combobox( left_panel, values=["IPE", "Personalizada", "Puente nuevo", "Barrido Paramétrico"], state='readonly', width=20 ) self.combo_tipo.pack(anchor=tk.W, pady=(0, 10)) self.combo_tipo.set("IPE") self.combo_tipo.bind('<>', self.on_section_type_changed) # Frame para parámetros IPE self.frame_ipe = ttk.Frame(left_panel) self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10)) ttk.Label(self.frame_ipe, text="Parámetros IPE:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) # H (Altura) ttk.Label(self.frame_ipe, text="H (altura, m):").pack(anchor=tk.W) self.entry_H = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.01, width=15, command=self.on_parameter_change_ipe) self.entry_H.pack(anchor=tk.W, fill=tk.X) self.entry_H.set("0.300") self.entry_H.bind('', self.on_parameter_change_ipe) self.entry_H.bind('', self.on_parameter_change_ipe) # b (Ancho) ttk.Label(self.frame_ipe, text="b (ancho flange, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_b = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.01, width=15, command=self.on_parameter_change_ipe) self.entry_b.pack(anchor=tk.W, fill=tk.X) self.entry_b.set("0.150") self.entry_b.bind('', self.on_parameter_change_ipe) self.entry_b.bind('', self.on_parameter_change_ipe) # tf (Espesor flange) ttk.Label(self.frame_ipe, text="tf (espesor flange, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_tf = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.005, width=15, command=self.on_parameter_change_ipe) self.entry_tf.pack(anchor=tk.W, fill=tk.X) self.entry_tf.set("0.0107") self.entry_tf.bind('', self.on_parameter_change_ipe) self.entry_tf.bind('', self.on_parameter_change_ipe) # tw (Espesor alma) ttk.Label(self.frame_ipe, text="tw (espesor alma, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_tw = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.005, width=15, command=self.on_parameter_change_ipe) self.entry_tw.pack(anchor=tk.W, fill=tk.X) self.entry_tw.set("0.0063") self.entry_tw.bind('', self.on_parameter_change_ipe) self.entry_tw.bind('', self.on_parameter_change_ipe) # Frame para sección personalizada self.frame_personalizada = ttk.Frame(left_panel) ttk.Label(self.frame_personalizada, text="Puntos (JSON):", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) ttk.Label(self.frame_personalizada, text="Formato: [[x1,y1], [x2,y2], ...]", font=("Arial", 8)).pack(anchor=tk.W) self.text_puntos = tk.Text(self.frame_personalizada, height=8, width=30, font=("Courier", 8)) self.text_puntos.pack(fill=tk.BOTH, expand=True, pady=(0, 5)) ttk.Button(self.frame_personalizada, text="Cargar puntos", command=self.load_custom_points).pack(fill=tk.X) # Frame para puente nuevo self.frame_puente_nuevo = ttk.Frame(left_panel) self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) ttk.Label(self.frame_puente_nuevo, text="Parámetros puente nuevo:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) # H (Altura exterior) ttk.Label(self.frame_puente_nuevo, text = "h (altura exterior, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_h_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.01, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_h_puente.pack(anchor=tk.W, fill=tk.X) self.entry_h_puente.set("0.300") self.entry_h_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_h_puente.bind('', self.on_parameter_change_puente_nuevo) # b (Ancho) ttk.Label(self.frame_puente_nuevo, text="b (ancho, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_b_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.005, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_b_puente.pack(anchor=tk.W, fill=tk.X) self.entry_b_puente.set("0.150") self.entry_b_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_b_puente.bind('', self.on_parameter_change_puente_nuevo) # tf (espesor ala) ttk.Label(self.frame_puente_nuevo, text="tf (espesor ala, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_tf_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_tf_puente.pack(anchor=tk.W, fill=tk.X) self.entry_tf_puente.set("0.0107") self.entry_tf_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_tf_puente.bind('', self.on_parameter_change_puente_nuevo) # tw (espesor alma) ttk.Label(self.frame_puente_nuevo, text="tw (espesor alma, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_tw_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_tw_puente.pack(anchor=tk.W, fill=tk.X) self.entry_tw_puente.set("0.0063") self.entry_tw_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_tw_puente.bind('', self.on_parameter_change_puente_nuevo) # ha (altura refuerzo ala) ttk.Label(self.frame_puente_nuevo, text="ha (altura refuerzo ala, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_ha_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_ha_puente.pack(anchor=tk.W, fill=tk.X) self.entry_ha_puente.set("0.02") self.entry_ha_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_ha_puente.bind('', self.on_parameter_change_puente_nuevo) # ta (espesor refuerzo ala) ttk.Label(self.frame_puente_nuevo, text="ta (espesor refuerzo ala, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_ta_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_ta_puente.pack(anchor=tk.W, fill=tk.X) self.entry_ta_puente.set("0.01") self.entry_ta_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_ta_puente.bind('', self.on_parameter_change_puente_nuevo) # tr (espesor refuerzo alma) ttk.Label(self.frame_puente_nuevo, text="tr (espesor refuerzo alma, m):").pack(anchor=tk.W, pady=(10, 0)) self.entry_tr_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_tr_puente.pack(anchor=tk.W, fill=tk.X) self.entry_tr_puente.set("0.01") self.entry_tr_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_tr_puente.bind('', self.on_parameter_change_puente_nuevo) # theta (ángulo refuerzo) ttk.Label(self.frame_puente_nuevo, text="theta (ángulo refuerzo, grados):").pack(anchor=tk.W, pady=(10, 0)) self.entry_theta_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=360.0, increment=1.0, width=15, command=self.on_parameter_change_puente_nuevo) self.entry_theta_puente.pack(anchor=tk.W, fill=tk.X) self.entry_theta_puente.set("45") self.entry_theta_puente.bind('', self.on_parameter_change_puente_nuevo) self.entry_theta_puente.bind('', self.on_parameter_change_puente_nuevo) # Frame para barrido paramétrico self.frame_sweep_tab = ttk.Frame(left_panel) self.create_sweep_tab_widgets() # Separador ttk.Separator(left_panel, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) # Panel de resultados ttk.Label(left_panel, text="Propiedades:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) self.text_results = tk.Text(left_panel, height=20, width=30, state=tk.DISABLED, font=("Courier", 8)) self.text_results.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Botón guardar ttk.Button(left_panel, text="Guardar nueva sección", command=self.save_new_section).pack(fill=tk.X) # ========== PANEL DERECHO (GRÁFICO) ========== right_panel = ttk.Frame(self) right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10) self.figure = plt.Figure(figsize=(6, 8), dpi=100) self.ax = self.figure.add_subplot(111) self.canvas = FigureCanvasTkAgg(self.figure, master=right_panel) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) # Cargar la primera sección después de que todos los widgets estén creados if self.secciones_data['secciones']: self.combo_seccion.current(0) self.on_section_changed(None) self.frame_puente_nuevo.pack_forget() def create_sweep_tab_widgets(self): """Crea los widgets del panel de barrido paramétrico""" self.frame_sweep_tab.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Título ttk.Label(self.frame_sweep_tab, text="Barrido Paramétrico", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5)) # Sección actual ttk.Label(self.frame_sweep_tab, text="Sección: IPE", font=("Arial", 9)).pack(anchor=tk.W, pady=(5, 10)) # Frame para parámetros fijos frame_fixed = ttk.LabelFrame(self.frame_sweep_tab, text="Parámetros Fijos", padding=5) frame_fixed.pack(fill=tk.X, pady=(0, 10)) # Variables para toggle fijo/barrido self.sweep_toggle_vars = {} self.sweep_fixed_entries = {} self.sweep_range_entries = {} params_ipe = ['H', 'b', 'tf', 'tw'] for param in params_ipe: container = ttk.Frame(frame_fixed) container.pack(fill=tk.X, pady=2) var = tk.BooleanVar(value=True) self.sweep_toggle_vars[param] = var chk = ttk.Checkbutton(container, text=f"Fijo: {param}", variable=var, command=lambda p=param: self.on_sweep_toggle(p)) chk.pack(side=tk.LEFT, fill=tk.X, expand=True) entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=10) entry.pack(side=tk.LEFT, padx=5) self.sweep_fixed_entries[param] = entry # Cargar valores actuales self.sweep_fixed_entries['H'].set("0.300") self.sweep_fixed_entries['b'].set("0.150") self.sweep_fixed_entries['tf'].set("0.0107") self.sweep_fixed_entries['tw'].set("0.0063") # Frame para parámetros a barrer frame_sweep = ttk.LabelFrame(self.frame_sweep_tab, text="Parámetros a Barrer", padding=5) frame_sweep.pack(fill=tk.X, pady=(0, 10)) self.sweep_range_labels = {} self.sweep_range_entries = {} for param in params_ipe: container = ttk.Frame(frame_sweep) container.pack(fill=tk.X, pady=2) lbl = ttk.Label(container, text=f"{param}:", width=5) lbl.pack(side=tk.LEFT) self.sweep_range_labels[param] = lbl ttk.Label(container, text="min:").pack(side=tk.LEFT, padx=(10, 2)) min_entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=8) min_entry.pack(side=tk.LEFT, padx=2) ttk.Label(container, text="max:").pack(side=tk.LEFT, padx=(10, 2)) max_entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=8) max_entry.pack(side=tk.LEFT, padx=2) ttk.Label(container, text="pasos:").pack(side=tk.LEFT, padx=(10, 2)) steps_entry = ttk.Spinbox(container, from_=2, to=20, increment=1, width=8) steps_entry.pack(side=tk.LEFT, padx=2) steps_entry.set(5) self.sweep_range_entries[param] = { 'min': min_entry, 'max': max_entry, 'steps': steps_entry } # Botones de control button_frame = ttk.Frame(self.frame_sweep_tab) button_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Button(button_frame, text="Ejecutar Barrido", command=self.execute_parametric_sweep).pack(side=tk.LEFT, padx=2) ttk.Button(button_frame, text="Exportar Excel", command=self.export_sweep_results).pack(side=tk.LEFT, padx=2) ttk.Button(button_frame, text="Limpiar", command=self.clear_sweep_results).pack(side=tk.LEFT, padx=2) # Ocultar frame de barrido inicialmente self.frame_sweep_tab.pack_forget() def on_sweep_toggle(self, param): """Alterna parámetro entre fijo y barrido""" is_fixed = self.sweep_toggle_vars[param].get() # Los rangos se mostrarán solo si no está fijo # Esta lógica se puede mejorar si es necesario def on_section_type_changed(self, event): """Cambia entre IPE, Personalizada, Puente nuevo y Barrido""" section_type = self.combo_tipo.get() if section_type == "IPE": self.frame_personalizada.pack_forget() self.frame_puente_nuevo.pack_forget() self.frame_sweep_tab.pack_forget() self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10)) self.on_parameter_change_ipe(None) elif section_type == "Barrido Paramétrico": self.frame_ipe.pack_forget() self.frame_personalizada.pack_forget() self.frame_puente_nuevo.pack_forget() self.frame_sweep_tab.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Precargar valores actuales como fijos if self.current_properties: try: self.sweep_fixed_entries['H'].delete(0, tk.END) self.sweep_fixed_entries['H'].insert(0, f"{float(self.entry_H.get()):.6f}") self.sweep_fixed_entries['b'].delete(0, tk.END) self.sweep_fixed_entries['b'].insert(0, f"{float(self.entry_b.get()):.6f}") self.sweep_fixed_entries['tf'].delete(0, tk.END) self.sweep_fixed_entries['tf'].insert(0, f"{float(self.entry_tf.get()):.6f}") self.sweep_fixed_entries['tw'].delete(0, tk.END) self.sweep_fixed_entries['tw'].insert(0, f"{float(self.entry_tw.get()):.6f}") except: pass elif section_type == "Puente nuevo": self.frame_ipe.pack_forget() self.frame_personalizada.pack_forget() self.frame_sweep_tab.pack_forget() self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) self.on_parameter_change_puente_nuevo(None) else: # Personalizada self.frame_ipe.pack_forget() self.frame_puente_nuevo.pack_forget() self.frame_sweep_tab.pack_forget() self.frame_personalizada.pack(fill=tk.BOTH, expand=True) if self.current_points is not None: import json puntos_json = json.dumps(self.current_points.tolist(), indent=2) self.text_puntos.delete(1.0, tk.END) self.text_puntos.insert(1.0, puntos_json) def load_custom_points(self): """Carga puntos personalizados desde el texto""" try: import json text = self.text_puntos.get(1.0, tk.END).strip() puntos = json.loads(text) self.current_points = np.array(puntos) self.current_properties = calculate_section_properties(self.current_points) self.update_plot() self.update_results() messagebox.showinfo("Éxito", "Puntos cargados correctamente") except json.JSONDecodeError: messagebox.showerror("Error", "Formato JSON inválido") except Exception as e: messagebox.showerror("Error", f"Error al cargar puntos: {str(e)}") def on_section_changed(self, event): """Se ejecuta cuando cambia la sección seleccionada""" idx = self.combo_seccion.current() if idx >= 0: seccion = self.secciones_data['secciones'][idx] self.current_section_name = seccion['nombre'] section_type = seccion.get('tipo', 'otro') # Actualizar el combo de tipo if section_type == 'ipe': self.combo_tipo.set("IPE") elif section_type == 'puente_nuevo': self.combo_tipo.set("Puente nuevo") else: self.combo_tipo.set("Personalizada") # Si tiene puntos directos, cargarlos if 'puntos' in seccion: self.current_points = np.array(seccion['puntos']) if section_type == 'ipe': self.current_properties = calculate_section_properties(self.current_points) if section_type == 'puente_nuevo': self.current_properties = calculate_section_properties(self.current_points) self.update_plot() self.update_results() # Si además es IPE, establecer los valores if section_type == 'ipe' and 'parametros' in seccion: params = seccion['parametros'] self.entry_H.delete(0, tk.END) self.entry_H.insert(0, f"{params['H']:.6f}") self.entry_b.delete(0, tk.END) self.entry_b.insert(0, f"{params['b']:.6f}") self.entry_tf.delete(0, tk.END) self.entry_tf.insert(0, f"{params['tf']:.6f}") self.entry_tw.delete(0, tk.END) self.entry_tw.insert(0, f"{params['tw']:.6f}") # Mostrar frame IPE self.frame_personalizada.pack_forget() self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10)) elif section_type == 'puente_nuevo' and 'parametros' in seccion: params = seccion['parametros'] self.entry_h_puente.delete(0, tk.END) self.entry_h_puente.insert(0, f"{params['h']:.6f}") self.entry_b_puente.delete(0, tk.END) self.entry_b_puente.insert(0, f"{params['b']:.6f}") self.entry_tf_puente.delete(0, tk.END) self.entry_tf_puente.insert(0, f"{params['tf']:.6f}") self.entry_tw_puente.delete(0, tk.END) self.entry_tw_puente.insert(0, f"{params['tw']:.6f}") self.entry_ha_puente.delete(0, tk.END) self.entry_ha_puente.insert(0, f"{params['ha']:.6f}") self.entry_ta_puente.delete(0, tk.END) self.entry_ta_puente.insert(0, f"{params['ta']:.6f}") self.entry_tr_puente.delete(0, tk.END) self.entry_tr_puente.insert(0, f"{params['tr']:.6f}") self.entry_theta_puente.delete(0, tk.END) self.entry_theta_puente.insert(0, f"{params['theta']:.6f}") # Mostrar frame puente nuevo self.frame_ipe.pack_forget() self.frame_personalizada.pack_forget() self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) else: # Mostrar frame personalizado self.frame_ipe.pack_forget() self.frame_puente_nuevo.pack_forget() self.frame_personalizada.pack(fill=tk.BOTH, expand=True) import json puntos_json = json.dumps(self.current_points.tolist(), indent=2) self.text_puntos.delete(1.0, tk.END) self.text_puntos.insert(1.0, puntos_json) # Si solo tiene parámetros IPE, generar puntos elif section_type == 'ipe' and 'parametros' in seccion: params = seccion['parametros'] self.combo_tipo.set("IPE") self.entry_H.delete(0, tk.END) self.entry_H.insert(0, f"{params['H']:.6f}") self.entry_b.delete(0, tk.END) self.entry_b.insert(0, f"{params['b']:.6f}") self.entry_tf.delete(0, tk.END) self.entry_tf.insert(0, f"{params['tf']:.6f}") self.entry_tw.delete(0, tk.END) self.entry_tw.insert(0, f"{params['tw']:.6f}") self.on_parameter_change_ipe(None) elif section_type == 'puente_nuevo' and 'parametros' in seccion: params = seccion['parametros'] self.combo_tipo.set("Puente nuevo") self.entry_h_puente.delete(0, tk.END) self.entry_h_puente.insert(0, f"{params['h']:.6f}") self.entry_b_puente.delete(0, tk.END) self.entry_b_puente.insert(0, f"{params['b']:.6f}") self.entry_tf_puente.delete(0, tk.END) self.entry_tf_puente.insert(0, f"{params['tf']:.6f}") self.entry_tw_puente.delete(0, tk.END) self.entry_tw_puente.insert(0, f"{params['tw']:.6f}") self.entry_ha_puente.delete(0, tk.END) self.entry_ha_puente.insert(0, f"{params['ha']:.6f}") self.entry_ta_puente.delete(0, tk.END) self.entry_ta_puente.insert(0, f"{params['ta']:.6f}") self.entry_tr_puente.delete(0, tk.END) self.entry_tr_puente.insert(0, f"{params['tr']:.6f}") self.entry_theta_puente.delete(0, tk.END) self.entry_theta_puente.insert(0, f"{params['theta']:.6f}") self.on_parameter_change_puente_nuevo(None) def on_parameter_change_ipe(self, event=None): """Se ejecuta cuando cambien los valores de entrada""" try: H = float(self.entry_H.get()) b = float(self.entry_b.get()) tf = float(self.entry_tf.get()) tw = float(self.entry_tw.get()) # Validar valores básicos if H <= 0 or b <= 0 or tf <= 0 or tw <= 0: return if tf > H/2 or tw > b: tf = min(tf, H/2) tw = min(tw, b) self.entry_tf.set(f"{tf:.6f}") self.entry_tw.set(f"{tw:.6f}") # Generar puntos y calcular self.current_points = generate_ipe_points(H, b, tf, tw) self.current_properties = calculate_section_properties(self.current_points) # Actualizar visualización self.update_plot() self.update_results() except ValueError: # Ignorar si los valores no son números válidos pass def on_parameter_change_puente_nuevo(self, event=None): """Se ejecuta cuando cambien los valores de entrada""" try: h = float(self.entry_h_puente.get()) b = float(self.entry_b_puente.get()) tf = float(self.entry_tf_puente.get()) tw = float(self.entry_tw_puente.get()) ha = float(self.entry_ha_puente.get()) ta = float(self.entry_ta_puente.get()) tr = float(self.entry_tr_puente.get()) theta = float(self.entry_theta_puente.get()) # Validar valores básicos if h <= 0 or b <= 0 or tf <= 0 or tw <= 0 or ha <= 0 or ta <= 0 or tr <= 0: return # Generar puntos y calcular self.current_points = generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta) self.current_properties = calculate_section_properties(self.current_points) # Actualizar visualización self.update_plot() self.update_results() except ValueError: # Ignorar si los valores no son números válidos pass def update_plot(self): """Actualiza el gráfico de la sección""" self.ax.clear() points = self.current_points props = self.current_properties px = points[:, 0] py = points[:, 1] xg = props['xg_orig'] yg = props['yg_orig'] bmax = props['bmax'] hmax = props['hmax'] tetha = props['tetha'] # Dibujar sección self.ax.plot(px, py, 'b-', linewidth=2) self.ax.fill(px, py, facecolor="lightblue", alpha=0.5) # Centro de gravedad self.ax.plot(xg, yg, 'ro', markersize=8, label='CDG') # Ejes principales tt = min(bmax, hmax) self.ax.arrow(xg, yg, tt/3, 0, head_width=tt/30, head_length=tt/30, fc='green', ec='green') self.ax.arrow(xg, yg, 0, tt/3, head_width=tt/30, head_length=tt/30, fc='green', ec='green') tet = tetha * (math.pi) / 180 self.ax.arrow(xg, yg, tt/3*math.cos(tet), tt/3*math.sin(tet), head_width=tt/30, head_length=tt/30, fc='red', ec='red') self.ax.arrow(xg, yg, -tt/3*math.sin(tet), tt/3*math.cos(tet), head_width=tt/30, head_length=tt/30, fc='red', ec='red') self.ax.set_aspect('equal') self.ax.grid(True, alpha=0.3) self.ax.set_xlabel('X (m)', fontweight='bold') self.ax.set_ylabel('Y (m)', fontweight='bold') self.ax.set_title(self.current_section_name or 'Sección IPE', fontweight='bold') self.ax.legend() self.figure.tight_layout() self.canvas.draw() def update_results(self): """Actualiza el panel de resultados""" props = self.current_properties results_text = f"""PROPIEDADES CALCULADAS GEOMÉTRICAS: Área: {props['area']:.2f} cm² Perímetro: {props['perimetro']:.2f} cm b_max: {props['bmax']*100:.2f} cm h_max: {props['hmax']*100:.2f} cm CDG (ref. origen): X: {props['xg']:.2f} cm Y: {props['yg']:.2f} cm INERCIA (eje Xg-Yg): Ixg: {props['ixg']:.2f} cm⁴ Iyg: {props['iyg']:.2f} cm⁴ Pxyg: {props['pxyg']:.2f} cm⁴ rx: {props['rx']:.2f} cm ry: {props['ry']:.2f} cm INERCIA (ejes principales): Imax: {props['imax']:.2f} cm⁴ Imin: {props['imin']:.2f} cm⁴ θ: {props['tetha']:.2f}° rmax: {props['rmax']:.2f} cm rmin: {props['rmin']:.2f} cm MÓDULO RESISTENTE: Wel1x: {props['wel1x']:.2f} cm³ Wel2x: {props['wel2x']:.2f} cm³ Wel1y: {props['wel1y']:.2f} cm³ Wel2y: {props['wel2y']:.2f} cm³ MECÁNICAS: Peso: {props['peso']:.2f} kg/m Nel: {props['nel']:.2f} kN Melx: {props['melx']:.2f} kN·m Mely: {props['mely']:.2f} kN·m """ self.text_results.config(state=tk.NORMAL) self.text_results.delete(1.0, tk.END) self.text_results.insert(1.0, results_text) self.text_results.config(state=tk.DISABLED) def save_new_section(self): """Guarda la sección actual en el JSON""" if not self.current_section_name or self.current_points is None: messagebox.showerror("Error", "Selecciona una sección primero") return # Preguntar nombre de la nueva sección new_name = tk.simpledialog.askstring("Guardar sección", "Nombre de la nueva sección:") if not new_name: return # Verificar si ya existe exists = any(s['nombre'] == new_name for s in self.secciones_data['secciones']) if exists: should_overwrite = messagebox.askyesno("Ya existe", f"La sección '{new_name}' ya existe. ¿Sobreescribir?") if not should_overwrite: return # Detectar tipo de sección section_type = self.combo_tipo.get().lower() if section_type == "ipe": section_type = "ipe" elif section_type == "puente nuevo": section_type = "puente_nuevo" else: section_type = "otro" # Crear nueva sección con puntos new_section = { 'nombre': new_name, 'tipo': section_type, 'puntos': self.current_points.tolist() # Guardar los puntos actuales } # Si es IPE, guardar también los parámetros if section_type == "ipe": try: H = float(self.entry_H.get()) b = float(self.entry_b.get()) tf = float(self.entry_tf.get()) tw = float(self.entry_tw.get()) if H > 0 and b > 0 and tf > 0 and tw > 0: # Validar que sean mayores a 0 new_section['parametros'] = { 'H': H, 'b': b, 'tf': tf, 'tw': tw } except: pass # Si no se pueden leer, solo guardamos puntos elif section_type == "puente_nuevo": try: h = float(self.entry_h_puente.get()) b = float(self.entry_b_puente.get()) tf = float(self.entry_tf_puente.get()) tw = float(self.entry_tw_puente.get()) ha = float(self.entry_ha_puente.get()) ta = float(self.entry_ta_puente.get()) tr = float(self.entry_tr_puente.get()) theta = float(self.entry_theta_puente.get()) if all(v > 0 for v in [h, b, tf, tw, ha, ta, tr]) and 0 <= theta <= 360: new_section['parametros'] = { 'h': h, 'b': b, 'tf': tf, 'tw': tw, 'ha': ha, 'ta': ta, 'tr': tr, 'theta': theta } except: pass # Si no se pueden leer, solo guardamos puntos # Eliminar si existe self.secciones_data['secciones'] = [s for s in self.secciones_data['secciones'] if s['nombre'] != new_name] self.secciones_data['secciones'].append(new_section) self.save_json() # Actualizar combo self.combo_seccion['values'] = [s['nombre'] for s in self.secciones_data['secciones']] self.combo_seccion.set(new_name) messagebox.showinfo("Éxito", f"Sección '{new_name}' guardada correctamente") # ==================== MÉTODOS DE BARRIDO PARAMÉTRICO ==================== def execute_parametric_sweep(self): """Ejecuta el barrido paramétrico""" try: # Recopilar configuración fixed_params = {} sweep_configs = {} for param in ['H', 'b', 'tf', 'tw']: is_fixed = self.sweep_toggle_vars[param].get() if is_fixed: value = float(self.sweep_fixed_entries[param].get()) fixed_params[param] = value else: min_val = float(self.sweep_range_entries[param]['min'].get()) max_val = float(self.sweep_range_entries[param]['max'].get()) steps = int(self.sweep_range_entries[param]['steps'].get()) if min_val >= max_val: messagebox.showerror("Error", f"Rango inválido para {param}: min >= max") return sweep_configs[param] = { 'min': min_val, 'max': max_val, 'steps': steps } if not sweep_configs: messagebox.showerror("Error", "Debe barrer al menos un parámetro") return if not fixed_params: messagebox.showwarning("Advertencia", "No hay parámetros fijos") # Ejecutar barrido messagebox.showinfo("Info", "Ejecutando barrido... Esto puede tomar unos segundos") sweep = ParametricSweep("ipe", fixed_params, sweep_configs) self.sweep_result = sweep.execute_sweep() self.sweep_pareto = sweep.find_pareto_front() # Calcular combinaciones teóricas vs válidas total_combos = 1 for config in sweep_configs.values(): total_combos *= config['steps'] valid_combos = len(self.sweep_result) invalid_combos = total_combos - valid_combos msg = f"✓ Barrido completado:\n" msg += f" Combinaciones teóricas: {total_combos}\n" msg += f" Combinaciones válidas: {valid_combos}\n" if invalid_combos > 0: msg += f" Rechazadas (geometría): {invalid_combos}\n" msg += f"\n✓ Soluciones ordenadas: {len(self.sweep_pareto)}" messagebox.showinfo("Éxito", msg) # Mostrar resultados self.show_sweep_results() except Exception as e: messagebox.showerror("Error", f"Error en barrido: {str(e)}") def show_sweep_results(self): """Muestra los resultados del barrido ordenados por eficiencia""" if self.sweep_result is None or self.sweep_pareto is None: messagebox.showerror("Error", "Ejecute primero el barrido") return try: # Crear figura con dos subplots if self.sweep_figure is not None: plt.close(self.sweep_figure) self.sweep_figure = plt.Figure(figsize=(14, 6), dpi=100) # Subplot 1: Visualización de soluciones ax1 = self.sweep_figure.add_subplot(121) # Obtener parámetros barridos sweep_params = [p for p in ['H', 'b', 'tf', 'tw'] if p in self.sweep_result.columns and self.sweep_result[p].nunique() > 1] scatter = None # Variable para almacenar scatter si existe if len(sweep_params) >= 2: # Scatter plot: Peso vs Eficiencia scatter = ax1.scatter(self.sweep_result['peso'], self.sweep_result['eficiencia'], c=self.sweep_result['ixg'], cmap='viridis', s=100, alpha=0.6, edgecolors='black') # Marcar el más eficiente con un símbolo especial (diamante dorado) most_efficient = self.sweep_pareto.iloc[0] ax1.scatter(most_efficient['peso'], most_efficient['eficiencia'], marker='D', s=400, c='gold', edgecolors='orange', linewidth=2.5, label='Óptimo', zorder=6) self.sweep_figure.colorbar(scatter, ax=ax1, label='Ix (cm⁴)') ax1.set_xlabel('Peso (kg/m)', fontweight='bold', fontsize=11) ax1.set_ylabel('Eficiencia = (Ix+Iy)/Peso*1000', fontweight='bold', fontsize=11) ax1.set_title('Soluciones: Peso vs Eficiencia\n(Dorado = Óptimo)', fontweight='bold') ax1.grid(True, alpha=0.3) ax1.legend(loc='best') elif len(sweep_params) == 1: # Plot simple si solo 1 parámetro ax1.plot(self.sweep_result[sweep_params[0]], self.sweep_result['eficiencia'], 'b.-', label='Todas', linewidth=2, markersize=8) top5 = self.sweep_pareto.head(5) ax1.plot(top5[sweep_params[0]], top5['eficiencia'], 'r*', markersize=20, label='Top 5', zorder=5) # Marcar el más eficiente most_efficient = self.sweep_pareto.iloc[0] ax1.plot(most_efficient[sweep_params[0]], most_efficient['eficiencia'], 'D', color='gold', markersize=5, markeredgecolor='orange', markeredgewidth=2, label='Más Eficiente', zorder=6) ax1.set_xlabel(f'{sweep_params[0]} (m)', fontweight='bold', fontsize=11) ax1.set_ylabel('Eficiencia', fontweight='bold', fontsize=11) ax1.set_title(f'Eficiencia vs {sweep_params[0]}', fontweight='bold') ax1.legend() ax1.grid(True, alpha=0.3) # Subplot 2: Tabla top 10 ax2 = self.sweep_figure.add_subplot(122) ax2.axis('tight') ax2.axis('off') # Preparar datos para tabla cols_to_show = [c for c in ['H', 'b', 'tf', 'tw', 'peso', 'ixg', 'iyg', 'eficiencia'] if c in self.sweep_pareto.columns] top_pareto = self.sweep_pareto.head(10)[cols_to_show].copy() # Formatear números table_data = [] for rank, (_, row) in enumerate(top_pareto.iterrows(), 1): formatted_row = [str(rank)] # Ranking for col in cols_to_show: val = row[col] if col in ['H', 'b', 'tf', 'tw']: formatted_row.append(f"{val:.4f}") else: formatted_row.append(f"{val:.2f}") table_data.append(formatted_row) col_labels = ['#'] + list(cols_to_show) table = ax2.table(cellText=table_data, colLabels=col_labels, cellLoc='center', loc='center', bbox=[0, 0, 1, 1]) table.auto_set_font_size(False) table.set_fontsize(8) table.scale(1, 1.8) # Colorear header for i in range(len(col_labels)): table[(0, i)].set_facecolor('#40466e') table[(0, i)].set_text_props(weight='bold', color='white') # Colorear filas alternadas for i in range(1, len(table_data) + 1): color = '#f0f0f0' if i % 2 == 0 else 'white' for j in range(len(col_labels)): table[(i, j)].set_facecolor(color) ax2.set_title('Top 10 Soluciones\n(Ordenadas por Eficiencia)', fontweight='bold', fontsize=11, pad=20) self.sweep_figure.tight_layout() # Mostrar en canvas if self.sweep_canvas is not None: self.sweep_canvas.get_tk_widget().destroy() right_panel = self.canvas.get_tk_widget().master self.sweep_canvas = FigureCanvasTkAgg(self.sweep_figure, master=right_panel) self.sweep_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.sweep_canvas.draw() # Agregar interactividad con hover DESPUÉS de crear el canvas if scatter is not None: self._setup_scatter_hover(ax1, scatter) except Exception as e: messagebox.showerror("Error", f"Error mostrando resultados: {str(e)}") import traceback traceback.print_exc() def _setup_scatter_hover(self, ax, scatter): """Configura interactividad con hover para mostrar valores de puntos""" # Almacenar referencias a los datos self.scatter_ax = ax self.scatter_data = self.sweep_result.copy() self.hover_annotation = None def on_hover(event): """Muestra información cuando el mouse pasa sobre puntos del scatter""" if event.inaxes != self.scatter_ax or len(self.scatter_data) == 0: if self.hover_annotation is not None: self.hover_annotation.remove() self.hover_annotation = None self.sweep_canvas.draw_idle() return # Calcular distancias a todos los puntos x, y = event.xdata, event.ydata if x is None or y is None: return # Distancias en coordenadas de display para mejor detección try: peso_data = self.scatter_data['peso'].values eficiencia_data = self.scatter_data['eficiencia'].values # Convertir coordenadas de datos a display x_display, y_display = self.scatter_ax.transData.transform((x, y)) puntos_display = self.scatter_ax.transData.transform( np.c_[peso_data, eficiencia_data] ) # Calcular distancia en píxeles dist = np.sqrt((puntos_display[:, 0] - x_display)**2 + (puntos_display[:, 1] - y_display)**2) # Umbral de 20 píxeles para detectar hover idx_cercano = np.argmin(dist) if dist[idx_cercano] < 20: # Obtener datos del punto row = self.scatter_data.iloc[idx_cercano] # Construir texto con información texto = f"Peso: {row['peso']:.2f} kg/m\n" texto += f"Eficiencia: {row['eficiencia']:.2f}\n" # Agregar parámetros que varían for param in ['H', 'b', 'tf', 'tw']: if param in self.scatter_data.columns and self.scatter_data[param].nunique() > 1: texto += f"{param}: {row[param]:.4f}\n" texto += f"Ix: {row['ixg']:.0f} cm⁴\n" texto += f"Iy: {row['iyg']:.0f} cm⁴" # Crear o actualizar anotación if self.hover_annotation is not None: self.hover_annotation.remove() self.hover_annotation = self.scatter_ax.annotate( texto, xy=(row['peso'], row['eficiencia']), xytext=(10, 10), textcoords='offset points', bbox=dict(boxstyle='round,pad=0.5', facecolor='yellow', alpha=0.7), fontsize=8, arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0', color='black') ) self.sweep_canvas.draw_idle() else: # Mover el mouse lejos de los puntos if self.hover_annotation is not None: self.hover_annotation.remove() self.hover_annotation = None self.sweep_canvas.draw_idle() except Exception: pass # Conectar evento de movimiento del mouse self.sweep_canvas.mpl_connect('motion_notify_event', on_hover) def export_sweep_results(self): """Exporta resultados del barrido a Excel""" if self.sweep_result is None: messagebox.showerror("Error", "No hay resultados para exportar. Ejecute primero el barrido") return try: # Pedir ruta de guardado filename = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")] ) if not filename: return # Crear archivo Excel con pandas with pd.ExcelWriter(filename, engine='openpyxl') as writer: # Hoja 1: Todos los resultados self.sweep_result.to_excel(writer, sheet_name='Resultados', index=False) # Hoja 2: Pareto front if self.sweep_pareto is not None: self.sweep_pareto.to_excel(writer, sheet_name='Pareto Front', index=False) messagebox.showinfo("Éxito", f"Resultados exportados a:\n{filename}") except Exception as e: messagebox.showerror("Error", f"Error exportando: {str(e)}") def clear_sweep_results(self): """Limpia los resultados del barrido""" self.sweep_result = None self.sweep_pareto = None if self.sweep_canvas is not None: self.sweep_canvas.get_tk_widget().destroy() self.sweep_canvas = None # Volver a gráfico normal self.ax.clear() self.canvas.draw() messagebox.showinfo("Info", "Resultados de barrido limpiados") if __name__ == "__main__": app = SectionDesignerApp() app.mainloop()