| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339 |
- 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('<<ComboboxSelected>>', 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('<<ComboboxSelected>>', 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('<FocusOut>', self.on_parameter_change_ipe)
- self.entry_H.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_ipe)
- self.entry_b.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_ipe)
- self.entry_tf.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_ipe)
- self.entry_tw.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_h_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_b_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_tf_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_tw_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_ha_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_ta_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_tr_puente.bind('<Return>', 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('<FocusOut>', self.on_parameter_change_puente_nuevo)
- self.entry_theta_puente.bind('<Return>', 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()
|