220 lines
7.6 KiB
Python
220 lines
7.6 KiB
Python
# tab_search.py
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import math
|
|
import matplotlib
|
|
|
|
matplotlib.use("TkAgg")
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
|
|
|
|
class TabSearch:
|
|
def __init__(self, notebook, tab_simulator):
|
|
"""
|
|
Tercera pestaña: 'Búsqueda de ángulo'.
|
|
Recibe 'tab_simulator' para leer la config en la pestaña 1:
|
|
- masa (m)
|
|
- coef de rozamiento (b)
|
|
- altura h0
|
|
- si hay drag o no
|
|
- alcance (si el modo es "Alcance (m)")
|
|
"""
|
|
self.notebook = notebook
|
|
self.tab_sim = tab_simulator # referencia a la pestaña de simulación
|
|
|
|
self.frame = tk.Frame(notebook, width=280)
|
|
self.frame.pack(side="left", fill="both", expand=True)
|
|
# Fijamos ancho y desactivamos propagación para la columna
|
|
self.frame.pack_propagate(False)
|
|
|
|
# -- Layout principal: izquierda (botones + info), derecha (gráfico) --
|
|
self.frame_left = tk.Frame(self.frame, width=240)
|
|
self.frame_left.pack(side="left", fill="y", padx=5, pady=5)
|
|
self.frame_left.pack_propagate(False)
|
|
|
|
self.frame_right = tk.Frame(self.frame)
|
|
self.frame_right.pack(side="right", fill="both", expand=True, padx=5, pady=5)
|
|
|
|
# 1) Botones e info en la parte izquierda
|
|
tk.Button(self.frame_left, text="Importar config", command=self.import_config).pack(pady=5)
|
|
tk.Button(self.frame_left, text="Ejecutar búsqueda", command=self.run_search).pack(pady=5)
|
|
|
|
# Etiquetas para mostrar la config importada
|
|
self.label_m = tk.Label(self.frame_left, text="Masa: ??? kg", bg="white")
|
|
self.label_m.pack(pady=2, anchor="w")
|
|
|
|
self.label_b = tk.Label(self.frame_left, text="b: ???", bg="white")
|
|
self.label_b.pack(pady=2, anchor="w")
|
|
|
|
self.label_h0 = tk.Label(self.frame_left, text="h0: ??? m", bg="white")
|
|
self.label_h0.pack(pady=2, anchor="w")
|
|
|
|
self.label_drag = tk.Label(self.frame_left, text="Drag: ???", bg="white")
|
|
self.label_drag.pack(pady=2, anchor="w")
|
|
|
|
self.label_x_target = tk.Label(self.frame_left, text="X_target: ???", bg="white")
|
|
self.label_x_target.pack(pady=2, anchor="w")
|
|
|
|
# Label para mostrar el ángulo/vel. hallados tras la búsqueda
|
|
self.label_result = tk.Label(self.frame_left, text="Resultado:\n...", fg="green", bg="white")
|
|
self.label_result.pack(pady=10, fill="x")
|
|
|
|
# 2) Creamos figure + canvas a la derecha (para dibujar la curva)
|
|
self.fig = plt.Figure(figsize=(4, 3), dpi=100)
|
|
self.ax = self.fig.add_subplot(111)
|
|
self.ax.set_xlabel("Ángulo (grados)")
|
|
self.ax.set_ylabel("Vel. requerida (m/s)")
|
|
|
|
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame_right)
|
|
self.canvas_widget = self.canvas.get_tk_widget()
|
|
self.canvas_widget.pack(fill="both", expand=True)
|
|
|
|
# Variables donde guardaremos la config importada
|
|
self.m = 1.0 # masa
|
|
self.b = 0.0 # coef rozamiento
|
|
self.h0 = 0.0 # altura
|
|
self.has_drag = False
|
|
self.x_target = None # Alcance importado, si está en modo "Alcance (m)"
|
|
|
|
def import_config(self):
|
|
"""
|
|
Lee la config actual de la pestaña 1 (simulador):
|
|
- masa
|
|
- b (si hay rozamiento)
|
|
- h0
|
|
- check_rozamiento => has_drag
|
|
- y si param es "Alcance (m)", tomamos ese v0 como x_target
|
|
"""
|
|
try:
|
|
# Masa
|
|
self.m = float(self.tab_sim.entry_masa.get())
|
|
|
|
# Rozamiento
|
|
if self.tab_sim.check_rozamiento.get():
|
|
self.has_drag = True
|
|
self.b = float(self.tab_sim.entry_b.get())
|
|
else:
|
|
self.has_drag = False
|
|
self.b = 0.0
|
|
|
|
# Altura
|
|
self.h0 = float(self.tab_sim.entry_h0.get())
|
|
|
|
# Alcance si param== "Alcance (m)"
|
|
if self.tab_sim.parametro_var.get() == "Alcance (m)":
|
|
self.x_target = float(self.tab_sim.entry_v0.get())
|
|
else:
|
|
self.x_target = None
|
|
|
|
# Actualizamos en pantalla
|
|
self.label_m.config(text=f"Masa: {self.m:.2f} kg")
|
|
self.label_b.config(text=f"b: {self.b:.4f}")
|
|
self.label_h0.config(text=f"h0: {self.h0:.2f} m")
|
|
self.label_drag.config(text=f"Drag: {self.has_drag}")
|
|
|
|
if self.x_target is not None:
|
|
self.label_x_target.config(text=f"X_target: {self.x_target:.2f} m")
|
|
else:
|
|
self.label_x_target.config(text="X_target: (No hay)")
|
|
|
|
self.label_result.config(text="Config importada OK", bg="lightgreen", fg="black")
|
|
|
|
except ValueError:
|
|
self.label_result.config(text="Error al leer config", bg="red", fg="white")
|
|
self.x_target = None
|
|
|
|
def run_search(self):
|
|
"""
|
|
Barre ángulos de 0..90 y busca la velocidad mínima
|
|
para alcanzar la distancia x_target (si existe).
|
|
Si no hay x_target (porque el modo era Vel(m/s)),
|
|
mostramos error.
|
|
"""
|
|
if self.x_target is None:
|
|
# Si el usuario está en modo "Velocidad (m/s)" en la pestaña 1,
|
|
# no tenemos 'x_target' válido. Lógica:
|
|
self.label_result.config(
|
|
text="Error: en la pestaña 1 no se eligió 'Alcance (m)'",
|
|
bg="red", fg="white"
|
|
)
|
|
return
|
|
|
|
X_target = self.x_target # usar la que importamos
|
|
angulos = []
|
|
velocidades = []
|
|
best_angle = None
|
|
best_v = 1e9
|
|
|
|
# Recorremos ángulos de 0..90
|
|
for angle_deg in range(0, 91):
|
|
v0_min = 0.0
|
|
v0_max = 300.0
|
|
final_v = 0.0
|
|
|
|
# Bisección
|
|
for _ in range(100):
|
|
guess = 0.5*(v0_min + v0_max)
|
|
dist = self.simular_dist(guess, angle_deg, self.has_drag)
|
|
if abs(dist - X_target) < 0.1:
|
|
final_v = guess
|
|
break
|
|
if dist < X_target:
|
|
v0_min = guess
|
|
else:
|
|
v0_max = guess
|
|
final_v = guess
|
|
|
|
angulos.append(angle_deg)
|
|
velocidades.append(final_v)
|
|
|
|
if final_v < best_v:
|
|
best_v = final_v
|
|
best_angle = angle_deg
|
|
|
|
# Dibujamos
|
|
self.ax.clear()
|
|
self.ax.set_xlabel("Ángulo (grados)")
|
|
self.ax.set_ylabel("Vel. requerida (m/s)")
|
|
self.ax.plot(angulos, velocidades, color="red")
|
|
|
|
# Marcamos el punto mínimo
|
|
self.ax.plot([best_angle], [best_v], marker="o", markersize=8, color="green")
|
|
self.canvas.draw()
|
|
|
|
self.label_result.config(
|
|
text=f"Mín. en {best_angle}°, v0={best_v:.2f} m/s",
|
|
bg="white", fg="green"
|
|
)
|
|
|
|
def simular_dist(self, v0_guess, angle_deg, drag):
|
|
"""
|
|
Integra la distancia horizontal partiendo de (0,h0)
|
|
hasta y<=0. Devuelve x final. Usa self.m, self.b, self.h0, drag (bool).
|
|
"""
|
|
dt = 0.01
|
|
x = 0.0
|
|
y = self.h0
|
|
alpha = math.radians(angle_deg)
|
|
vx = v0_guess * math.cos(alpha)
|
|
vy = v0_guess * math.sin(alpha)
|
|
|
|
while True:
|
|
if drag:
|
|
ax = -(self.b / self.m) * vx
|
|
ay = -9.8 - (self.b / self.m) * vy
|
|
else:
|
|
ax = 0.0
|
|
ay = -9.8
|
|
|
|
vx += ax*dt
|
|
vy += ay*dt
|
|
x += vx*dt
|
|
y += vy*dt
|
|
|
|
if y <= 0:
|
|
break
|
|
|
|
return x
|