|
|
@@ -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()
|