# target_simulator/gui/analysis_window.py """ A Toplevel window for displaying real-time performance analysis, including error statistics and plots. """ import tkinter as tk from tkinter import ttk, messagebox from typing import Optional, Dict from target_simulator.analysis.performance_analyzer import PerformanceAnalyzer from target_simulator.analysis.simulation_state_hub import SimulationStateHub try: from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg MATPLOTLIB_AVAILABLE = True except ImportError: MATPLOTLIB_AVAILABLE = False UPDATE_INTERVAL_MS = 1000 # Update analysis every second class AnalysisWindow(tk.Toplevel): """ A window that displays real-time analysis of tracking performance. """ def __init__(self, master, analyzer: PerformanceAnalyzer, hub: SimulationStateHub): super().__init__(master) self.title("Performance Analysis") self.geometry("800x600") self.transient(master) if not MATPLOTLIB_AVAILABLE: messagebox.showerror( "Dependency Missing", "Matplotlib is required for the analysis window. Please install it (`pip install matplotlib`).", parent=self, ) self.destroy() return self._analyzer = analyzer self._hub = hub self._active = True self.selected_target_id = tk.IntVar() self._create_widgets() self._update_loop() self.protocol("WM_DELETE_WINDOW", self._on_close) def _create_widgets(self): main_pane = ttk.PanedWindow(self, orient=tk.VERTICAL) main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # --- Top Frame for Stats Table --- stats_frame = ttk.LabelFrame(main_pane, text="Error Statistics (feet)") main_pane.add(stats_frame, weight=1) self._create_stats_widgets(stats_frame) # --- Bottom Frame for Plot --- plot_frame = ttk.LabelFrame(main_pane, text="Error Over Time (feet)") main_pane.add(plot_frame, weight=3) self._create_plot_widgets(plot_frame) def _create_stats_widgets(self, parent): top_bar = ttk.Frame(parent) top_bar.pack(fill=tk.X, padx=5, pady=5) ttk.Label(top_bar, text="Select Target ID:").pack(side=tk.LEFT) self.target_selector = ttk.Combobox( top_bar, textvariable=self.selected_target_id, state="readonly", width=5 ) self.target_selector.pack(side=tk.LEFT, padx=5) self.target_selector.bind("<>", self._on_target_select) columns = ("metric", "x_error", "y_error", "z_error") self.stats_tree = ttk.Treeview(parent, columns=columns, show="headings") self.stats_tree.heading("metric", text="Metric") self.stats_tree.heading("x_error", text="Error X") self.stats_tree.heading("y_error", text="Error Y") self.stats_tree.heading("z_error", text="Error Z") self.stats_tree.column("metric", width=120, anchor=tk.W) self.stats_tree.column("x_error", anchor=tk.E) self.stats_tree.column("y_error", anchor=tk.E) self.stats_tree.column("z_error", anchor=tk.E) self.stats_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) def _create_plot_widgets(self, parent): fig = Figure(figsize=(5, 3), dpi=100) self.ax = fig.add_subplot(111) self.ax.set_title("Instantaneous Error") self.ax.set_xlabel("Time (s)") self.ax.set_ylabel("Error (ft)") (self.line_x,) = self.ax.plot([], [], lw=2, label="Error X") (self.line_y,) = self.ax.plot([], [], lw=2, label="Error Y") (self.line_z,) = self.ax.plot([], [], lw=2, label="Error Z") self.ax.legend() self.ax.grid(True) fig.tight_layout() self.canvas = FigureCanvasTkAgg(fig, master=parent) self.canvas.draw() self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) def _update_loop(self): if not self._active: return self._update_target_selector() analysis_results = self._analyzer.analyze() if self.selected_target_id.get() in analysis_results: self._update_stats_table(analysis_results[self.selected_target_id.get()]) self._update_plot(self.selected_target_id.get()) else: self._clear_views() self.after(UPDATE_INTERVAL_MS, self._update_loop) def _update_target_selector(self): target_ids = sorted(self._hub.get_all_target_ids()) self.target_selector["values"] = target_ids if target_ids and self.selected_target_id.get() not in target_ids: self.selected_target_id.set(target_ids[0]) def _on_target_select(self, event=None): # Trigger an immediate update when user changes selection analysis_results = self._analyzer.analyze() if self.selected_target_id.get() in analysis_results: self._update_stats_table(analysis_results[self.selected_target_id.get()]) self._update_plot(self.selected_target_id.get()) else: self._clear_views() def _update_stats_table(self, results: Dict): self.stats_tree.delete(*self.stats_tree.get_children()) metrics = ["mean", "std_dev", "rmse"] for metric in metrics: self.stats_tree.insert( "", "end", values=( metric.replace("_", " ").title(), f"{results['x'][metric]:.3f}", f"{results['y'][metric]:.3f}", f"{results['z'][metric]:.3f}", ), ) def _update_plot(self, target_id: int): history = self._hub.get_target_history(target_id) if not history or not history["real"] or len(history["simulated"]) < 2: self.line_x.set_data([], []) self.line_y.set_data([], []) self.line_z.set_data([], []) self.ax.relim() self.ax.autoscale_view() self.canvas.draw_idle() return times, errors_x, errors_y, errors_z = [], [], [], [] sim_hist = sorted(history["simulated"]) for real_state in history["real"]: real_ts, real_x, real_y, real_z = real_state p1, p2 = self._analyzer._find_bracketing_points(real_ts, sim_hist) if p1 and p2: interp_state = self._analyzer._interpolate(real_ts, p1, p2) _ts, interp_x, interp_y, interp_z = interp_state times.append(real_ts) errors_x.append(real_x - interp_x) errors_y.append(real_y - interp_y) errors_z.append(real_z - interp_z) self.line_x.set_data(times, errors_x) self.line_y.set_data(times, errors_y) self.line_z.set_data(times, errors_z) self.ax.relim() self.ax.autoscale_view() self.canvas.draw_idle() def _clear_views(self): self.stats_tree.delete(*self.stats_tree.get_children()) self.line_x.set_data([], []) self.line_y.set_data([], []) self.line_z.set_data([], []) self.ax.relim() self.ax.autoscale_view() self.canvas.draw_idle() def _on_close(self): self._active = False self.destroy()