diff --git a/debug_brackets.py b/debug_brackets.py new file mode 100644 index 0000000..52a304b --- /dev/null +++ b/debug_brackets.py @@ -0,0 +1,31 @@ +import json + +data = json.load(open('archive_simulations/20251113_170236_scenario_dritto.json')) +sim = data['simulation_results']['0']['simulated'] +real = data['simulation_results']['0']['real'] + +print(f"Total simulated states: {len(sim)}") +print(f"Total real states: {len(real)}") +print(f"Sim range: {sim[0][0]:.2f} to {sim[-1][0]:.2f}") +print(f"Real range: {real[0][0]:.2f} to {real[-1][0]:.2f}") + +# Check how many real states find brackets +bracketed = 0 +first_bracketed_time = None +last_bracketed_time = None + +for real_state in real: + real_ts = real_state[0] + # Find bracketing points + for i in range(len(sim) - 1): + if sim[i][0] <= real_ts <= sim[i + 1][0]: + bracketed += 1 + if first_bracketed_time is None: + first_bracketed_time = real_ts + last_bracketed_time = real_ts + break + +print(f"\nBracketed real states: {bracketed} / {len(real)} ({100*bracketed/len(real):.1f}%)") +if first_bracketed_time and last_bracketed_time: + print(f"Bracketed time range: {first_bracketed_time:.2f} to {last_bracketed_time:.2f}") + print(f"Bracketed duration: {last_bracketed_time - first_bracketed_time:.2f}s") diff --git a/debug_targets.py b/debug_targets.py new file mode 100644 index 0000000..7600114 --- /dev/null +++ b/debug_targets.py @@ -0,0 +1,9 @@ +import json + +data = json.load(open('archive_simulations/20251113_170236_scenario_dritto.json')) +targets = list(data['simulation_results'].keys()) +print(f'Targets in file: {targets}') +for t in targets: + real_count = len(data['simulation_results'][t]['real']) + sim_count = len(data['simulation_results'][t]['simulated']) + print(f'Target {t}: {real_count} real states, {sim_count} simulated states') diff --git a/target_simulator/__main__.py b/target_simulator/__main__.py index b8233a8..c0f047d 100644 --- a/target_simulator/__main__.py +++ b/target_simulator/__main__.py @@ -30,7 +30,10 @@ def main(): """Initializes and runs the application.""" import logging + # Silence verbose loggers from third-party libraries logging.getLogger("matplotlib").setLevel(logging.WARNING) + logging.getLogger("PIL").setLevel(logging.INFO) + logging.getLogger("PIL.PngImagePlugin").setLevel(logging.INFO) app = MainView() diff --git a/target_simulator/gui/analysis_window.py b/target_simulator/gui/analysis_window.py index 33adc7e..e451a1b 100644 --- a/target_simulator/gui/analysis_window.py +++ b/target_simulator/gui/analysis_window.py @@ -37,6 +37,7 @@ class AnalysisWindow(tk.Toplevel): # State variables self.selected_target_id = tk.IntVar(value=0) self._active = True + self._filtered_errors = None # Cache for spike-filtered errors used in statistics # Carica i dati e inizializza l'analizzatore self._load_data_and_setup(archive_filepath) @@ -74,8 +75,9 @@ class AnalysisWindow(tk.Toplevel): self.latency_timestamps = [] self.latency_values_ms = [] - # Create a temporary hub and populate it with historical data - self._hub = SimulationStateHub() + # Create a temporary hub with unlimited history size for analysis + # The default history_size of 200 is too small for full simulation playback + self._hub = SimulationStateHub(history_size=100000) results = archive_data.get("simulation_results", {}) for target_id_str, data in results.items(): target_id = int(target_id_str) @@ -231,7 +233,8 @@ class AnalysisWindow(tk.Toplevel): fig = Figure(figsize=(5, 6), dpi=100) # Use GridSpec for aligned subplots with shared x-axis alignment - gs = fig.add_gridspec(2, 1, height_ratios=[2, 1], hspace=0.3) + # Increased top margin to avoid title overlap with toolbar + gs = fig.add_gridspec(2, 1, height_ratios=[2, 1], hspace=0.35, top=0.95) # Top subplot: Instantaneous Error self.ax = fig.add_subplot(gs[0, 0]) @@ -252,8 +255,8 @@ class AnalysisWindow(tk.Toplevel): except Exception: pass - # Bottom subplot: Latency over time - self.ax_latency = fig.add_subplot(gs[1, 0], sharex=None) + # Bottom subplot: Latency over time - SHARE X-AXIS with top plot for synchronized zoom + self.ax_latency = fig.add_subplot(gs[1, 0], sharex=self.ax) self.ax_latency.set_title("Latency Evolution") self.ax_latency.set_xlabel( "Time (s)" @@ -272,9 +275,24 @@ class AnalysisWindow(tk.Toplevel): fig.tight_layout() - self.canvas = FigureCanvasTkAgg(fig, master=parent) + # Create frame for toolbar and canvas + plot_container = ttk.Frame(parent) + plot_container.pack(fill=tk.BOTH, expand=True) + + # Add matplotlib navigation toolbar for zoom/pan functionality at the top + from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk + + toolbar_frame = ttk.Frame(plot_container) + toolbar_frame.pack(side=tk.TOP, fill=tk.X) + + self.canvas = FigureCanvasTkAgg(fig, master=plot_container) + + toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame) + toolbar.update() + + # Pack canvas after toolbar so it's below + self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) self.canvas.draw() - self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) def _update_target_selector(self): """Refresh the target selector Combobox with IDs from the state hub. @@ -304,6 +322,8 @@ class AnalysisWindow(tk.Toplevel): def _update_stats_table(self, results: Dict): """Populate the stats Treeview with aggregated error metrics. + + Uses filtered error data (excluding spikes) if available. Args: results (Dict): A mapping containing 'x', 'y', 'z' metrics and @@ -311,18 +331,54 @@ class AnalysisWindow(tk.Toplevel): """ self.stats_tree.delete(*self.stats_tree.get_children()) - # Add rows for each error axis (X, Y, Z) - for axis in ["x", "y", "z"]: - self.stats_tree.insert( - "", - "end", - values=( - f"Error {axis.upper()}", - f"{results[axis]['mean']:.3f}", - f"{results[axis]['std_dev']:.3f}", - f"{results[axis]['rmse']:.3f}", - ), - ) + # Use filtered errors if available, otherwise fall back to analyzer results + if hasattr(self, '_filtered_errors') and self._filtered_errors: + import math + # Compute statistics from filtered data + for axis in ["x", "y", "z"]: + errors = self._filtered_errors[axis] + if errors: + n = len(errors) + mean = sum(errors) / n + variance = sum((x - mean) ** 2 for x in errors) / n + std_dev = math.sqrt(variance) + rmse = math.sqrt(sum(x**2 for x in errors) / n) + + self.stats_tree.insert( + "", + "end", + values=( + f"Error {axis.upper()}", + f"{mean:.3f}", + f"{std_dev:.3f}", + f"{rmse:.3f}", + ), + ) + else: + # No data after filtering + self.stats_tree.insert( + "", + "end", + values=( + f"Error {axis.upper()}", + "N/A", + "N/A", + "N/A", + ), + ) + else: + # Fallback to analyzer results (unfiltered) + for axis in ["x", "y", "z"]: + self.stats_tree.insert( + "", + "end", + values=( + f"Error {axis.upper()}", + f"{results[axis]['mean']:.3f}", + f"{results[axis]['std_dev']:.3f}", + f"{results[axis]['rmse']:.3f}", + ), + ) # Add latency row if available if self.estimated_latency_ms is not None: @@ -364,6 +420,8 @@ class AnalysisWindow(tk.Toplevel): def _update_plot(self, target_id: int): """Update the matplotlib plot for the given target using hub history. + + Also computes and stores filtered errors (excluding outliers) for statistics. Args: target_id (int): The identifier of the target to plot errors for. @@ -376,6 +434,8 @@ class AnalysisWindow(tk.Toplevel): self.ax.relim() self.ax.autoscale_view() self.canvas.draw_idle() + # Clear filtered data cache + self._filtered_errors = None return times, errors_x, errors_y, errors_z = [], [], [], [] @@ -394,9 +454,88 @@ class AnalysisWindow(tk.Toplevel): 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) + if not times: + 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() + self._filtered_errors = None + return + + # Filter initial transient/acquisition spikes + # Use a threshold based on the median of stable data (after initial period) + filtered_times, filtered_x, filtered_y, filtered_z = [], [], [], [] + outlier_times, outlier_x, outlier_y, outlier_z = [], [], [], [] + + # Take a sample window after initial seconds to compute typical error magnitude + min_time = min(times) + sample_window_start = min_time + 5.0 # Skip first 5 seconds + sample_window_end = min_time + 15.0 # Sample from 5s to 15s + + sample_errors = [] + for i, t in enumerate(times): + if sample_window_start <= t <= sample_window_end: + err_magnitude = (errors_x[i]**2 + errors_y[i]**2 + errors_z[i]**2) ** 0.5 + sample_errors.append(err_magnitude) + + # Compute threshold: median of sample + 10x (very permissive for normal errors) + if sample_errors: + import statistics + median_err = statistics.median(sample_errors) + # Use 20x median as threshold to catch only extreme outliers + threshold = max(median_err * 20, 500.0) # At least 500 ft threshold + else: + # Fallback if no sample data available + threshold = 1000.0 + + # Classify points as normal or outliers + for i, t in enumerate(times): + err_magnitude = (errors_x[i]**2 + errors_y[i]**2 + errors_z[i]**2) ** 0.5 + if err_magnitude > threshold: + outlier_times.append(t) + outlier_x.append(errors_x[i]) + outlier_y.append(errors_y[i]) + outlier_z.append(errors_z[i]) + else: + filtered_times.append(t) + filtered_x.append(errors_x[i]) + filtered_y.append(errors_y[i]) + filtered_z.append(errors_z[i]) + + # Store filtered errors for statistics computation + self._filtered_errors = { + 'x': filtered_x, + 'y': filtered_y, + 'z': filtered_z + } + + # Plot filtered (normal) data + self.line_x.set_data(filtered_times, filtered_x) + self.line_y.set_data(filtered_times, filtered_y) + self.line_z.set_data(filtered_times, filtered_z) + + # Add annotation if outliers were detected + # Clear previous annotations + for txt in getattr(self.ax, '_spike_annotations', []): + txt.remove() + self.ax._spike_annotations = [] + + if outlier_times: + # Add text annotation about filtered spikes + outlier_count = len(outlier_times) + max_outlier_mag = max((outlier_x[i]**2 + outlier_y[i]**2 + outlier_z[i]**2)**0.5 + for i in range(len(outlier_times))) + annotation_text = (f"⚠ {outlier_count} acquisition spike(s) filtered\n" + f"(max error: {max_outlier_mag:.0f} ft at t={outlier_times[0]:.1f}s)\n" + f"Spikes excluded from statistics") + txt = self.ax.text(0.02, 0.98, annotation_text, + transform=self.ax.transAxes, + verticalalignment='top', + bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7), + fontsize=9) + self.ax._spike_annotations = [txt] self.ax.relim() self.ax.autoscale_view()