app_designer.py 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339
  1. import tkinter as tk
  2. from tkinter import ttk, messagebox, simpledialog, filedialog
  3. import json
  4. import numpy as np
  5. import math
  6. import matplotlib.pyplot as plt
  7. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  8. import pandas as pd
  9. import os
  10. from parametric_sweep import ParametricSweep
  11. # ==================== CONSTANTES DE MATERIAL ====================
  12. DENS = 7850 # kg/m3
  13. FY = 355 # MPa
  14. GAMMAS = 1.05 # Factor seguridad acero
  15. EYOUNG = 210000 # MPa
  16. NU = 0.3 # Coef Poisson
  17. FYD = FY * 10**6 / GAMMAS # Pa
  18. EYOUNG_PA = EYOUNG * 10**6 # Pa
  19. G = EYOUNG_PA / (2 * (1 + NU)) # Pa
  20. # ==================== FUNCIONES DE CÁLCULO ====================
  21. def generate_ipe_points(H, b, tf, tw):
  22. """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
  23. # H: altura total, b: ancho flange, tf: espesor flange, tw: espesor alma
  24. h_alma = H - 2*tf
  25. x_alma_ini = (b - tw) / 2
  26. x_alma_fin = (b + tw) / 2
  27. # Puntos de la sección (sentido horario)
  28. points = np.array([
  29. [0, 0], # Esquina inferior izquierda flange
  30. [b, 0], # Esquina inferior derecha flange
  31. [b, tf], # Unión alma-flange derecha
  32. [x_alma_fin, tf], # Esquina superior alma derecha
  33. [x_alma_fin, H - tf], # Esquina inferior alma derecha (arriba)
  34. [b, H - tf], # Unión alma-flange arriba
  35. [b, H], # Esquina superior derecha flange
  36. [0, H], # Esquina superior izquierda flange
  37. [0, H - tf], # Unión alma-flange arriba izquierda
  38. [x_alma_ini, H - tf], # Esquina inferior alma izquierda (arriba)
  39. [x_alma_ini, tf], # Esquina superior alma izquierda
  40. [0, tf], # Unión alma-flange izquierda
  41. [0, 0], # Cerrar la sección
  42. ])
  43. return points
  44. def generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta):
  45. """Genera los puntos de la sección tipo puente nuevo a partir de parámetros normalizados"""
  46. # h: altura exterior, b: ancho total, tf: espesor ala, tw: espesor alma,
  47. hr = (b-tw) / 2 * math.tan(math.radians(theta))
  48. hl = tr / math.cos(math.radians(theta))
  49. hr_ = (b/2 - ta - tw/2)* math.tan(math.radians(theta))
  50. # Puntos de la sección (sentido horario)
  51. points = np.array([
  52. [0, 0],
  53. [b, 0],
  54. [b, tf + ha],
  55. [(b + tw)/2, tf + ha + hr],
  56. [(b + tw)/2, tf + ha + hr - hl],
  57. [b - ta, tf + ha + hr - hl - hr_],
  58. [b - ta, tf],
  59. [(b + tw)/2, tf],
  60. [(b + tw)/2, h - tf],
  61. [b - ta, h - tf],
  62. [b - ta, h - tf - ha - hr + hl + hr_],
  63. [(b + tw)/2, h - tf - ha - hr + hl],
  64. [(b + tw)/2, h - tf - ha - hr],
  65. [b, h - tf - ha],
  66. [b, h],
  67. [0, h],
  68. [0, h - tf - ha],
  69. [(b - tw)/2, h - tf - ha - hr],
  70. [(b - tw)/2, h - tf - ha - hr + hl],
  71. [ta, h - tf - ha - hr + hl + hr_],
  72. [ta, h - tf],
  73. [(b - tw)/2, h - tf],
  74. [(b - tw)/2, tf],
  75. [ta, tf],
  76. [ta, tf + ha + hr - hl - hr_],
  77. [(b - tw)/2, tf + ha + hr - hl],
  78. [(b - tw)/2, tf + ha + hr],
  79. [0, tf + ha],
  80. [0, 0],
  81. ])
  82. return points
  83. def calculate_section_properties(points):
  84. """Calcula todas las propiedades de la sección - CÓDIGO ORIGINAL SIN MODIFICAR"""
  85. npuntos = len(points)
  86. puntos = points
  87. px = puntos[:, 0]
  88. py = puntos[:, 1]
  89. # ANCHO Y CANTO MÁXIMO
  90. bmax = np.amax(px) - np.amin(px)
  91. hmax = np.amax(py) - np.amin(py)
  92. # PERÍMETRO
  93. long_i = np.zeros(npuntos - 1)
  94. for i in range(npuntos - 1):
  95. long_i[i] = ((puntos[i+1, 0] - puntos[i, 0])**2 + (puntos[i+1, 1] - puntos[i, 1])**2)**(1/2)
  96. perimetro = abs(sum(long_i))
  97. # ÁREA
  98. area_i = np.zeros(npuntos - 1)
  99. for i in range(npuntos - 1):
  100. area_i[i] = (puntos[i+1, 0] - puntos[i, 0]) * (puntos[i+1, 1] + puntos[i, 1]) / 2
  101. area = abs(sum(area_i))
  102. # PESO POR METRO
  103. peso = area * DENS
  104. # CENTRO DE GRAVEDAD
  105. cdg_i = np.zeros([npuntos, 2])
  106. for i in range(npuntos - 1):
  107. h1 = puntos[i, 1]
  108. h2 = puntos[i+1, 1]
  109. b = puntos[i+1, 0] - puntos[i, 0]
  110. d = puntos[i, 0]
  111. if h1 + h2 == 0:
  112. cdg_i[i, 1] = 0
  113. else:
  114. cdg_i[i, 1] = 1/3 * (h1*h1 + h1*h2 + h2*h2) / (h1 + h2)
  115. if h1 + h2 == 0:
  116. cdg_i[i, 0] = d + b/2
  117. else:
  118. cdg_i[i, 0] = d + b/3 * (h1 + 2*h2) / (h1 + h2)
  119. # MOMENTO ESTÁTICO
  120. statico_i = np.zeros([npuntos, 2])
  121. for i in range(npuntos - 1):
  122. statico_i[i, 1] = area_i[i] * cdg_i[i, 1]
  123. statico_i[i, 0] = area_i[i] * cdg_i[i, 0]
  124. cdg = sum(statico_i) / sum(area_i)
  125. xg = cdg[0]
  126. yg = cdg[1]
  127. # FIBRAS MÁS ALEJADAS
  128. v1y = np.amax(py) - yg
  129. v2y = np.amin(py) - yg
  130. v1x = np.amax(px) - xg
  131. v2x = np.amin(px) - xg
  132. # MOMENTOS DE INERCIA
  133. inercia_i = np.zeros([npuntos, 3])
  134. for i in range(npuntos - 1):
  135. h1 = puntos[i, 1]
  136. h2 = puntos[i+1, 1]
  137. b = puntos[i+1, 0] - puntos[i, 0]
  138. d = puntos[i, 0]
  139. xgi = cdg_i[i, 0]
  140. ygi = cdg_i[i, 1]
  141. ai = area_i[i]
  142. # Ixg
  143. if h2 >= h1:
  144. ixcuad_G_local = 1/12 * b * (h1**3) + b * h1 * (h1/2 - ygi)**2
  145. ixtriang_G_loc = 1/36 * b * (h2-h1)**3 + 1/2 * b * (h2-h1) * ((2*h1+h2)/3 - ygi)**2
  146. else:
  147. ixcuad_G_local = 1/12 * b * (h2**3) + b * h2 * (h2/2 - ygi)**2
  148. ixtriang_G_loc = 1/36 * b * (h1-h2)**3 + 1/2 * b * (h1-h2) * ((2*h2+h1)/3 - ygi)**2
  149. inercia_i[i, 0] = ixcuad_G_local + ixtriang_G_loc + ai * (yg - ygi)**2
  150. # Iyg
  151. if h2 >= h1:
  152. iycuad = 1/12 * h1 * b**3 + h1 * b * (b/2 + d - xgi)**2
  153. iytrian = 1/36 * (h2-h1) * b**3 + 1/2 * b * (h2-h1) * (2/3*b + d - xgi)**2
  154. else:
  155. iycuad = 1/12 * h2 * b**3 + h2 * b * (b/2 + d - xgi)**2
  156. iytrian = 1/36 * (h1-h2) * b**3 + 1/2 * b * (h1-h2) * (1/3*b + d - xgi)**2
  157. inercia_i[i, 1] = iycuad + iytrian + ai * (xg - xgi)**2
  158. # Pxyg
  159. if h2 >= h1:
  160. pxygcuadrado = b * h1 * (-h1/2 + ygi) * (-d - b/2 + xgi)
  161. pxytriangulo = b*b * (h2-h1)**2 / 72 + b * (h2-h1) / 2 * (-(h2-h1)/3 - h1 + ygi) * (-d - 2/3*b + xgi)
  162. else:
  163. pxygcuadrado = b * h2 * (-h2/2 + ygi) * (-d - b/2 + xgi)
  164. pxytriangulo = -b*b * (h1-h2)**2 / 72 + b * (h1-h2) / 2 * (-(h1-h2)/3 - h2 + ygi) * (-d - 1/3*b + xgi)
  165. inercia_i[i, 2] = pxygcuadrado + pxytriangulo + ai * (-xg + xgi) * (-yg + ygi)
  166. ig = sum(inercia_i)
  167. ixg = abs(ig[0])
  168. iyg = abs(ig[1])
  169. pxyg = ig[2]
  170. if sum(area_i) >= 0:
  171. pxyg = pxyg
  172. else:
  173. pxyg = -pxyg
  174. # RADIOS DE GIRO
  175. rx = (ixg / area)**0.5
  176. ry = (iyg / area)**0.5
  177. # EJES PRINCIPALES DE INERCIA
  178. ic = (ixg + iyg) / 2
  179. ir = (((ixg - iyg)/2)**2 + pxyg**2)**0.5
  180. imax = ic + ir
  181. imin = ic - ir
  182. rmax = (imax / area)**0.5
  183. rmin = (imin / area)**0.5
  184. # ORIENTACIÓN DE EJES PRINCIPALES
  185. rest = ixg - iyg
  186. if abs(rest) < 10**-12:
  187. tetha = 45
  188. else:
  189. tetha = 0.5 * math.atan((pxyg*2) / (ixg - iyg))
  190. tetha = abs(tetha) * 180 / math.pi
  191. if pxyg > 0:
  192. if ixg > iyg:
  193. tetha = -tetha
  194. else:
  195. tetha = tetha
  196. else:
  197. if ixg > iyg:
  198. tetha = tetha
  199. else:
  200. tetha = -tetha
  201. # MÓDULO RESISTENTE ELÁSTICO
  202. wel1x = abs(ixg / v1y)
  203. wel2x = abs(ixg / v2y)
  204. wel1y = abs(iyg / v1x)
  205. wel2y = abs(iyg / v2x)
  206. # AXIL Y MOMENTO ELÁSTICO
  207. nel = area * FYD
  208. melx = min(wel1x, wel2x) * FYD
  209. mely = min(wel1y, wel2y) * FYD
  210. # CONVERSIÓN A UNIDADES PRÁCTICAS (cm, cm2, cm3, cm4, kN)
  211. pot = 2
  212. bmax_cm = bmax * 10**pot
  213. hmax_cm = hmax * 10**pot
  214. perimetro_cm = perimetro * 10**pot
  215. xg_cm = xg * 10**pot
  216. yg_cm = yg * 10**pot
  217. v1y_cm = v1y * 10**pot
  218. v2y_cm = v2y * 10**pot
  219. v1x_cm = v1x * 10**pot
  220. v2x_cm = v2x * 10**pot
  221. rx_cm = rx * 10**pot
  222. ry_cm = ry * 10**pot
  223. rmax_cm = rmax * 10**pot
  224. rmin_cm = rmin * 10**pot
  225. area_cm2 = area * 10**(pot*2)
  226. wel1x_cm3 = wel1x * 10**(pot*3)
  227. wel2x_cm3 = wel2x * 10**(pot*3)
  228. wel1y_cm3 = wel1y * 10**(pot*3)
  229. wel2y_cm3 = wel2y * 10**(pot*3)
  230. ixg_cm4 = ixg * 10**(pot*4)
  231. iyg_cm4 = iyg * 10**(pot*4)
  232. pxyg_cm4 = pxyg * 10**(pot*4)
  233. imax_cm4 = imax * 10**(pot*4)
  234. imin_cm4 = imin * 10**(pot*4)
  235. nel_kn = nel / 1000
  236. melx_kn = melx / 1000
  237. mely_kn = mely / 1000
  238. return {
  239. 'area': area_cm2,
  240. 'perimetro': perimetro_cm,
  241. 'xg': xg_cm,
  242. 'yg': yg_cm,
  243. 'v1y': v1y_cm,
  244. 'v2y': v2y_cm,
  245. 'v1x': v1x_cm,
  246. 'v2x': v2x_cm,
  247. 'ixg': ixg_cm4,
  248. 'iyg': iyg_cm4,
  249. 'pxyg': pxyg_cm4,
  250. 'imax': imax_cm4,
  251. 'imin': imin_cm4,
  252. 'rx': rx_cm,
  253. 'ry': ry_cm,
  254. 'rmax': rmax_cm,
  255. 'rmin': rmin_cm,
  256. 'wel1x': wel1x_cm3,
  257. 'wel2x': wel2x_cm3,
  258. 'wel1y': wel1y_cm3,
  259. 'wel2y': wel2y_cm3,
  260. 'tetha': tetha,
  261. 'peso': peso,
  262. 'nel': nel_kn,
  263. 'melx': melx_kn,
  264. 'mely': mely_kn,
  265. 'xg_orig': xg,
  266. 'yg_orig': yg,
  267. 'bmax': bmax,
  268. 'hmax': hmax,
  269. 'points': points
  270. }
  271. # ==================== APLICACIÓN TKINTER ====================
  272. class SectionDesignerApp(tk.Tk):
  273. def __init__(self):
  274. super().__init__()
  275. self.title("Diseñador de Secciones de Acero")
  276. self.geometry("1200x800")
  277. self.json_path = r"c:\Users\Daniel.p\Documents\Automatizaciones\Propiedades seccion\secciones_config.json"
  278. self.load_json()
  279. self.current_section_name = None
  280. self.current_points = None
  281. self.current_properties = None
  282. # Variables para barrido paramétrico
  283. self.sweep_result = None # DataFrame de resultados
  284. self.sweep_pareto = None # DataFrame con Pareto front
  285. self.sweep_figure = None # Figura con gráficas
  286. self.sweep_canvas = None # Canvas para mostrar gráficas
  287. self.create_widgets()
  288. def load_json(self):
  289. """Carga el archivo JSON de configuración"""
  290. if os.path.exists(self.json_path):
  291. with open(self.json_path, 'r') as f:
  292. self.secciones_data = json.load(f)
  293. else:
  294. self.secciones_data = {"secciones": []}
  295. def save_json(self):
  296. """Guarda los cambios al JSON"""
  297. with open(self.json_path, 'w') as f:
  298. json.dump(self.secciones_data, f, indent=2)
  299. def create_widgets(self):
  300. """Crea la interfaz gráfica"""
  301. # ========== PANEL IZQUIERDO (CONTROLES) ==========
  302. left_panel = ttk.Frame(self, width=250)
  303. left_panel.pack(side=tk.LEFT, fill=tk.BOTH, padx=10, pady=10)
  304. # Selector de sección
  305. ttk.Label(left_panel, text="Sección:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  306. self.combo_seccion = ttk.Combobox(
  307. left_panel,
  308. values=[s['nombre'] for s in self.secciones_data['secciones']],
  309. state='readonly',
  310. width=20
  311. )
  312. self.combo_seccion.pack(anchor=tk.W)
  313. self.combo_seccion.bind('<<ComboboxSelected>>', self.on_section_changed)
  314. # Separador
  315. ttk.Separator(left_panel, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
  316. # Selector de tipo de sección
  317. ttk.Label(left_panel, text="Tipo de sección:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  318. self.combo_tipo = ttk.Combobox(
  319. left_panel,
  320. values=["IPE", "Personalizada", "Puente nuevo", "Barrido Paramétrico"],
  321. state='readonly',
  322. width=20
  323. )
  324. self.combo_tipo.pack(anchor=tk.W, pady=(0, 10))
  325. self.combo_tipo.set("IPE")
  326. self.combo_tipo.bind('<<ComboboxSelected>>', self.on_section_type_changed)
  327. # Frame para parámetros IPE
  328. self.frame_ipe = ttk.Frame(left_panel)
  329. self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10))
  330. ttk.Label(self.frame_ipe, text="Parámetros IPE:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  331. # H (Altura)
  332. ttk.Label(self.frame_ipe, text="H (altura, m):").pack(anchor=tk.W)
  333. 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)
  334. self.entry_H.pack(anchor=tk.W, fill=tk.X)
  335. self.entry_H.set("0.300")
  336. self.entry_H.bind('<FocusOut>', self.on_parameter_change_ipe)
  337. self.entry_H.bind('<Return>', self.on_parameter_change_ipe)
  338. # b (Ancho)
  339. ttk.Label(self.frame_ipe, text="b (ancho flange, m):").pack(anchor=tk.W, pady=(10, 0))
  340. 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)
  341. self.entry_b.pack(anchor=tk.W, fill=tk.X)
  342. self.entry_b.set("0.150")
  343. self.entry_b.bind('<FocusOut>', self.on_parameter_change_ipe)
  344. self.entry_b.bind('<Return>', self.on_parameter_change_ipe)
  345. # tf (Espesor flange)
  346. ttk.Label(self.frame_ipe, text="tf (espesor flange, m):").pack(anchor=tk.W, pady=(10, 0))
  347. 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)
  348. self.entry_tf.pack(anchor=tk.W, fill=tk.X)
  349. self.entry_tf.set("0.0107")
  350. self.entry_tf.bind('<FocusOut>', self.on_parameter_change_ipe)
  351. self.entry_tf.bind('<Return>', self.on_parameter_change_ipe)
  352. # tw (Espesor alma)
  353. ttk.Label(self.frame_ipe, text="tw (espesor alma, m):").pack(anchor=tk.W, pady=(10, 0))
  354. 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)
  355. self.entry_tw.pack(anchor=tk.W, fill=tk.X)
  356. self.entry_tw.set("0.0063")
  357. self.entry_tw.bind('<FocusOut>', self.on_parameter_change_ipe)
  358. self.entry_tw.bind('<Return>', self.on_parameter_change_ipe)
  359. # Frame para sección personalizada
  360. self.frame_personalizada = ttk.Frame(left_panel)
  361. ttk.Label(self.frame_personalizada, text="Puntos (JSON):", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  362. ttk.Label(self.frame_personalizada, text="Formato: [[x1,y1], [x2,y2], ...]", font=("Arial", 8)).pack(anchor=tk.W)
  363. self.text_puntos = tk.Text(self.frame_personalizada, height=8, width=30, font=("Courier", 8))
  364. self.text_puntos.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
  365. ttk.Button(self.frame_personalizada, text="Cargar puntos", command=self.load_custom_points).pack(fill=tk.X)
  366. # Frame para puente nuevo
  367. self.frame_puente_nuevo = ttk.Frame(left_panel)
  368. self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  369. ttk.Label(self.frame_puente_nuevo, text="Parámetros puente nuevo:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  370. # H (Altura exterior)
  371. ttk.Label(self.frame_puente_nuevo, text = "h (altura exterior, m):").pack(anchor=tk.W, pady=(10, 0))
  372. 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)
  373. self.entry_h_puente.pack(anchor=tk.W, fill=tk.X)
  374. self.entry_h_puente.set("0.300")
  375. self.entry_h_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  376. self.entry_h_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  377. # b (Ancho)
  378. ttk.Label(self.frame_puente_nuevo, text="b (ancho, m):").pack(anchor=tk.W, pady=(10, 0))
  379. 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)
  380. self.entry_b_puente.pack(anchor=tk.W, fill=tk.X)
  381. self.entry_b_puente.set("0.150")
  382. self.entry_b_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  383. self.entry_b_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  384. # tf (espesor ala)
  385. ttk.Label(self.frame_puente_nuevo, text="tf (espesor ala, m):").pack(anchor=tk.W, pady=(10, 0))
  386. 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)
  387. self.entry_tf_puente.pack(anchor=tk.W, fill=tk.X)
  388. self.entry_tf_puente.set("0.0107")
  389. self.entry_tf_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  390. self.entry_tf_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  391. # tw (espesor alma)
  392. ttk.Label(self.frame_puente_nuevo, text="tw (espesor alma, m):").pack(anchor=tk.W, pady=(10, 0))
  393. 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)
  394. self.entry_tw_puente.pack(anchor=tk.W, fill=tk.X)
  395. self.entry_tw_puente.set("0.0063")
  396. self.entry_tw_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  397. self.entry_tw_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  398. # ha (altura refuerzo ala)
  399. ttk.Label(self.frame_puente_nuevo, text="ha (altura refuerzo ala, m):").pack(anchor=tk.W, pady=(10, 0))
  400. 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)
  401. self.entry_ha_puente.pack(anchor=tk.W, fill=tk.X)
  402. self.entry_ha_puente.set("0.02")
  403. self.entry_ha_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  404. self.entry_ha_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  405. # ta (espesor refuerzo ala)
  406. ttk.Label(self.frame_puente_nuevo, text="ta (espesor refuerzo ala, m):").pack(anchor=tk.W, pady=(10, 0))
  407. 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)
  408. self.entry_ta_puente.pack(anchor=tk.W, fill=tk.X)
  409. self.entry_ta_puente.set("0.01")
  410. self.entry_ta_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  411. self.entry_ta_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  412. # tr (espesor refuerzo alma)
  413. ttk.Label(self.frame_puente_nuevo, text="tr (espesor refuerzo alma, m):").pack(anchor=tk.W, pady=(10, 0))
  414. 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)
  415. self.entry_tr_puente.pack(anchor=tk.W, fill=tk.X)
  416. self.entry_tr_puente.set("0.01")
  417. self.entry_tr_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  418. self.entry_tr_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  419. # theta (ángulo refuerzo)
  420. ttk.Label(self.frame_puente_nuevo, text="theta (ángulo refuerzo, grados):").pack(anchor=tk.W, pady=(10, 0))
  421. 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)
  422. self.entry_theta_puente.pack(anchor=tk.W, fill=tk.X)
  423. self.entry_theta_puente.set("45")
  424. self.entry_theta_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  425. self.entry_theta_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  426. # Frame para barrido paramétrico
  427. self.frame_sweep_tab = ttk.Frame(left_panel)
  428. self.create_sweep_tab_widgets()
  429. # Separador
  430. ttk.Separator(left_panel, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
  431. # Panel de resultados
  432. ttk.Label(left_panel, text="Propiedades:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  433. self.text_results = tk.Text(left_panel, height=20, width=30, state=tk.DISABLED, font=("Courier", 8))
  434. self.text_results.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  435. # Botón guardar
  436. ttk.Button(left_panel, text="Guardar nueva sección", command=self.save_new_section).pack(fill=tk.X)
  437. # ========== PANEL DERECHO (GRÁFICO) ==========
  438. right_panel = ttk.Frame(self)
  439. right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)
  440. self.figure = plt.Figure(figsize=(6, 8), dpi=100)
  441. self.ax = self.figure.add_subplot(111)
  442. self.canvas = FigureCanvasTkAgg(self.figure, master=right_panel)
  443. self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
  444. # Cargar la primera sección después de que todos los widgets estén creados
  445. if self.secciones_data['secciones']:
  446. self.combo_seccion.current(0)
  447. self.on_section_changed(None)
  448. self.frame_puente_nuevo.pack_forget()
  449. def create_sweep_tab_widgets(self):
  450. """Crea los widgets del panel de barrido paramétrico"""
  451. self.frame_sweep_tab.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  452. # Título
  453. ttk.Label(self.frame_sweep_tab, text="Barrido Paramétrico", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  454. # Sección actual
  455. ttk.Label(self.frame_sweep_tab, text="Sección: IPE", font=("Arial", 9)).pack(anchor=tk.W, pady=(5, 10))
  456. # Frame para parámetros fijos
  457. frame_fixed = ttk.LabelFrame(self.frame_sweep_tab, text="Parámetros Fijos", padding=5)
  458. frame_fixed.pack(fill=tk.X, pady=(0, 10))
  459. # Variables para toggle fijo/barrido
  460. self.sweep_toggle_vars = {}
  461. self.sweep_fixed_entries = {}
  462. self.sweep_range_entries = {}
  463. params_ipe = ['H', 'b', 'tf', 'tw']
  464. for param in params_ipe:
  465. container = ttk.Frame(frame_fixed)
  466. container.pack(fill=tk.X, pady=2)
  467. var = tk.BooleanVar(value=True)
  468. self.sweep_toggle_vars[param] = var
  469. chk = ttk.Checkbutton(container, text=f"Fijo: {param}", variable=var,
  470. command=lambda p=param: self.on_sweep_toggle(p))
  471. chk.pack(side=tk.LEFT, fill=tk.X, expand=True)
  472. entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=10)
  473. entry.pack(side=tk.LEFT, padx=5)
  474. self.sweep_fixed_entries[param] = entry
  475. # Cargar valores actuales
  476. self.sweep_fixed_entries['H'].set("0.300")
  477. self.sweep_fixed_entries['b'].set("0.150")
  478. self.sweep_fixed_entries['tf'].set("0.0107")
  479. self.sweep_fixed_entries['tw'].set("0.0063")
  480. # Frame para parámetros a barrer
  481. frame_sweep = ttk.LabelFrame(self.frame_sweep_tab, text="Parámetros a Barrer", padding=5)
  482. frame_sweep.pack(fill=tk.X, pady=(0, 10))
  483. self.sweep_range_labels = {}
  484. self.sweep_range_entries = {}
  485. for param in params_ipe:
  486. container = ttk.Frame(frame_sweep)
  487. container.pack(fill=tk.X, pady=2)
  488. lbl = ttk.Label(container, text=f"{param}:", width=5)
  489. lbl.pack(side=tk.LEFT)
  490. self.sweep_range_labels[param] = lbl
  491. ttk.Label(container, text="min:").pack(side=tk.LEFT, padx=(10, 2))
  492. min_entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=8)
  493. min_entry.pack(side=tk.LEFT, padx=2)
  494. ttk.Label(container, text="max:").pack(side=tk.LEFT, padx=(10, 2))
  495. max_entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=8)
  496. max_entry.pack(side=tk.LEFT, padx=2)
  497. ttk.Label(container, text="pasos:").pack(side=tk.LEFT, padx=(10, 2))
  498. steps_entry = ttk.Spinbox(container, from_=2, to=20, increment=1, width=8)
  499. steps_entry.pack(side=tk.LEFT, padx=2)
  500. steps_entry.set(5)
  501. self.sweep_range_entries[param] = {
  502. 'min': min_entry,
  503. 'max': max_entry,
  504. 'steps': steps_entry
  505. }
  506. # Botones de control
  507. button_frame = ttk.Frame(self.frame_sweep_tab)
  508. button_frame.pack(fill=tk.X, pady=(10, 0))
  509. ttk.Button(button_frame, text="Ejecutar Barrido", command=self.execute_parametric_sweep).pack(side=tk.LEFT, padx=2)
  510. ttk.Button(button_frame, text="Exportar Excel", command=self.export_sweep_results).pack(side=tk.LEFT, padx=2)
  511. ttk.Button(button_frame, text="Limpiar", command=self.clear_sweep_results).pack(side=tk.LEFT, padx=2)
  512. # Ocultar frame de barrido inicialmente
  513. self.frame_sweep_tab.pack_forget()
  514. def on_sweep_toggle(self, param):
  515. """Alterna parámetro entre fijo y barrido"""
  516. is_fixed = self.sweep_toggle_vars[param].get()
  517. # Los rangos se mostrarán solo si no está fijo
  518. # Esta lógica se puede mejorar si es necesario
  519. def on_section_type_changed(self, event):
  520. """Cambia entre IPE, Personalizada, Puente nuevo y Barrido"""
  521. section_type = self.combo_tipo.get()
  522. if section_type == "IPE":
  523. self.frame_personalizada.pack_forget()
  524. self.frame_puente_nuevo.pack_forget()
  525. self.frame_sweep_tab.pack_forget()
  526. self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10))
  527. self.on_parameter_change_ipe(None)
  528. elif section_type == "Barrido Paramétrico":
  529. self.frame_ipe.pack_forget()
  530. self.frame_personalizada.pack_forget()
  531. self.frame_puente_nuevo.pack_forget()
  532. self.frame_sweep_tab.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  533. # Precargar valores actuales como fijos
  534. if self.current_properties:
  535. try:
  536. self.sweep_fixed_entries['H'].delete(0, tk.END)
  537. self.sweep_fixed_entries['H'].insert(0, f"{float(self.entry_H.get()):.6f}")
  538. self.sweep_fixed_entries['b'].delete(0, tk.END)
  539. self.sweep_fixed_entries['b'].insert(0, f"{float(self.entry_b.get()):.6f}")
  540. self.sweep_fixed_entries['tf'].delete(0, tk.END)
  541. self.sweep_fixed_entries['tf'].insert(0, f"{float(self.entry_tf.get()):.6f}")
  542. self.sweep_fixed_entries['tw'].delete(0, tk.END)
  543. self.sweep_fixed_entries['tw'].insert(0, f"{float(self.entry_tw.get()):.6f}")
  544. except:
  545. pass
  546. elif section_type == "Puente nuevo":
  547. self.frame_ipe.pack_forget()
  548. self.frame_personalizada.pack_forget()
  549. self.frame_sweep_tab.pack_forget()
  550. self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  551. self.on_parameter_change_puente_nuevo(None)
  552. else: # Personalizada
  553. self.frame_ipe.pack_forget()
  554. self.frame_puente_nuevo.pack_forget()
  555. self.frame_sweep_tab.pack_forget()
  556. self.frame_personalizada.pack(fill=tk.BOTH, expand=True)
  557. if self.current_points is not None:
  558. import json
  559. puntos_json = json.dumps(self.current_points.tolist(), indent=2)
  560. self.text_puntos.delete(1.0, tk.END)
  561. self.text_puntos.insert(1.0, puntos_json)
  562. def load_custom_points(self):
  563. """Carga puntos personalizados desde el texto"""
  564. try:
  565. import json
  566. text = self.text_puntos.get(1.0, tk.END).strip()
  567. puntos = json.loads(text)
  568. self.current_points = np.array(puntos)
  569. self.current_properties = calculate_section_properties(self.current_points)
  570. self.update_plot()
  571. self.update_results()
  572. messagebox.showinfo("Éxito", "Puntos cargados correctamente")
  573. except json.JSONDecodeError:
  574. messagebox.showerror("Error", "Formato JSON inválido")
  575. except Exception as e:
  576. messagebox.showerror("Error", f"Error al cargar puntos: {str(e)}")
  577. def on_section_changed(self, event):
  578. """Se ejecuta cuando cambia la sección seleccionada"""
  579. idx = self.combo_seccion.current()
  580. if idx >= 0:
  581. seccion = self.secciones_data['secciones'][idx]
  582. self.current_section_name = seccion['nombre']
  583. section_type = seccion.get('tipo', 'otro')
  584. # Actualizar el combo de tipo
  585. if section_type == 'ipe':
  586. self.combo_tipo.set("IPE")
  587. elif section_type == 'puente_nuevo':
  588. self.combo_tipo.set("Puente nuevo")
  589. else:
  590. self.combo_tipo.set("Personalizada")
  591. # Si tiene puntos directos, cargarlos
  592. if 'puntos' in seccion:
  593. self.current_points = np.array(seccion['puntos'])
  594. if section_type == 'ipe':
  595. self.current_properties = calculate_section_properties(self.current_points)
  596. if section_type == 'puente_nuevo':
  597. self.current_properties = calculate_section_properties(self.current_points)
  598. self.update_plot()
  599. self.update_results()
  600. # Si además es IPE, establecer los valores
  601. if section_type == 'ipe' and 'parametros' in seccion:
  602. params = seccion['parametros']
  603. self.entry_H.delete(0, tk.END)
  604. self.entry_H.insert(0, f"{params['H']:.6f}")
  605. self.entry_b.delete(0, tk.END)
  606. self.entry_b.insert(0, f"{params['b']:.6f}")
  607. self.entry_tf.delete(0, tk.END)
  608. self.entry_tf.insert(0, f"{params['tf']:.6f}")
  609. self.entry_tw.delete(0, tk.END)
  610. self.entry_tw.insert(0, f"{params['tw']:.6f}")
  611. # Mostrar frame IPE
  612. self.frame_personalizada.pack_forget()
  613. self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10))
  614. elif section_type == 'puente_nuevo' and 'parametros' in seccion:
  615. params = seccion['parametros']
  616. self.entry_h_puente.delete(0, tk.END)
  617. self.entry_h_puente.insert(0, f"{params['h']:.6f}")
  618. self.entry_b_puente.delete(0, tk.END)
  619. self.entry_b_puente.insert(0, f"{params['b']:.6f}")
  620. self.entry_tf_puente.delete(0, tk.END)
  621. self.entry_tf_puente.insert(0, f"{params['tf']:.6f}")
  622. self.entry_tw_puente.delete(0, tk.END)
  623. self.entry_tw_puente.insert(0, f"{params['tw']:.6f}")
  624. self.entry_ha_puente.delete(0, tk.END)
  625. self.entry_ha_puente.insert(0, f"{params['ha']:.6f}")
  626. self.entry_ta_puente.delete(0, tk.END)
  627. self.entry_ta_puente.insert(0, f"{params['ta']:.6f}")
  628. self.entry_tr_puente.delete(0, tk.END)
  629. self.entry_tr_puente.insert(0, f"{params['tr']:.6f}")
  630. self.entry_theta_puente.delete(0, tk.END)
  631. self.entry_theta_puente.insert(0, f"{params['theta']:.6f}")
  632. # Mostrar frame puente nuevo
  633. self.frame_ipe.pack_forget()
  634. self.frame_personalizada.pack_forget()
  635. self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  636. else:
  637. # Mostrar frame personalizado
  638. self.frame_ipe.pack_forget()
  639. self.frame_puente_nuevo.pack_forget()
  640. self.frame_personalizada.pack(fill=tk.BOTH, expand=True)
  641. import json
  642. puntos_json = json.dumps(self.current_points.tolist(), indent=2)
  643. self.text_puntos.delete(1.0, tk.END)
  644. self.text_puntos.insert(1.0, puntos_json)
  645. # Si solo tiene parámetros IPE, generar puntos
  646. elif section_type == 'ipe' and 'parametros' in seccion:
  647. params = seccion['parametros']
  648. self.combo_tipo.set("IPE")
  649. self.entry_H.delete(0, tk.END)
  650. self.entry_H.insert(0, f"{params['H']:.6f}")
  651. self.entry_b.delete(0, tk.END)
  652. self.entry_b.insert(0, f"{params['b']:.6f}")
  653. self.entry_tf.delete(0, tk.END)
  654. self.entry_tf.insert(0, f"{params['tf']:.6f}")
  655. self.entry_tw.delete(0, tk.END)
  656. self.entry_tw.insert(0, f"{params['tw']:.6f}")
  657. self.on_parameter_change_ipe(None)
  658. elif section_type == 'puente_nuevo' and 'parametros' in seccion:
  659. params = seccion['parametros']
  660. self.combo_tipo.set("Puente nuevo")
  661. self.entry_h_puente.delete(0, tk.END)
  662. self.entry_h_puente.insert(0, f"{params['h']:.6f}")
  663. self.entry_b_puente.delete(0, tk.END)
  664. self.entry_b_puente.insert(0, f"{params['b']:.6f}")
  665. self.entry_tf_puente.delete(0, tk.END)
  666. self.entry_tf_puente.insert(0, f"{params['tf']:.6f}")
  667. self.entry_tw_puente.delete(0, tk.END)
  668. self.entry_tw_puente.insert(0, f"{params['tw']:.6f}")
  669. self.entry_ha_puente.delete(0, tk.END)
  670. self.entry_ha_puente.insert(0, f"{params['ha']:.6f}")
  671. self.entry_ta_puente.delete(0, tk.END)
  672. self.entry_ta_puente.insert(0, f"{params['ta']:.6f}")
  673. self.entry_tr_puente.delete(0, tk.END)
  674. self.entry_tr_puente.insert(0, f"{params['tr']:.6f}")
  675. self.entry_theta_puente.delete(0, tk.END)
  676. self.entry_theta_puente.insert(0, f"{params['theta']:.6f}")
  677. self.on_parameter_change_puente_nuevo(None)
  678. def on_parameter_change_ipe(self, event=None):
  679. """Se ejecuta cuando cambien los valores de entrada"""
  680. try:
  681. H = float(self.entry_H.get())
  682. b = float(self.entry_b.get())
  683. tf = float(self.entry_tf.get())
  684. tw = float(self.entry_tw.get())
  685. # Validar valores básicos
  686. if H <= 0 or b <= 0 or tf <= 0 or tw <= 0:
  687. return
  688. if tf > H/2 or tw > b:
  689. tf = min(tf, H/2)
  690. tw = min(tw, b)
  691. self.entry_tf.set(f"{tf:.6f}")
  692. self.entry_tw.set(f"{tw:.6f}")
  693. # Generar puntos y calcular
  694. self.current_points = generate_ipe_points(H, b, tf, tw)
  695. self.current_properties = calculate_section_properties(self.current_points)
  696. # Actualizar visualización
  697. self.update_plot()
  698. self.update_results()
  699. except ValueError:
  700. # Ignorar si los valores no son números válidos
  701. pass
  702. def on_parameter_change_puente_nuevo(self, event=None):
  703. """Se ejecuta cuando cambien los valores de entrada"""
  704. try:
  705. h = float(self.entry_h_puente.get())
  706. b = float(self.entry_b_puente.get())
  707. tf = float(self.entry_tf_puente.get())
  708. tw = float(self.entry_tw_puente.get())
  709. ha = float(self.entry_ha_puente.get())
  710. ta = float(self.entry_ta_puente.get())
  711. tr = float(self.entry_tr_puente.get())
  712. theta = float(self.entry_theta_puente.get())
  713. # Validar valores básicos
  714. if h <= 0 or b <= 0 or tf <= 0 or tw <= 0 or ha <= 0 or ta <= 0 or tr <= 0:
  715. return
  716. # Generar puntos y calcular
  717. self.current_points = generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta)
  718. self.current_properties = calculate_section_properties(self.current_points)
  719. # Actualizar visualización
  720. self.update_plot()
  721. self.update_results()
  722. except ValueError:
  723. # Ignorar si los valores no son números válidos
  724. pass
  725. def update_plot(self):
  726. """Actualiza el gráfico de la sección"""
  727. self.ax.clear()
  728. points = self.current_points
  729. props = self.current_properties
  730. px = points[:, 0]
  731. py = points[:, 1]
  732. xg = props['xg_orig']
  733. yg = props['yg_orig']
  734. bmax = props['bmax']
  735. hmax = props['hmax']
  736. tetha = props['tetha']
  737. # Dibujar sección
  738. self.ax.plot(px, py, 'b-', linewidth=2)
  739. self.ax.fill(px, py, facecolor="lightblue", alpha=0.5)
  740. # Centro de gravedad
  741. self.ax.plot(xg, yg, 'ro', markersize=8, label='CDG')
  742. # Ejes principales
  743. tt = min(bmax, hmax)
  744. self.ax.arrow(xg, yg, tt/3, 0, head_width=tt/30, head_length=tt/30, fc='green', ec='green')
  745. self.ax.arrow(xg, yg, 0, tt/3, head_width=tt/30, head_length=tt/30, fc='green', ec='green')
  746. tet = tetha * (math.pi) / 180
  747. 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')
  748. 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')
  749. self.ax.set_aspect('equal')
  750. self.ax.grid(True, alpha=0.3)
  751. self.ax.set_xlabel('X (m)', fontweight='bold')
  752. self.ax.set_ylabel('Y (m)', fontweight='bold')
  753. self.ax.set_title(self.current_section_name or 'Sección IPE', fontweight='bold')
  754. self.ax.legend()
  755. self.figure.tight_layout()
  756. self.canvas.draw()
  757. def update_results(self):
  758. """Actualiza el panel de resultados"""
  759. props = self.current_properties
  760. results_text = f"""PROPIEDADES CALCULADAS
  761. GEOMÉTRICAS:
  762. Área: {props['area']:.2f} cm²
  763. Perímetro: {props['perimetro']:.2f} cm
  764. b_max: {props['bmax']*100:.2f} cm
  765. h_max: {props['hmax']*100:.2f} cm
  766. CDG (ref. origen):
  767. X: {props['xg']:.2f} cm
  768. Y: {props['yg']:.2f} cm
  769. INERCIA (eje Xg-Yg):
  770. Ixg: {props['ixg']:.2f} cm⁴
  771. Iyg: {props['iyg']:.2f} cm⁴
  772. Pxyg: {props['pxyg']:.2f} cm⁴
  773. rx: {props['rx']:.2f} cm
  774. ry: {props['ry']:.2f} cm
  775. INERCIA (ejes principales):
  776. Imax: {props['imax']:.2f} cm⁴
  777. Imin: {props['imin']:.2f} cm⁴
  778. θ: {props['tetha']:.2f}°
  779. rmax: {props['rmax']:.2f} cm
  780. rmin: {props['rmin']:.2f} cm
  781. MÓDULO RESISTENTE:
  782. Wel1x: {props['wel1x']:.2f} cm³
  783. Wel2x: {props['wel2x']:.2f} cm³
  784. Wel1y: {props['wel1y']:.2f} cm³
  785. Wel2y: {props['wel2y']:.2f} cm³
  786. MECÁNICAS:
  787. Peso: {props['peso']:.2f} kg/m
  788. Nel: {props['nel']:.2f} kN
  789. Melx: {props['melx']:.2f} kN·m
  790. Mely: {props['mely']:.2f} kN·m
  791. """
  792. self.text_results.config(state=tk.NORMAL)
  793. self.text_results.delete(1.0, tk.END)
  794. self.text_results.insert(1.0, results_text)
  795. self.text_results.config(state=tk.DISABLED)
  796. def save_new_section(self):
  797. """Guarda la sección actual en el JSON"""
  798. if not self.current_section_name or self.current_points is None:
  799. messagebox.showerror("Error", "Selecciona una sección primero")
  800. return
  801. # Preguntar nombre de la nueva sección
  802. new_name = tk.simpledialog.askstring("Guardar sección", "Nombre de la nueva sección:")
  803. if not new_name:
  804. return
  805. # Verificar si ya existe
  806. exists = any(s['nombre'] == new_name for s in self.secciones_data['secciones'])
  807. if exists:
  808. should_overwrite = messagebox.askyesno("Ya existe", f"La sección '{new_name}' ya existe. ¿Sobreescribir?")
  809. if not should_overwrite:
  810. return
  811. # Detectar tipo de sección
  812. section_type = self.combo_tipo.get().lower()
  813. if section_type == "ipe":
  814. section_type = "ipe"
  815. elif section_type == "puente nuevo":
  816. section_type = "puente_nuevo"
  817. else:
  818. section_type = "otro"
  819. # Crear nueva sección con puntos
  820. new_section = {
  821. 'nombre': new_name,
  822. 'tipo': section_type,
  823. 'puntos': self.current_points.tolist() # Guardar los puntos actuales
  824. }
  825. # Si es IPE, guardar también los parámetros
  826. if section_type == "ipe":
  827. try:
  828. H = float(self.entry_H.get())
  829. b = float(self.entry_b.get())
  830. tf = float(self.entry_tf.get())
  831. tw = float(self.entry_tw.get())
  832. if H > 0 and b > 0 and tf > 0 and tw > 0: # Validar que sean mayores a 0
  833. new_section['parametros'] = {
  834. 'H': H,
  835. 'b': b,
  836. 'tf': tf,
  837. 'tw': tw
  838. }
  839. except:
  840. pass # Si no se pueden leer, solo guardamos puntos
  841. elif section_type == "puente_nuevo":
  842. try:
  843. h = float(self.entry_h_puente.get())
  844. b = float(self.entry_b_puente.get())
  845. tf = float(self.entry_tf_puente.get())
  846. tw = float(self.entry_tw_puente.get())
  847. ha = float(self.entry_ha_puente.get())
  848. ta = float(self.entry_ta_puente.get())
  849. tr = float(self.entry_tr_puente.get())
  850. theta = float(self.entry_theta_puente.get())
  851. if all(v > 0 for v in [h, b, tf, tw, ha, ta, tr]) and 0 <= theta <= 360:
  852. new_section['parametros'] = {
  853. 'h': h,
  854. 'b': b,
  855. 'tf': tf,
  856. 'tw': tw,
  857. 'ha': ha,
  858. 'ta': ta,
  859. 'tr': tr,
  860. 'theta': theta
  861. }
  862. except:
  863. pass # Si no se pueden leer, solo guardamos puntos
  864. # Eliminar si existe
  865. self.secciones_data['secciones'] = [s for s in self.secciones_data['secciones'] if s['nombre'] != new_name]
  866. self.secciones_data['secciones'].append(new_section)
  867. self.save_json()
  868. # Actualizar combo
  869. self.combo_seccion['values'] = [s['nombre'] for s in self.secciones_data['secciones']]
  870. self.combo_seccion.set(new_name)
  871. messagebox.showinfo("Éxito", f"Sección '{new_name}' guardada correctamente")
  872. # ==================== MÉTODOS DE BARRIDO PARAMÉTRICO ====================
  873. def execute_parametric_sweep(self):
  874. """Ejecuta el barrido paramétrico"""
  875. try:
  876. # Recopilar configuración
  877. fixed_params = {}
  878. sweep_configs = {}
  879. for param in ['H', 'b', 'tf', 'tw']:
  880. is_fixed = self.sweep_toggle_vars[param].get()
  881. if is_fixed:
  882. value = float(self.sweep_fixed_entries[param].get())
  883. fixed_params[param] = value
  884. else:
  885. min_val = float(self.sweep_range_entries[param]['min'].get())
  886. max_val = float(self.sweep_range_entries[param]['max'].get())
  887. steps = int(self.sweep_range_entries[param]['steps'].get())
  888. if min_val >= max_val:
  889. messagebox.showerror("Error", f"Rango inválido para {param}: min >= max")
  890. return
  891. sweep_configs[param] = {
  892. 'min': min_val,
  893. 'max': max_val,
  894. 'steps': steps
  895. }
  896. if not sweep_configs:
  897. messagebox.showerror("Error", "Debe barrer al menos un parámetro")
  898. return
  899. if not fixed_params:
  900. messagebox.showwarning("Advertencia", "No hay parámetros fijos")
  901. # Ejecutar barrido
  902. messagebox.showinfo("Info", "Ejecutando barrido... Esto puede tomar unos segundos")
  903. sweep = ParametricSweep("ipe", fixed_params, sweep_configs)
  904. self.sweep_result = sweep.execute_sweep()
  905. self.sweep_pareto = sweep.find_pareto_front()
  906. # Calcular combinaciones teóricas vs válidas
  907. total_combos = 1
  908. for config in sweep_configs.values():
  909. total_combos *= config['steps']
  910. valid_combos = len(self.sweep_result)
  911. invalid_combos = total_combos - valid_combos
  912. msg = f"✓ Barrido completado:\n"
  913. msg += f" Combinaciones teóricas: {total_combos}\n"
  914. msg += f" Combinaciones válidas: {valid_combos}\n"
  915. if invalid_combos > 0:
  916. msg += f" Rechazadas (geometría): {invalid_combos}\n"
  917. msg += f"\n✓ Soluciones ordenadas: {len(self.sweep_pareto)}"
  918. messagebox.showinfo("Éxito", msg)
  919. # Mostrar resultados
  920. self.show_sweep_results()
  921. except Exception as e:
  922. messagebox.showerror("Error", f"Error en barrido: {str(e)}")
  923. def show_sweep_results(self):
  924. """Muestra los resultados del barrido ordenados por eficiencia"""
  925. if self.sweep_result is None or self.sweep_pareto is None:
  926. messagebox.showerror("Error", "Ejecute primero el barrido")
  927. return
  928. try:
  929. # Crear figura con dos subplots
  930. if self.sweep_figure is not None:
  931. plt.close(self.sweep_figure)
  932. self.sweep_figure = plt.Figure(figsize=(14, 6), dpi=100)
  933. # Subplot 1: Visualización de soluciones
  934. ax1 = self.sweep_figure.add_subplot(121)
  935. # Obtener parámetros barridos
  936. sweep_params = [p for p in ['H', 'b', 'tf', 'tw'] if p in self.sweep_result.columns and self.sweep_result[p].nunique() > 1]
  937. scatter = None # Variable para almacenar scatter si existe
  938. if len(sweep_params) >= 2:
  939. # Scatter plot: Peso vs Eficiencia
  940. scatter = ax1.scatter(self.sweep_result['peso'],
  941. self.sweep_result['eficiencia'],
  942. c=self.sweep_result['ixg'],
  943. cmap='viridis', s=100, alpha=0.6, edgecolors='black')
  944. # Marcar el más eficiente con un símbolo especial (diamante dorado)
  945. most_efficient = self.sweep_pareto.iloc[0]
  946. ax1.scatter(most_efficient['peso'], most_efficient['eficiencia'],
  947. marker='D', s=400, c='gold', edgecolors='orange',
  948. linewidth=2.5, label='Óptimo', zorder=6)
  949. self.sweep_figure.colorbar(scatter, ax=ax1, label='Ix (cm⁴)')
  950. ax1.set_xlabel('Peso (kg/m)', fontweight='bold', fontsize=11)
  951. ax1.set_ylabel('Eficiencia = (Ix+Iy)/Peso*1000', fontweight='bold', fontsize=11)
  952. ax1.set_title('Soluciones: Peso vs Eficiencia\n(Dorado = Óptimo)', fontweight='bold')
  953. ax1.grid(True, alpha=0.3)
  954. ax1.legend(loc='best')
  955. elif len(sweep_params) == 1:
  956. # Plot simple si solo 1 parámetro
  957. ax1.plot(self.sweep_result[sweep_params[0]], self.sweep_result['eficiencia'], 'b.-', label='Todas', linewidth=2, markersize=8)
  958. top5 = self.sweep_pareto.head(5)
  959. ax1.plot(top5[sweep_params[0]], top5['eficiencia'], 'r*', markersize=20, label='Top 5', zorder=5)
  960. # Marcar el más eficiente
  961. most_efficient = self.sweep_pareto.iloc[0]
  962. ax1.plot(most_efficient[sweep_params[0]], most_efficient['eficiencia'], 'D',
  963. color='gold', markersize=5, markeredgecolor='orange', markeredgewidth=2,
  964. label='Más Eficiente', zorder=6)
  965. ax1.set_xlabel(f'{sweep_params[0]} (m)', fontweight='bold', fontsize=11)
  966. ax1.set_ylabel('Eficiencia', fontweight='bold', fontsize=11)
  967. ax1.set_title(f'Eficiencia vs {sweep_params[0]}', fontweight='bold')
  968. ax1.legend()
  969. ax1.grid(True, alpha=0.3)
  970. # Subplot 2: Tabla top 10
  971. ax2 = self.sweep_figure.add_subplot(122)
  972. ax2.axis('tight')
  973. ax2.axis('off')
  974. # Preparar datos para tabla
  975. cols_to_show = [c for c in ['H', 'b', 'tf', 'tw', 'peso', 'ixg', 'iyg', 'eficiencia'] if c in self.sweep_pareto.columns]
  976. top_pareto = self.sweep_pareto.head(10)[cols_to_show].copy()
  977. # Formatear números
  978. table_data = []
  979. for rank, (_, row) in enumerate(top_pareto.iterrows(), 1):
  980. formatted_row = [str(rank)] # Ranking
  981. for col in cols_to_show:
  982. val = row[col]
  983. if col in ['H', 'b', 'tf', 'tw']:
  984. formatted_row.append(f"{val:.4f}")
  985. else:
  986. formatted_row.append(f"{val:.2f}")
  987. table_data.append(formatted_row)
  988. col_labels = ['#'] + list(cols_to_show)
  989. table = ax2.table(cellText=table_data, colLabels=col_labels,
  990. cellLoc='center', loc='center', bbox=[0, 0, 1, 1])
  991. table.auto_set_font_size(False)
  992. table.set_fontsize(8)
  993. table.scale(1, 1.8)
  994. # Colorear header
  995. for i in range(len(col_labels)):
  996. table[(0, i)].set_facecolor('#40466e')
  997. table[(0, i)].set_text_props(weight='bold', color='white')
  998. # Colorear filas alternadas
  999. for i in range(1, len(table_data) + 1):
  1000. color = '#f0f0f0' if i % 2 == 0 else 'white'
  1001. for j in range(len(col_labels)):
  1002. table[(i, j)].set_facecolor(color)
  1003. ax2.set_title('Top 10 Soluciones\n(Ordenadas por Eficiencia)', fontweight='bold', fontsize=11, pad=20)
  1004. self.sweep_figure.tight_layout()
  1005. # Mostrar en canvas
  1006. if self.sweep_canvas is not None:
  1007. self.sweep_canvas.get_tk_widget().destroy()
  1008. right_panel = self.canvas.get_tk_widget().master
  1009. self.sweep_canvas = FigureCanvasTkAgg(self.sweep_figure, master=right_panel)
  1010. self.sweep_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
  1011. self.sweep_canvas.draw()
  1012. # Agregar interactividad con hover DESPUÉS de crear el canvas
  1013. if scatter is not None:
  1014. self._setup_scatter_hover(ax1, scatter)
  1015. except Exception as e:
  1016. messagebox.showerror("Error", f"Error mostrando resultados: {str(e)}")
  1017. import traceback
  1018. traceback.print_exc()
  1019. def _setup_scatter_hover(self, ax, scatter):
  1020. """Configura interactividad con hover para mostrar valores de puntos"""
  1021. # Almacenar referencias a los datos
  1022. self.scatter_ax = ax
  1023. self.scatter_data = self.sweep_result.copy()
  1024. self.hover_annotation = None
  1025. def on_hover(event):
  1026. """Muestra información cuando el mouse pasa sobre puntos del scatter"""
  1027. if event.inaxes != self.scatter_ax or len(self.scatter_data) == 0:
  1028. if self.hover_annotation is not None:
  1029. self.hover_annotation.remove()
  1030. self.hover_annotation = None
  1031. self.sweep_canvas.draw_idle()
  1032. return
  1033. # Calcular distancias a todos los puntos
  1034. x, y = event.xdata, event.ydata
  1035. if x is None or y is None:
  1036. return
  1037. # Distancias en coordenadas de display para mejor detección
  1038. try:
  1039. peso_data = self.scatter_data['peso'].values
  1040. eficiencia_data = self.scatter_data['eficiencia'].values
  1041. # Convertir coordenadas de datos a display
  1042. x_display, y_display = self.scatter_ax.transData.transform((x, y))
  1043. puntos_display = self.scatter_ax.transData.transform(
  1044. np.c_[peso_data, eficiencia_data]
  1045. )
  1046. # Calcular distancia en píxeles
  1047. dist = np.sqrt((puntos_display[:, 0] - x_display)**2 +
  1048. (puntos_display[:, 1] - y_display)**2)
  1049. # Umbral de 20 píxeles para detectar hover
  1050. idx_cercano = np.argmin(dist)
  1051. if dist[idx_cercano] < 20:
  1052. # Obtener datos del punto
  1053. row = self.scatter_data.iloc[idx_cercano]
  1054. # Construir texto con información
  1055. texto = f"Peso: {row['peso']:.2f} kg/m\n"
  1056. texto += f"Eficiencia: {row['eficiencia']:.2f}\n"
  1057. # Agregar parámetros que varían
  1058. for param in ['H', 'b', 'tf', 'tw']:
  1059. if param in self.scatter_data.columns and self.scatter_data[param].nunique() > 1:
  1060. texto += f"{param}: {row[param]:.4f}\n"
  1061. texto += f"Ix: {row['ixg']:.0f} cm⁴\n"
  1062. texto += f"Iy: {row['iyg']:.0f} cm⁴"
  1063. # Crear o actualizar anotación
  1064. if self.hover_annotation is not None:
  1065. self.hover_annotation.remove()
  1066. self.hover_annotation = self.scatter_ax.annotate(
  1067. texto,
  1068. xy=(row['peso'], row['eficiencia']),
  1069. xytext=(10, 10),
  1070. textcoords='offset points',
  1071. bbox=dict(boxstyle='round,pad=0.5', facecolor='yellow', alpha=0.7),
  1072. fontsize=8,
  1073. arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0', color='black')
  1074. )
  1075. self.sweep_canvas.draw_idle()
  1076. else:
  1077. # Mover el mouse lejos de los puntos
  1078. if self.hover_annotation is not None:
  1079. self.hover_annotation.remove()
  1080. self.hover_annotation = None
  1081. self.sweep_canvas.draw_idle()
  1082. except Exception:
  1083. pass
  1084. # Conectar evento de movimiento del mouse
  1085. self.sweep_canvas.mpl_connect('motion_notify_event', on_hover)
  1086. def export_sweep_results(self):
  1087. """Exporta resultados del barrido a Excel"""
  1088. if self.sweep_result is None:
  1089. messagebox.showerror("Error", "No hay resultados para exportar. Ejecute primero el barrido")
  1090. return
  1091. try:
  1092. # Pedir ruta de guardado
  1093. filename = filedialog.asksaveasfilename(
  1094. defaultextension=".xlsx",
  1095. filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")]
  1096. )
  1097. if not filename:
  1098. return
  1099. # Crear archivo Excel con pandas
  1100. with pd.ExcelWriter(filename, engine='openpyxl') as writer:
  1101. # Hoja 1: Todos los resultados
  1102. self.sweep_result.to_excel(writer, sheet_name='Resultados', index=False)
  1103. # Hoja 2: Pareto front
  1104. if self.sweep_pareto is not None:
  1105. self.sweep_pareto.to_excel(writer, sheet_name='Pareto Front', index=False)
  1106. messagebox.showinfo("Éxito", f"Resultados exportados a:\n{filename}")
  1107. except Exception as e:
  1108. messagebox.showerror("Error", f"Error exportando: {str(e)}")
  1109. def clear_sweep_results(self):
  1110. """Limpia los resultados del barrido"""
  1111. self.sweep_result = None
  1112. self.sweep_pareto = None
  1113. if self.sweep_canvas is not None:
  1114. self.sweep_canvas.get_tk_widget().destroy()
  1115. self.sweep_canvas = None
  1116. # Volver a gráfico normal
  1117. self.ax.clear()
  1118. self.canvas.draw()
  1119. messagebox.showinfo("Info", "Resultados de barrido limpiados")
  1120. if __name__ == "__main__":
  1121. app = SectionDesignerApp()
  1122. app.mainloop()