| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- """
- 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
|