""" Módulo de Barrido Paramétrico para Secciones de Acero Permite realizar barridos paramétricos dinámicos sobre secciones IPE y Puente nuevo, fijando algunos parámetros y variando otros sistemáticamente. """ import numpy as np import pandas as pd import itertools from typing import Dict, List, Tuple import math def generate_ipe_points(H, b, tf, tw): """Genera los puntos de una sección IPE a partir de parámetros normalizados""" h_alma = H - 2*tf x_alma_ini = (b - tw) / 2 x_alma_fin = (b + tw) / 2 points = np.array([ [0, 0], [b, 0], [b, tf], [x_alma_fin, tf], [x_alma_fin, H - tf], [b, H - tf], [b, H], [0, H], [0, H - tf], [x_alma_ini, H - tf], [x_alma_ini, tf], [0, tf], [0, 0], ]) 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""" 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)) 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 propiedades de la sección a partir de puntos""" # Constantes de material DENS = 7850 # kg/m3 FY = 355 # MPa GAMMAS = 1.05 EYOUNG = 210000 # MPa NU = 0.3 FYD = FY * 10**6 / GAMMAS npuntos = len(points) px = points[:, 0] py = points[:, 1] 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] = ((points[i+1, 0] - points[i, 0])**2 + (points[i+1, 1] - points[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] = (points[i+1, 0] - points[i, 0]) * (points[i+1, 1] + points[i, 1]) / 2 area = abs(sum(area_i)) # Peso peso = area * DENS # Centro de gravedad cdg_i = np.zeros([npuntos, 2]) for i in range(npuntos - 1): h1 = points[i, 1] h2 = points[i+1, 1] b = points[i+1, 0] - points[i, 0] d = points[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) 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 = points[i, 1] h2 = points[i+1, 1] b = points[i+1, 0] - points[i, 0] d = points[i, 0] xgi = cdg_i[i, 0] ygi = cdg_i[i, 1] ai = area_i[i] 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 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 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 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 # Conversión a unidades prácticas (cm, cm2, cm3, cm4, kN) pot = 2 area_cm2 = area * 10**(pot*2) ixg_cm4 = ixg * 10**(pot*4) iyg_cm4 = iyg * 10**(pot*4) imax_cm4 = imax * 10**(pot*4) imin_cm4 = imin * 10**(pot*4) return { 'area': area_cm2, 'ixg': ixg_cm4, 'iyg': iyg_cm4, 'imax': imax_cm4, 'imin': imin_cm4, 'peso': peso, } class ParametricSweep: """Ejecuta barridos paramétricos de secciones""" def __init__(self, section_type: str, fixed_params: Dict, sweep_configs: Dict): """ Args: section_type: "ipe" o "puente_nuevo" fixed_params: {param_name: value, ...} sweep_configs: {param_name: {"min": v, "max": v, "steps": n}, ...} """ self.section_type = section_type self.fixed_params = fixed_params.copy() self.sweep_configs = sweep_configs.copy() self.results_df = None self.pareto_front = None def _is_valid_geometry(self, params: Dict) -> bool: """Valida que la geometría sea válida para IPE""" if self.section_type == "ipe": H = params.get('H', 0) b = params.get('b', 0) tf = params.get('tf', 0) tw = params.get('tw', 0) # Validaciones básicas if H <= 0 or b <= 0 or tf <= 0 or tw <= 0: return False # Espesor de flange no puede ser más de la mitad de altura if tf > H / 2: return False # Espesor de alma no puede ser mayor que ancho if tw > b: return False # Altura del alma positiva if H - 2*tf <= 0: return False return True def generate_sweep_matrix(self) -> List[Dict]: """Genera todas las combinaciones de parámetros a barrer""" # Obtener parámetros a barrer sweep_params = list(self.sweep_configs.keys()) # Generar valores para cada parámetro param_values = {} for param in sweep_params: config = self.sweep_configs[param] values = np.linspace(config['min'], config['max'], config['steps']) param_values[param] = values # Generar todas las combinaciones combinations = [] for combo in itertools.product(*[param_values[p] for p in sweep_params]): params = self.fixed_params.copy() for i, param in enumerate(sweep_params): params[param] = combo[i] if self._is_valid_geometry(params): combinations.append(params) return combinations def execute_sweep(self) -> pd.DataFrame: """Ejecuta el barrido y retorna DataFrame con resultados""" combinations = self.generate_sweep_matrix() if not combinations: raise ValueError("No hay combinaciones válidas en el barrido") results = [] for params in combinations: # Generar puntos según tipo if self.section_type == "ipe": points = generate_ipe_points( params['H'], params['b'], params['tf'], params['tw'] ) elif self.section_type == "puente_nuevo": points = generate_puente_nuevo_points( params['h'], params['b'], params['tf'], params['tw'], params['ha'], params['ta'], params['tr'], params['theta'] ) else: continue # Calcular propiedades props = calculate_section_properties(points) # Construir fila de resultados row = params.copy() row.update(props) results.append(row) self.results_df = pd.DataFrame(results) # Normalizar valores para cálculo de Pareto self.results_df['peso_norm'] = ( self.results_df['peso'] / self.results_df['peso'].max() ) self.results_df['ixg_norm'] = ( self.results_df['ixg'] / self.results_df['ixg'].max() ) self.results_df['iyg_norm'] = ( self.results_df['iyg'] / self.results_df['iyg'].max() ) return self.results_df def find_pareto_front(self) -> pd.DataFrame: """Ordena soluciones por eficiencia: (Ix+Iy) / peso Mayor eficiencia = mejor inercia con menos peso """ if self.results_df is None: raise ValueError("Ejecutar execute_sweep() primero") # Calcular eficiencia para cada solución self.results_df['eficiencia'] = ( (self.results_df['ixg'] + self.results_df['iyg']) / (self.results_df['peso'] * 1000) ) # Ordenar por eficiencia descendente (mejor primero) self.pareto_front = self.results_df.sort_values('eficiencia', ascending=False) return self.pareto_front def get_results_dataframe(self) -> pd.DataFrame: """Retorna DataFrame con todos los resultados""" return self.results_df def get_pareto_front(self) -> pd.DataFrame: """Retorna DataFrame con Pareto front""" return self.pareto_front