""" Main GUI module for the Radar Scenario Simulator (Tkinter version). This file contains the implementation of the main application window using Tkinter, organized with a Notebook widget for clarity and featuring proactive guidance. """ import tkinter as tk from tkinter import ttk, messagebox, simpledialog import numpy as np from scipy.constants import c from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure from ..core.simulation_engine import RadarConfig, Target, generate_iq_data from ..utils import radar_math, config_manager class AddTargetDialog(tk.Toplevel): # ... (omitted for brevity, same as before) def __init__(self, parent): super().__init__(parent) self.title("Add New Target") self.transient(parent) self.grab_set() self.result = None frame = ttk.Frame(self, padding="10") frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.vars = { "pos_x": tk.DoubleVar(value=5000.0), "pos_y": tk.DoubleVar(value=0.0), "pos_z": tk.DoubleVar(value=0.0), "vel_x": tk.DoubleVar(value=-150.0), "vel_y": tk.DoubleVar(value=0.0), "vel_z": tk.DoubleVar(value=0.0), "rcs": tk.DoubleVar(value=1.0) } ttk.Label(frame, text="Initial Position X (m):").grid(row=0, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars["pos_x"]).grid(row=0, column=1, sticky=(tk.W, tk.E), pady=2) ttk.Label(frame, text="Initial Position Y (m):").grid(row=1, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars["pos_y"]).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=2) ttk.Label(frame, text="Initial Position Z (m):").grid(row=2, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars["pos_z"]).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=2) ttk.Label(frame, text="Velocity X (m/s):").grid(row=3, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars["vel_x"]).grid(row=3, column=1, sticky=(tk.W, tk.E), pady=2) ttk.Label(frame, text="Velocity Y (m/s):").grid(row=4, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars["vel_y"]).grid(row=4, column=1, sticky=(tk.W, tk.E), pady=2) ttk.Label(frame, text="Velocity Z (m/s):").grid(row=5, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars["vel_z"]).grid(row=5, column=1, sticky=(tk.W, tk.E), pady=2) ttk.Label(frame, text="RCS (m^2):").grid(row=6, column=0, sticky=tk.W, pady=2) ttk.Spinbox(frame, from_=0.01, to=1e6, textvariable=self.vars["rcs"]).grid(row=6, column=1, sticky=(tk.W, tk.E), pady=2) button_frame = ttk.Frame(frame) button_frame.grid(row=7, column=0, columnspan=2, pady=10) ttk.Button(button_frame, text="OK", command=self.on_ok).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="Cancel", command=self.destroy).pack(side=tk.LEFT, padx=5) def on_ok(self): self.result = {key: var.get() for key, var in self.vars.items()} self.destroy() def show(self): self.wait_window() return self.result class App(tk.Tk): """Main application window.""" def __init__(self): super().__init__() self.title("Radar Scenario Simulator") self.geometry("1200x800") paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL) paned_window.pack(fill=tk.BOTH, expand=True) left_frame = ttk.Frame(paned_window, width=450) paned_window.add(left_frame, weight=1) right_frame = ttk.Frame(paned_window) paned_window.add(right_frame, weight=3) self.vars = { "carrier_frequency": tk.DoubleVar(value=9.5e9), "prf": tk.DoubleVar(value=2000.0), "duty_cycle": tk.DoubleVar(value=10.0), "sample_rate": tk.DoubleVar(value=5e6), "num_pulses": tk.IntVar(value=256), "min_db": tk.DoubleVar(value=-60.0), "max_db": tk.DoubleVar(value=0.0), "auto_scale": tk.BooleanVar(value=True), "pulse_width_text": tk.StringVar(), "listening_time_text": tk.StringVar(), "max_range_text": tk.StringVar(), "max_velocity_text": tk.StringVar() } self.profiles = config_manager.load_profiles() self.selected_profile = tk.StringVar() # Add trace for real-time updates self.vars["prf"].trace_add("write", self.update_derived_parameters) self.vars["carrier_frequency"].trace_add("write", self.update_derived_parameters) self.vars["duty_cycle"].trace_add("write", self.update_derived_parameters) notebook = ttk.Notebook(left_frame) notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) config_tab, target_tab, scenario_tab = ttk.Frame(notebook), ttk.Frame(notebook), ttk.Frame(notebook) notebook.add(config_tab, text="Configurazioni") notebook.add(target_tab, text="Target") notebook.add(scenario_tab, text="Scenario") self._populate_config_tab(config_tab) self._populate_target_tab(target_tab) self._populate_scenario_tab(scenario_tab) plot_group = ttk.LabelFrame(right_frame, text="Range-Doppler Map", padding=10) plot_group.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.figure = Figure(figsize=(5, 4), dpi=100) self.canvas = FigureCanvasTkAgg(self.figure, master=plot_group) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.toggle_amplitude_controls() self.update_derived_parameters() # Initial call def _populate_config_tab(self, tab): profile_group = ttk.LabelFrame(tab, text="Radar Profiles", padding=10) profile_group.pack(fill=tk.X, padx=5, pady=5) profile_frame = ttk.Frame(profile_group) profile_frame.pack(fill=tk.X, pady=2) ttk.Label(profile_frame, text="Profile:").pack(side=tk.LEFT, padx=(0, 5)) self.profile_combobox = ttk.Combobox(profile_frame, textvariable=self.selected_profile, state='readonly') self.profile_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True) self.profile_combobox.bind('<>', self.on_profile_select) btn_frame = ttk.Frame(profile_group) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="Save Current...", command=self.save_profile).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="Delete Selected", command=self.delete_profile).pack(side=tk.LEFT, padx=5) self.refresh_profile_list() radar_group = ttk.LabelFrame(tab, text="Radar Configuration", padding=10) radar_group.pack(fill=tk.X, padx=5, pady=5) self._create_labeled_spinbox(radar_group, "Carrier Frequency (Hz):", self.vars["carrier_frequency"], 1e6, 100e9) self._create_labeled_spinbox(radar_group, "PRF (Hz):", self.vars["prf"], 1, 100000) self._create_labeled_spinbox(radar_group, "Duty Cycle (%):", self.vars["duty_cycle"], 0.1, 50.0) self._create_labeled_spinbox(radar_group, "Sample Rate (Hz):", self.vars["sample_rate"], 1e3, 100e6) derived_group = ttk.LabelFrame(tab, text="Derived & Calculated Values", padding=10) derived_group.pack(fill=tk.X, padx=5, pady=5) self.pulse_width_label = ttk.Label(derived_group, textvariable=self.vars["pulse_width_text"]) self.pulse_width_label.pack(anchor=tk.W) self.listening_time_label = ttk.Label(derived_group, textvariable=self.vars["listening_time_text"]) self.listening_time_label.pack(anchor=tk.W) ttk.Separator(derived_group, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5) self.max_range_label = ttk.Label(derived_group, textvariable=self.vars["max_range_text"]) self.max_range_label.pack(anchor=tk.W) self.max_velocity_label = ttk.Label(derived_group, textvariable=self.vars["max_velocity_text"]) self.max_velocity_label.pack(anchor=tk.W) def _populate_target_tab(self, tab): target_group = ttk.LabelFrame(tab, text="Target Management", padding=10) target_group.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self._create_target_table(target_group) def _populate_scenario_tab(self, tab): sim_group = ttk.LabelFrame(tab, text="Simulation & Plotting Control", padding=10) sim_group.pack(fill=tk.X, padx=5, pady=5) self._create_sim_controls(sim_group) def _create_labeled_spinbox(self, parent, text, var, from_, to, is_db=False): frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=2) ttk.Label(frame, text=text, width=25).pack(side=tk.LEFT) spinbox = ttk.Spinbox(frame, from_=from_, to=to, textvariable=var, format="%.2e") spinbox.pack(side=tk.LEFT, fill=tk.X, expand=True) if is_db: if "min" in text.lower(): self.min_db_spinbox = spinbox else: self.max_db_spinbox = spinbox def _create_target_table(self, parent): frame = ttk.Frame(parent) frame.pack(fill=tk.BOTH, expand=True) cols = ("Pos X", "Pos Y", "Pos Z", "Vel X", "Vel Y", "Vel Z", "RCS") self.target_table = ttk.Treeview(frame, columns=cols, show="headings") for col in cols: self.target_table.heading(col, text=col); self.target_table.column(col, width=50, anchor=tk.CENTER) self.target_table.pack(fill=tk.BOTH, expand=True) btn_frame = ttk.Frame(parent) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="Add Target", command=self.open_add_target_dialog).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="Remove Selected", command=self.remove_selected_target).pack(side=tk.LEFT, padx=5) def _create_sim_controls(self, parent): self._create_labeled_spinbox(parent, "Number of Pulses:", self.vars["num_pulses"], 1, 8192) auto_scale_check = ttk.Checkbutton(parent, text="Auto-Scale Amplitude", variable=self.vars["auto_scale"], command=self.toggle_amplitude_controls) auto_scale_check.pack(fill=tk.X, pady=5) self._create_labeled_spinbox(parent, "Min Display Amplitude (dB):", self.vars["min_db"], -120, 100, is_db=True) self._create_labeled_spinbox(parent, "Max Display Amplitude (dB):", self.vars["max_db"], -120, 100, is_db=True) self.generate_button = ttk.Button(parent, text="Generate Scenario", command=self.run_simulation) self.generate_button.pack(pady=5) def toggle_amplitude_controls(self): state = tk.DISABLED if self.vars["auto_scale"].get() else tk.NORMAL self.min_db_spinbox.config(state=state) self.max_db_spinbox.config(state=state) def update_derived_parameters(self, *args): try: prf = self.vars["prf"].get() carrier_freq = self.vars["carrier_frequency"].get() duty_cycle = self.vars["duty_cycle"].get() pri = 1.0 / prf pulse_width = pri * (duty_cycle / 100.0) listening_time = pri - pulse_width self.vars["pulse_width_text"].set(f"Pulse Width: {pulse_width * 1e6:,.2f} µs") self.vars["listening_time_text"].set(f"Listening Time: {listening_time * 1e6:,.2f} µs") max_range = radar_math.calculate_max_unambiguous_range(prf) max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range:,.0f} m") self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: \u00b1{max_vel:,.1f} m/s") self.check_target_warnings() except (tk.TclError, ValueError, ZeroDivisionError): pass # Ignore errors during startup or when entry is invalid def check_target_warnings(self): try: prf = self.vars["prf"].get() max_range = radar_math.calculate_max_unambiguous_range(prf) max_vel = radar_math.calculate_max_unambiguous_velocity(self.vars["carrier_frequency"].get(), prf) range_warning, vel_warning = False, False for item in self.target_table.get_children(): values = [float(v) for v in self.target_table.item(item)['values']] target_range = np.linalg.norm(values[0:3]) target_vel = np.linalg.norm(values[3:6]) if target_range > max_range: range_warning = True if target_vel > max_vel: vel_warning = True self.max_range_label.config(foreground='orange' if range_warning else 'black') self.max_velocity_label.config(foreground='orange' if vel_warning else 'black') except (tk.TclError, ValueError, ZeroDivisionError): pass def open_add_target_dialog(self): dialog = AddTargetDialog(self) result = dialog.show() if result: self.add_target_to_table(result) def add_target_to_table(self, data): self.target_table.insert("", tk.END, values=[f"{v:.2f}" for v in data.values()]) self.check_target_warnings() def remove_selected_target(self): for i in self.target_table.selection(): self.target_table.delete(i) self.check_target_warnings() def on_profile_select(self, event=None): profile_name = self.selected_profile.get() if profile_name in self.profiles: profile_data = self.profiles[profile_name] self.vars["carrier_frequency"].set(profile_data.get("carrier_frequency", 9.5e9)) self.vars["prf"].set(profile_data.get("prf", 2000.0)) self.vars["duty_cycle"].set(profile_data.get("duty_cycle", 10.0)) self.vars["sample_rate"].set(profile_data.get("sample_rate", 5e6)) messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' has been loaded.", parent=self) def save_profile(self): profile_name = simpledialog.askstring("Save Profile", "Enter a name for this profile:", parent=self) if not profile_name or not profile_name.strip(): return profile_name = profile_name.strip() if profile_name in self.profiles: if not messagebox.askyesno("Overwrite Profile", f"Profile '{profile_name}' already exists. Overwrite it?", parent=self): return current_config = { "carrier_frequency": self.vars["carrier_frequency"].get(), "prf": self.vars["prf"].get(), "duty_cycle": self.vars["duty_cycle"].get(), "sample_rate": self.vars["sample_rate"].get(), } self.profiles[profile_name] = current_config if config_manager.save_profiles(self.profiles): self.refresh_profile_list() self.selected_profile.set(profile_name) messagebox.showinfo("Profile Saved", f"Profile '{profile_name}' saved successfully.", parent=self) else: messagebox.showerror("Error", "Could not save profiles to file.", parent=self) def delete_profile(self): profile_name = self.selected_profile.get() if not profile_name: messagebox.showwarning("No Profile Selected", "Please select a profile to delete.", parent=self) return if messagebox.askyesno("Delete Profile", f"Are you sure you want to delete the profile '{profile_name}'?", parent=self): if profile_name in self.profiles: del self.profiles[profile_name] if config_manager.save_profiles(self.profiles): self.refresh_profile_list() messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' has been deleted.", parent=self) else: messagebox.showerror("Error", "Could not save profiles to file.", parent=self) def refresh_profile_list(self): self.profile_combobox['values'] = sorted(list(self.profiles.keys())) self.selected_profile.set('') def run_simulation(self): self.generate_button.config(text="Generating...", state=tk.DISABLED) self.update_idletasks() try: prf = self.vars["prf"].get() duty_cycle = self.vars["duty_cycle"].get() pulse_width = (1.0 / prf) * (duty_cycle / 100.0) config = RadarConfig(carrier_frequency=self.vars["carrier_frequency"].get(), prf=prf, pulse_width=pulse_width, sample_rate=self.vars["sample_rate"].get()) targets = [] for item in self.target_table.get_children(): values = self.target_table.item(item)['values'] try: float_values = [float(v) for v in values] pos, vel, rcs = np.array(float_values[0:3]), np.array(float_values[3:6]), float_values[6] targets.append(Target(initial_position=pos, velocity=vel, rcs=rcs)) except (ValueError, IndexError) as e: messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values}. Error: {e}") num_pulses = self.vars["num_pulses"].get() iq_data = generate_iq_data(config, targets, num_pulses) self.plot_range_doppler(iq_data, config) except Exception as e: messagebox.showerror("Simulation Error", f"An error occurred: {e}") finally: self.generate_button.config(text="Generate Scenario", state=tk.NORMAL) def plot_range_doppler(self, iq_data, config): self.figure.clear(); self.ax = self.figure.add_subplot(111) if iq_data.size == 0: self.ax.text(0.5, 0.5, "No data to display", ha='center', va='center'); self.canvas.draw(); return window = np.hanning(iq_data.shape[0])[:, np.newaxis] iq_data_windowed = iq_data * window range_doppler_map = np.fft.fftshift(np.fft.fft(iq_data_windowed, axis=0), axes=0) range_doppler_map = np.fft.fftshift(np.fft.fft(range_doppler_map, axis=1), axes=1) epsilon = 1e-10 range_doppler_map_db = 20 * np.log10(np.abs(range_doppler_map) + epsilon) vmin, vmax = None, None if self.vars["auto_scale"].get(): if np.any(np.isfinite(range_doppler_map_db)): vmin = np.nanmin(range_doppler_map_db[np.isfinite(range_doppler_map_db)]); vmax = np.nanmax(range_doppler_map_db) self.vars["min_db"].set(round(vmin, 2)); self.vars["max_db"].set(round(vmax, 2)) else: vmin, vmax = -100, 0 else: vmin, vmax = self.vars["min_db"].get(), self.vars["max_db"].get() doppler_freq_axis = np.fft.fftshift(np.fft.fftfreq(iq_data.shape[0], d=1.0/config.prf)) velocity_axis = doppler_freq_axis * (c / config.carrier_frequency) / 2 range_axis_samples = iq_data.shape[1] range_axis_m = np.arange(range_axis_samples) * c / (2 * config.sample_rate) im = self.ax.imshow(range_doppler_map_db, aspect='auto', extent=[range_axis_m[0], range_axis_m[-1], velocity_axis[0], velocity_axis[-1]], cmap='jet', vmin=vmin, vmax=vmax) self.ax.set_title('Range-Doppler Map'); self.ax.set_xlabel('Range (m)'); self.ax.set_ylabel('Velocity (m/s)') self.figure.colorbar(im, ax=self.ax, label='Amplitude (dB)') self.canvas.draw() def start_gui(): app = App() app.mainloop()