Переглянути джерело

funciones de barrido de ipe

dacowars 3 тижнів тому
батько
коміт
5487e7155a

BIN
__pycache__/app_designer.cpython-313.pyc


BIN
__pycache__/parametric_sweep.cpython-313.pyc


+ 441 - 7
app_designer.py

@@ -1,11 +1,13 @@
 import tkinter as tk
-from tkinter import ttk, messagebox, simpledialog
+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
@@ -322,6 +324,12 @@ class SectionDesignerApp(tk.Tk):
         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):
@@ -362,7 +370,7 @@ class SectionDesignerApp(tk.Tk):
         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"],
+            values=["IPE", "Personalizada", "Puente nuevo", "Barrido Paramétrico"],
             state='readonly',
             width=20
         )
@@ -428,7 +436,7 @@ class SectionDesignerApp(tk.Tk):
         # 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.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)
@@ -484,11 +492,15 @@ class SectionDesignerApp(tk.Tk):
         # 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.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)
 
@@ -518,26 +530,136 @@ class SectionDesignerApp(tk.Tk):
 
         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 y Personalizada"""
+        """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)
-            # Aquí podrías cargar valores por defecto o actuales para el puente nuevo
         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)
-            # Mostrar los puntos actuales en el texto
             if self.current_points is not None:
                 import json
                 puntos_json = json.dumps(self.current_points.tolist(), indent=2)
@@ -900,6 +1022,318 @@ MECÁNICAS:
         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()

+ 354 - 0
parametric_sweep.py

@@ -0,0 +1,354 @@
+"""
+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