parametric_sweep.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. """
  2. Módulo de Barrido Paramétrico para Secciones de Acero
  3. Permite realizar barridos paramétricos dinámicos sobre secciones IPE y Puente nuevo,
  4. fijando algunos parámetros y variando otros sistemáticamente.
  5. """
  6. import numpy as np
  7. import pandas as pd
  8. import itertools
  9. from typing import Dict, List, Tuple
  10. import math
  11. def generate_ipe_points(H, b, tf, tw):
  12. """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
  13. h_alma = H - 2*tf
  14. x_alma_ini = (b - tw) / 2
  15. x_alma_fin = (b + tw) / 2
  16. points = np.array([
  17. [0, 0],
  18. [b, 0],
  19. [b, tf],
  20. [x_alma_fin, tf],
  21. [x_alma_fin, H - tf],
  22. [b, H - tf],
  23. [b, H],
  24. [0, H],
  25. [0, H - tf],
  26. [x_alma_ini, H - tf],
  27. [x_alma_ini, tf],
  28. [0, tf],
  29. [0, 0],
  30. ])
  31. return points
  32. def generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta):
  33. """Genera los puntos de la sección tipo puente nuevo"""
  34. hr = (b-tw) / 2 * math.tan(math.radians(theta))
  35. hl = tr / math.cos(math.radians(theta))
  36. hr_ = (b/2 - ta - tw/2)* math.tan(math.radians(theta))
  37. points = np.array([
  38. [0, 0],
  39. [b, 0],
  40. [b, tf + ha],
  41. [(b + tw)/2, tf + ha + hr],
  42. [(b + tw)/2, tf + ha + hr - hl],
  43. [b - ta, tf + ha + hr - hl - hr_],
  44. [b - ta, tf],
  45. [(b + tw)/2, tf],
  46. [(b + tw)/2, h - tf],
  47. [b - ta, h - tf],
  48. [b - ta, h - tf - ha - hr + hl + hr_],
  49. [(b + tw)/2, h - tf - ha - hr + hl],
  50. [(b + tw)/2, h - tf - ha - hr],
  51. [b, h - tf - ha],
  52. [b, h],
  53. [0, h],
  54. [0, h - tf - ha],
  55. [(b - tw)/2, h - tf - ha - hr],
  56. [(b - tw)/2, h - tf - ha - hr + hl],
  57. [ta, h - tf - ha - hr + hl + hr_],
  58. [ta, h - tf],
  59. [(b - tw)/2, h - tf],
  60. [(b - tw)/2, tf],
  61. [ta, tf],
  62. [ta, tf + ha + hr - hl - hr_],
  63. [(b - tw)/2, tf + ha + hr - hl],
  64. [(b - tw)/2, tf + ha + hr],
  65. [0, tf + ha],
  66. [0, 0],
  67. ])
  68. return points
  69. def calculate_section_properties(points):
  70. """Calcula propiedades de la sección a partir de puntos"""
  71. # Constantes de material
  72. DENS = 7850 # kg/m3
  73. FY = 355 # MPa
  74. GAMMAS = 1.05
  75. EYOUNG = 210000 # MPa
  76. NU = 0.3
  77. FYD = FY * 10**6 / GAMMAS
  78. npuntos = len(points)
  79. px = points[:, 0]
  80. py = points[:, 1]
  81. bmax = np.amax(px) - np.amin(px)
  82. hmax = np.amax(py) - np.amin(py)
  83. # Perímetro
  84. long_i = np.zeros(npuntos - 1)
  85. for i in range(npuntos - 1):
  86. long_i[i] = ((points[i+1, 0] - points[i, 0])**2 + (points[i+1, 1] - points[i, 1])**2)**(1/2)
  87. perimetro = abs(sum(long_i))
  88. # Área
  89. area_i = np.zeros(npuntos - 1)
  90. for i in range(npuntos - 1):
  91. area_i[i] = (points[i+1, 0] - points[i, 0]) * (points[i+1, 1] + points[i, 1]) / 2
  92. area = abs(sum(area_i))
  93. # Peso
  94. peso = area * DENS
  95. # Centro de gravedad
  96. cdg_i = np.zeros([npuntos, 2])
  97. for i in range(npuntos - 1):
  98. h1 = points[i, 1]
  99. h2 = points[i+1, 1]
  100. b = points[i+1, 0] - points[i, 0]
  101. d = points[i, 0]
  102. if h1 + h2 == 0:
  103. cdg_i[i, 1] = 0
  104. else:
  105. cdg_i[i, 1] = 1/3 * (h1*h1 + h1*h2 + h2*h2) / (h1 + h2)
  106. if h1 + h2 == 0:
  107. cdg_i[i, 0] = d + b/2
  108. else:
  109. cdg_i[i, 0] = d + b/3 * (h1 + 2*h2) / (h1 + h2)
  110. statico_i = np.zeros([npuntos, 2])
  111. for i in range(npuntos - 1):
  112. statico_i[i, 1] = area_i[i] * cdg_i[i, 1]
  113. statico_i[i, 0] = area_i[i] * cdg_i[i, 0]
  114. cdg = sum(statico_i) / sum(area_i)
  115. xg = cdg[0]
  116. yg = cdg[1]
  117. # Fibras más alejadas
  118. v1y = np.amax(py) - yg
  119. v2y = np.amin(py) - yg
  120. v1x = np.amax(px) - xg
  121. v2x = np.amin(px) - xg
  122. # Momentos de inercia
  123. inercia_i = np.zeros([npuntos, 3])
  124. for i in range(npuntos - 1):
  125. h1 = points[i, 1]
  126. h2 = points[i+1, 1]
  127. b = points[i+1, 0] - points[i, 0]
  128. d = points[i, 0]
  129. xgi = cdg_i[i, 0]
  130. ygi = cdg_i[i, 1]
  131. ai = area_i[i]
  132. if h2 >= h1:
  133. ixcuad_G_local = 1/12 * b * (h1**3) + b * h1 * (h1/2 - ygi)**2
  134. ixtriang_G_loc = 1/36 * b * (h2-h1)**3 + 1/2 * b * (h2-h1) * ((2*h1+h2)/3 - ygi)**2
  135. else:
  136. ixcuad_G_local = 1/12 * b * (h2**3) + b * h2 * (h2/2 - ygi)**2
  137. ixtriang_G_loc = 1/36 * b * (h1-h2)**3 + 1/2 * b * (h1-h2) * ((2*h2+h1)/3 - ygi)**2
  138. inercia_i[i, 0] = ixcuad_G_local + ixtriang_G_loc + ai * (yg - ygi)**2
  139. if h2 >= h1:
  140. iycuad = 1/12 * h1 * b**3 + h1 * b * (b/2 + d - xgi)**2
  141. iytrian = 1/36 * (h2-h1) * b**3 + 1/2 * b * (h2-h1) * (2/3*b + d - xgi)**2
  142. else:
  143. iycuad = 1/12 * h2 * b**3 + h2 * b * (b/2 + d - xgi)**2
  144. iytrian = 1/36 * (h1-h2) * b**3 + 1/2 * b * (h1-h2) * (1/3*b + d - xgi)**2
  145. inercia_i[i, 1] = iycuad + iytrian + ai * (xg - xgi)**2
  146. if h2 >= h1:
  147. pxygcuadrado = b * h1 * (-h1/2 + ygi) * (-d - b/2 + xgi)
  148. pxytriangulo = b*b * (h2-h1)**2 / 72 + b * (h2-h1) / 2 * (-(h2-h1)/3 - h1 + ygi) * (-d - 2/3*b + xgi)
  149. else:
  150. pxygcuadrado = b * h2 * (-h2/2 + ygi) * (-d - b/2 + xgi)
  151. pxytriangulo = -b*b * (h1-h2)**2 / 72 + b * (h1-h2) / 2 * (-(h1-h2)/3 - h2 + ygi) * (-d - 1/3*b + xgi)
  152. inercia_i[i, 2] = pxygcuadrado + pxytriangulo + ai * (-xg + xgi) * (-yg + ygi)
  153. ig = sum(inercia_i)
  154. ixg = abs(ig[0])
  155. iyg = abs(ig[1])
  156. pxyg = ig[2]
  157. if sum(area_i) >= 0:
  158. pxyg = pxyg
  159. else:
  160. pxyg = -pxyg
  161. # Radios de giro
  162. rx = (ixg / area)**0.5
  163. ry = (iyg / area)**0.5
  164. # Ejes principales
  165. ic = (ixg + iyg) / 2
  166. ir = (((ixg - iyg)/2)**2 + pxyg**2)**0.5
  167. imax = ic + ir
  168. imin = ic - ir
  169. rmax = (imax / area)**0.5
  170. rmin = (imin / area)**0.5
  171. # Conversión a unidades prácticas (cm, cm2, cm3, cm4, kN)
  172. pot = 2
  173. area_cm2 = area * 10**(pot*2)
  174. ixg_cm4 = ixg * 10**(pot*4)
  175. iyg_cm4 = iyg * 10**(pot*4)
  176. imax_cm4 = imax * 10**(pot*4)
  177. imin_cm4 = imin * 10**(pot*4)
  178. return {
  179. 'area': area_cm2,
  180. 'ixg': ixg_cm4,
  181. 'iyg': iyg_cm4,
  182. 'imax': imax_cm4,
  183. 'imin': imin_cm4,
  184. 'peso': peso,
  185. }
  186. class ParametricSweep:
  187. """Ejecuta barridos paramétricos de secciones"""
  188. def __init__(self, section_type: str, fixed_params: Dict, sweep_configs: Dict):
  189. """
  190. Args:
  191. section_type: "ipe" o "puente_nuevo"
  192. fixed_params: {param_name: value, ...}
  193. sweep_configs: {param_name: {"min": v, "max": v, "steps": n}, ...}
  194. """
  195. self.section_type = section_type
  196. self.fixed_params = fixed_params.copy()
  197. self.sweep_configs = sweep_configs.copy()
  198. self.results_df = None
  199. self.pareto_front = None
  200. def _is_valid_geometry(self, params: Dict) -> bool:
  201. """Valida que la geometría sea válida para IPE"""
  202. if self.section_type == "ipe":
  203. H = params.get('H', 0)
  204. b = params.get('b', 0)
  205. tf = params.get('tf', 0)
  206. tw = params.get('tw', 0)
  207. # Validaciones básicas
  208. if H <= 0 or b <= 0 or tf <= 0 or tw <= 0:
  209. return False
  210. # Espesor de flange no puede ser más de la mitad de altura
  211. if tf > H / 2:
  212. return False
  213. # Espesor de alma no puede ser mayor que ancho
  214. if tw > b:
  215. return False
  216. # Altura del alma positiva
  217. if H - 2*tf <= 0:
  218. return False
  219. return True
  220. def generate_sweep_matrix(self) -> List[Dict]:
  221. """Genera todas las combinaciones de parámetros a barrer"""
  222. # Obtener parámetros a barrer
  223. sweep_params = list(self.sweep_configs.keys())
  224. # Generar valores para cada parámetro
  225. param_values = {}
  226. for param in sweep_params:
  227. config = self.sweep_configs[param]
  228. values = np.linspace(config['min'], config['max'], config['steps'])
  229. param_values[param] = values
  230. # Generar todas las combinaciones
  231. combinations = []
  232. for combo in itertools.product(*[param_values[p] for p in sweep_params]):
  233. params = self.fixed_params.copy()
  234. for i, param in enumerate(sweep_params):
  235. params[param] = combo[i]
  236. if self._is_valid_geometry(params):
  237. combinations.append(params)
  238. return combinations
  239. def execute_sweep(self) -> pd.DataFrame:
  240. """Ejecuta el barrido y retorna DataFrame con resultados"""
  241. combinations = self.generate_sweep_matrix()
  242. if not combinations:
  243. raise ValueError("No hay combinaciones válidas en el barrido")
  244. results = []
  245. for params in combinations:
  246. # Generar puntos según tipo
  247. if self.section_type == "ipe":
  248. points = generate_ipe_points(
  249. params['H'], params['b'], params['tf'], params['tw']
  250. )
  251. elif self.section_type == "puente_nuevo":
  252. points = generate_puente_nuevo_points(
  253. params['h'], params['b'], params['tf'], params['tw'],
  254. params['ha'], params['ta'], params['tr'], params['theta']
  255. )
  256. else:
  257. continue
  258. # Calcular propiedades
  259. props = calculate_section_properties(points)
  260. # Construir fila de resultados
  261. row = params.copy()
  262. row.update(props)
  263. results.append(row)
  264. self.results_df = pd.DataFrame(results)
  265. # Normalizar valores para cálculo de Pareto
  266. self.results_df['peso_norm'] = (
  267. self.results_df['peso'] / self.results_df['peso'].max()
  268. )
  269. self.results_df['ixg_norm'] = (
  270. self.results_df['ixg'] / self.results_df['ixg'].max()
  271. )
  272. self.results_df['iyg_norm'] = (
  273. self.results_df['iyg'] / self.results_df['iyg'].max()
  274. )
  275. return self.results_df
  276. def find_pareto_front(self) -> pd.DataFrame:
  277. """Ordena soluciones por eficiencia: (Ix+Iy) / peso
  278. Mayor eficiencia = mejor inercia con menos peso
  279. """
  280. if self.results_df is None:
  281. raise ValueError("Ejecutar execute_sweep() primero")
  282. # Calcular eficiencia para cada solución
  283. self.results_df['eficiencia'] = (
  284. (self.results_df['ixg']) /
  285. (self.results_df['peso'] * 1000)
  286. )
  287. # Ordenar por eficiencia descendente (mejor primero)
  288. self.pareto_front = self.results_df.sort_values('eficiencia', ascending=False)
  289. return self.pareto_front
  290. def get_results_dataframe(self) -> pd.DataFrame:
  291. """Retorna DataFrame con todos los resultados"""
  292. return self.results_df
  293. def get_pareto_front(self) -> pd.DataFrame:
  294. """Retorna DataFrame con Pareto front"""
  295. return self.pareto_front