From e5895f116337ccce32538aa9aa1fbb4cbc70b873 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Tue, 30 Sep 2025 15:10:50 +0200 Subject: [PATCH] add NM --- scenario_simulator/gui/gui.py | 381 +++++++++++++++++---- scenario_simulator/utils/config_manager.py | 50 ++- scenario_simulator/utils/radar_math.py | 57 ++- 3 files changed, 407 insertions(+), 81 deletions(-) diff --git a/scenario_simulator/gui/gui.py b/scenario_simulator/gui/gui.py index 68c90cc..55f553d 100644 --- a/scenario_simulator/gui/gui.py +++ b/scenario_simulator/gui/gui.py @@ -25,59 +25,156 @@ from .simulation_manager import SimulationManager from ..utils import logger # --- Helper Dialog for Adding/Editing Targets --- +# Sostituisci l'intera classe AddTargetDialog nel file gui/gui.py con questa + class AddTargetDialog(tk.Toplevel): - """Dialog window for adding or editing target parameters.""" - def __init__(self, parent, target_data=None): + """Dialog window for adding or editing target parameters with dual input modes.""" + def __init__(self, parent, target_data_si=None): super().__init__(parent) - self.title("Add New Target" if target_data is None else "Edit Target") + self.title("Add New Target" if target_data_si is None else "Edit Target") self.transient(parent) self.grab_set() - self.result = None + self.result_si = None - frame = ttk.Frame(self, padding="10") - frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + main_frame = ttk.Frame(self, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) - self.vars = { - "pos_x": tk.DoubleVar(value=target_data["pos_x"] if target_data else radar_math.meters_to_nm(5000.0)), - "pos_y": tk.DoubleVar(value=target_data["pos_y"] if target_data else radar_math.meters_to_nm(0.0)), - "pos_z": tk.DoubleVar(value=target_data["pos_z"] if target_data else radar_math.meters_to_nm(0.0)), - "vel_x": tk.DoubleVar(value=target_data["vel_x"] if target_data else radar_math.mps_to_knots(-150.0)), - "vel_y": tk.DoubleVar(value=target_data["vel_y"] if target_data else radar_math.mps_to_knots(0.0)), - "vel_z": tk.DoubleVar(value=target_data["vel_z"] if target_data else radar_math.mps_to_knots(0.0)), - "rcs": tk.DoubleVar(value=target_data["rcs"] if target_data else 1.0) - } + # --- Mode Selection --- + self.input_mode = tk.StringVar(value="Cartesian") + mode_frame = ttk.Frame(main_frame) + mode_frame.grid(row=0, column=0, columnspan=2, pady=(0, 10), sticky=tk.W) + ttk.Label(mode_frame, text="Position Input Mode:").pack(side=tk.LEFT, padx=(0, 10)) + ttk.Radiobutton(mode_frame, text="Cartesian (X, Y, Z)", variable=self.input_mode, value="Cartesian", command=self._update_input_mode).pack(side=tk.LEFT) + ttk.Radiobutton(mode_frame, text="Spherical (R, Az, El)", variable=self.input_mode, value="Spherical", command=self._update_input_mode).pack(side=tk.LEFT) - labels = ["Initial Position X (NM):", "Initial Position Y (NM):", "Initial Position Z (NM):", - "Velocity X (knots):", "Velocity Y (knots):", "Velocity Z (knots):", "RCS (m^2):"] - keys = ["pos_x", "pos_y", "pos_z", "vel_x", "vel_y", "vel_z", "rcs"] + # --- Data Initialization --- + self._initialize_vars(target_data_si) - for i, (label_text, key) in enumerate(zip(labels, keys)): - ttk.Label(frame, text=label_text).grid(row=i, column=0, sticky=tk.W, pady=2) - spinbox = ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars[key]) - spinbox.grid(row=i, column=1, sticky=(tk.W, tk.E), pady=2) - if key == "rcs": spinbox.config(from_=0.01) + # --- Input Frames --- + self.cartesian_frame = ttk.Frame(main_frame) + self.cartesian_frame.grid(row=1, column=0, columnspan=2, sticky='ew') + self.spherical_frame = ttk.Frame(main_frame) + self.spherical_frame.grid(row=1, column=0, columnspan=2, sticky='ew') + + self._populate_cartesian_frame() + self._populate_spherical_frame() - button_frame = ttk.Frame(frame) - button_frame.grid(row=len(labels), column=0, columnspan=2, pady=10) + # --- Common Velocity and RCS Frame --- + common_frame = ttk.Frame(main_frame) + common_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky='ew') + self._populate_common_frame(common_frame) + + # --- Buttons --- + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=3, 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) + self._update_input_mode() # Set initial visibility + + def _initialize_vars(self, target_data_si): + """Initialize all tk variables from SI data, calculating both coordinate systems.""" + # Default values in SI + pos_x_m, pos_y_m, pos_z_m = 5000.0, 0.0, 0.0 + vel_x_mps, vel_y_mps, vel_z_mps = -150.0, 0.0, 0.0 + rcs = 1.0 + + if target_data_si: + pos_x_m, pos_y_m, pos_z_m = target_data_si["pos_x"], target_data_si["pos_y"], target_data_si["pos_z"] + vel_x_mps, vel_y_mps, vel_z_mps = target_data_si["vel_x"], target_data_si["vel_y"], target_data_si["vel_z"] + rcs = target_data_si["rcs"] + + # Calculate spherical coordinates from SI cartesian + range_m, az_deg, el_deg = radar_math.cartesian_to_spherical(pos_x_m, pos_y_m, pos_z_m) + + self.vars = { + # Display units + "pos_x_nm": tk.DoubleVar(value=radar_math.meters_to_nm(pos_x_m)), + "pos_y_nm": tk.DoubleVar(value=radar_math.meters_to_nm(pos_y_m)), + "pos_z_nm": tk.DoubleVar(value=radar_math.meters_to_nm(pos_z_m)), + "range_nm": tk.DoubleVar(value=radar_math.meters_to_nm(range_m)), + "azimuth_deg": tk.DoubleVar(value=az_deg), + "elevation_deg": tk.DoubleVar(value=el_deg), + "vel_x_knots": tk.DoubleVar(value=radar_math.mps_to_knots(vel_x_mps)), + "vel_y_knots": tk.DoubleVar(value=radar_math.mps_to_knots(vel_y_mps)), + "vel_z_knots": tk.DoubleVar(value=radar_math.mps_to_knots(vel_z_mps)), + "rcs": tk.DoubleVar(value=rcs) + } + + def _create_spinbox(self, parent, label_text, var_key, from_=-1e6, to=1e6): + ttk.Label(parent, text=label_text).pack(side=tk.LEFT) + spinbox = ttk.Spinbox(parent, from_=from_, to=to, textvariable=self.vars[var_key], width=10) + spinbox.pack(side=tk.RIGHT, fill=tk.X, expand=True) + + def _populate_cartesian_frame(self): + labels = ["Position X (NM):", "Position Y (NM):", "Position Z (NM):"] + keys = ["pos_x_nm", "pos_y_nm", "pos_z_nm"] + for label, key in zip(labels, keys): + frame = ttk.Frame(self.cartesian_frame) + frame.pack(fill=tk.X, pady=1) + self._create_spinbox(frame, label, key) + + def _populate_spherical_frame(self): + frame_r = ttk.Frame(self.spherical_frame); frame_r.pack(fill=tk.X, pady=1) + self._create_spinbox(frame_r, "Range (NM):", "range_nm", from_=0.0) + + frame_az = ttk.Frame(self.spherical_frame); frame_az.pack(fill=tk.X, pady=1) + self._create_spinbox(frame_az, "Azimuth (deg):", "azimuth_deg", from_=-180.0, to=180.0) + + frame_el = ttk.Frame(self.spherical_frame); frame_el.pack(fill=tk.X, pady=1) + self._create_spinbox(frame_el, "Elevation (deg):", "elevation_deg", from_=-90.0, to=90.0) + + def _populate_common_frame(self, parent): + ttk.Separator(parent).pack(fill=tk.X, pady=5) + labels = ["Velocity X (knots):", "Velocity Y (knots):", "Velocity Z (knots):", "RCS (m^2):"] + keys = ["vel_x_knots", "vel_y_knots", "vel_z_knots", "rcs"] + for label, key in zip(labels, keys): + frame = ttk.Frame(parent) + frame.pack(fill=tk.X, pady=1) + from_val = 0.01 if key == "rcs" else -1e6 + self._create_spinbox(frame, label, key, from_=from_val) + + def _update_input_mode(self): + """Show the correct frame based on the selected radio button.""" + if self.input_mode.get() == "Cartesian": + self.spherical_frame.grid_remove() + self.cartesian_frame.grid() + else: # Spherical + self.cartesian_frame.grid_remove() + self.spherical_frame.grid() + def on_ok(self): - result_nm_knots = {key: var.get() for key, var in self.vars.items()} - self.result = { - "pos_x": radar_math.nm_to_meters(result_nm_knots["pos_x"]), - "pos_y": radar_math.nm_to_meters(result_nm_knots["pos_y"]), - "pos_z": radar_math.nm_to_meters(result_nm_knots["pos_z"]), - "vel_x": radar_math.knots_to_mps(result_nm_knots["vel_x"]), - "vel_y": radar_math.knots_to_mps(result_nm_knots["vel_y"]), - "vel_z": radar_math.knots_to_mps(result_nm_knots["vel_z"]), - "rcs": result_nm_knots["rcs"] + """Convert data from the active input mode back to SI and store it.""" + # Get common values + vel_x_mps = radar_math.knots_to_mps(self.vars["vel_x_knots"].get()) + vel_y_mps = radar_math.knots_to_mps(self.vars["vel_y_knots"].get()) + vel_z_mps = radar_math.knots_to_mps(self.vars["vel_z_knots"].get()) + rcs = self.vars["rcs"].get() + + # Get position based on the active mode + if self.input_mode.get() == "Cartesian": + pos_x_m = radar_math.nm_to_meters(self.vars["pos_x_nm"].get()) + pos_y_m = radar_math.nm_to_meters(self.vars["pos_y_nm"].get()) + pos_z_m = radar_math.nm_to_meters(self.vars["pos_z_nm"].get()) + else: # Spherical + range_nm = self.vars["range_nm"].get() + azimuth_deg = self.vars["azimuth_deg"].get() + elevation_deg = self.vars["elevation_deg"].get() + + range_m = radar_math.nm_to_meters(range_nm) + pos_x_m, pos_y_m, pos_z_m = radar_math.spherical_to_cartesian(range_m, azimuth_deg, elevation_deg) + + self.result_si = { + "pos_x": pos_x_m, "pos_y": pos_y_m, "pos_z": pos_z_m, + "vel_x": vel_x_mps, "vel_y": vel_y_mps, "vel_z": vel_z_mps, + "rcs": rcs } self.destroy() def show(self): + """Show the dialog and return the result in SI units.""" self.wait_window() - return self.result + return self.result_si # --- Main Application Class --- class App(tk.Tk): @@ -219,6 +316,8 @@ class App(tk.Tk): } self.profiles = config_manager.load_profiles() self.selected_profile = tk.StringVar() + self.scenarios = config_manager.load_scenarios() + self.selected_scenario = tk.StringVar() for key in ["prf", "carrier_frequency", "duty_cycle", "beamwidth_az_deg", "scan_mode", "scan_speed_deg_s", "min_az_deg", "max_az_deg"]: self.vars[key].trace_add("write", self.update_derived_parameters) @@ -290,7 +389,28 @@ class App(tk.Tk): ttk.Label(derived_group, textvariable=self.vars[key]).pack(anchor=tk.W) def _populate_target_tab(self, tab): - target_group = ttk.LabelFrame(tab, text="Target Management", padding=10) + """Populates the Target tab with scenario management and the target table.""" + # --- NUOVO: Scenario Management Group --- + scenario_group = ttk.LabelFrame(tab, text="Scenario Management", padding=10) + scenario_group.pack(fill=tk.X, padx=5, pady=5) + + scenario_frame = ttk.Frame(scenario_group) + scenario_frame.pack(fill=tk.X, pady=2) + + ttk.Label(scenario_frame, text="Scenario:").pack(side=tk.LEFT, padx=(0, 5)) + self.scenario_combobox = ttk.Combobox(scenario_frame, textvariable=self.selected_scenario, state='readonly') + self.scenario_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True) + self.scenario_combobox.bind('<>', self.on_scenario_select) + + btn_frame_scenario = ttk.Frame(scenario_group) + btn_frame_scenario.pack(fill=tk.X, pady=5) + ttk.Button(btn_frame_scenario, text="Save Current...", command=self.save_scenario).pack(side=tk.LEFT, padx=5) + ttk.Button(btn_frame_scenario, text="Delete Selected", command=self.delete_scenario).pack(side=tk.LEFT, padx=5) + + self.refresh_scenario_list() + + # --- Target Management Group (la tabella esistente) --- + target_group = ttk.LabelFrame(tab, text="Target List", padding=10) target_group.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self._create_target_table(target_group) @@ -307,14 +427,16 @@ class App(tk.Tk): self.analysis_text.config(yscrollcommand=analysis_text_scroll.set) def _create_target_table(self, parent): + """Creates and configures the target Treeview table with display units.""" frame = ttk.Frame(parent) frame.pack(fill=tk.BOTH, expand=True) - cols = ("Pos X (m)", "Pos Y (m)", "Pos Z (m)", "Vel X (m/s)", "Vel Y (m/s)", "Vel Z (m/s)", "RCS (m^2)") + # --- MODIFICA: Aggiornamento dei nomi delle colonne --- + cols = ("Pos X (NM)", "Pos Y (NM)", "Pos Z (NM)", "Vel X (knots)", "Vel Y (knots)", "Vel Z (knots)", "RCS (m^2)") self.target_table = ttk.Treeview(frame, columns=cols, show="headings") self.target_table.column("#0", width=0, stretch=tk.NO) for col in cols: self.target_table.heading(col, text=col) - self.target_table.column(col, width=60, anchor=tk.CENTER) + self.target_table.column(col, width=80, anchor=tk.CENTER) # Increased width self.target_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.target_table.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) @@ -414,9 +536,23 @@ class App(tk.Tk): if self.plot_manager: self.plot_manager.update_ppi_range() - def add_target_to_table(self, data): - # data is in meters and m/s from the dialog - self.target_table.insert("", tk.END, values=[f"{v:.2f}" for v in data.values()]) + def add_target_to_table(self, data_si): + """ + Adds a target to the table, converting SI data to display units. + Args: + data_si (dict): Target data in SI units (meters, m/s). + """ + # --- MODIFICA: Conversione da SI a unità di visualizzazione --- + values_to_display = [ + f"{radar_math.meters_to_nm(data_si['pos_x']):.2f}", + f"{radar_math.meters_to_nm(data_si['pos_y']):.2f}", + f"{radar_math.meters_to_nm(data_si['pos_z']):.2f}", + f"{radar_math.mps_to_knots(data_si['vel_x']):.2f}", + f"{radar_math.mps_to_knots(data_si['vel_y']):.2f}", + f"{radar_math.mps_to_knots(data_si['vel_z']):.2f}", + f"{data_si['rcs']:.2f}" + ] + self.target_table.insert("", tk.END, values=values_to_display) self.check_target_warnings() if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) @@ -435,18 +571,30 @@ class App(tk.Tk): return RadarConfig(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(), antenna_config=antenna_cfg, scan_config=scan_cfg) def get_targets_from_gui(self) -> list[Target]: + """ + Extracts target data from the Treeview, converts it from display units back to SI, + and returns a list of Target objects. + """ targets = [] for item in self.target_table.get_children(): - values = self.target_table.item(item)['values'] + values_display_units = self.target_table.item(item)['values'] try: - float_values = [float(v) for v in values] - pos = np.array(float_values[0:3]) - vel = np.array(float_values[3:6]) - rcs = float_values[6] + # --- MODIFICA: Conversione da unità di visualizzazione a SI --- + pos = np.array([ + radar_math.nm_to_meters(float(values_display_units[0])), + radar_math.nm_to_meters(float(values_display_units[1])), + radar_math.nm_to_meters(float(values_display_units[2])) + ]) + vel = np.array([ + radar_math.knots_to_mps(float(values_display_units[3])), + radar_math.knots_to_mps(float(values_display_units[4])), + radar_math.knots_to_mps(float(values_display_units[5])) + ]) + rcs = float(values_display_units[6]) targets.append(Target(initial_position=pos, velocity=vel, rcs=rcs)) except (ValueError, IndexError) as e: - self.log.error(f"Skipping invalid target data: {values}. Error: {e}") - messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values}. Error: {e}", parent=self) + self.log.error(f"Skipping invalid target data from table: {values_display_units}. Error: {e}") + messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values_display_units}. Error: {e}", parent=self) return targets def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi): @@ -570,23 +718,41 @@ class App(tk.Tk): if result: self.add_target_to_table(result) def on_target_double_click(self, event): + """Handles double-click on target table to edit a target.""" selected_item = self.target_table.selection() if not selected_item: return - item_data = self.target_table.item(selected_item[0])['values'] - # Data in table is in m and m/s, convert to NM and knots for dialog - target_data = { - "pos_x": radar_math.meters_to_nm(float(item_data[0])), - "pos_y": radar_math.meters_to_nm(float(item_data[1])), - "pos_z": radar_math.meters_to_nm(float(item_data[2])), - "vel_x": radar_math.mps_to_knots(float(item_data[3])), - "vel_y": radar_math.mps_to_knots(float(item_data[4])), - "vel_z": radar_math.mps_to_knots(float(item_data[5])), - "rcs": float(item_data[6]) - } - dialog = AddTargetDialog(self, target_data=target_data) - result = dialog.show() - if result: - self.target_table.item(selected_item[0], values=[f"{v:.2f}" for v in result.values()]) + item_data_display_units = self.target_table.item(selected_item[0])['values'] + + # --- MODIFICA: Conversione da unità di visualizzazione a SI per il dialogo --- + try: + target_data_si = { + "pos_x": radar_math.nm_to_meters(float(item_data_display_units[0])), + "pos_y": radar_math.nm_to_meters(float(item_data_display_units[1])), + "pos_z": radar_math.nm_to_meters(float(item_data_display_units[2])), + "vel_x": radar_math.knots_to_mps(float(item_data_display_units[3])), + "vel_y": radar_math.knots_to_mps(float(item_data_display_units[4])), + "vel_z": radar_math.knots_to_mps(float(item_data_display_units[5])), + "rcs": float(item_data_display_units[6]) + } + except (ValueError, IndexError): + self.log.error("Could not parse target data from table for editing.") + return + + dialog = AddTargetDialog(self, target_data_si=target_data_si) + result_si = dialog.show() + + if result_si: + # Result is already in SI. Convert to display units to update the table row. + values_to_display = [ + f"{radar_math.meters_to_nm(result_si['pos_x']):.2f}", + f"{radar_math.meters_to_nm(result_si['pos_y']):.2f}", + f"{radar_math.meters_to_nm(result_si['pos_z']):.2f}", + f"{radar_math.mps_to_knots(result_si['vel_x']):.2f}", + f"{radar_math.mps_to_knots(result_si['vel_y']):.2f}", + f"{radar_math.mps_to_knots(result_si['vel_z']):.2f}", + f"{result_si['rcs']:.2f}" + ] + self.target_table.item(selected_item[0], values=values_to_display) self.check_target_warnings() if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) @@ -642,6 +808,95 @@ class App(tk.Tk): if hasattr(self, 'max_az_spinbox'): self.max_az_spinbox.config(state=state) if hasattr(self, 'scan_speed_spinbox'): self.scan_speed_spinbox.config(state=state) self.update_derived_parameters() + + def on_scenario_select(self, event=None): + """Loads the selected target scenario into the target table, converting for display.""" + scenario_name = self.selected_scenario.get() + if not scenario_name or scenario_name not in self.scenarios: return + + self.log.info(f"Loading scenario: '{scenario_name}'") + for item in self.target_table.get_children(): self.target_table.delete(item) + + target_list_si = self.scenarios[scenario_name] + for target_data_si in target_list_si: + # --- MODIFICA: Conversione da SI a unità di visualizzazione --- + values_to_display = [ + f"{radar_math.meters_to_nm(target_data_si['initial_position'][0]):.2f}", + f"{radar_math.meters_to_nm(target_data_si['initial_position'][1]):.2f}", + f"{radar_math.meters_to_nm(target_data_si['initial_position'][2]):.2f}", + f"{radar_math.mps_to_knots(target_data_si['velocity'][0]):.2f}", + f"{radar_math.mps_to_knots(target_data_si['velocity'][1]):.2f}", + f"{radar_math.mps_to_knots(target_data_si['velocity'][2]):.2f}", + f"{target_data_si['rcs']:.2f}" + ] + self.target_table.insert("", tk.END, values=values_to_display) + + self.check_target_warnings() + if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) + messagebox.showinfo("Scenario Loaded", f"Scenario '{scenario_name}' has been loaded.", parent=self) + + def save_scenario(self): + """Saves the current list of targets as a new scenario.""" + scenario_name = simpledialog.askstring("Save Scenario", "Enter a name for this scenario:", parent=self) + if not scenario_name or not scenario_name.strip(): + return + + scenario_name = scenario_name.strip() + if scenario_name in self.scenarios: + if not messagebox.askyesno("Overwrite Scenario", f"Scenario '{scenario_name}' already exists. Overwrite it?", parent=self): + return + + current_targets_data = [] + for item in self.target_table.get_children(): + values = self.target_table.item(item)['values'] + try: + float_values = [float(v) for v in values] + target_dict = { + "initial_position": float_values[0:3], + "velocity": float_values[3:6], + "rcs": float_values[6] + } + current_targets_data.append(target_dict) + except (ValueError, IndexError): + self.log.error(f"Could not parse target data for saving: {values}") + continue # Skip invalid rows + + if not current_targets_data: + messagebox.showwarning("No Targets", "Cannot save an empty scenario.", parent=self) + return + + self.scenarios[scenario_name] = current_targets_data + if config_manager.save_scenarios(self.scenarios): + self.refresh_scenario_list() + self.selected_scenario.set(scenario_name) + self.log.info(f"Scenario '{scenario_name}' saved successfully.") + messagebox.showinfo("Scenario Saved", f"Scenario '{scenario_name}' saved successfully.", parent=self) + else: + self.log.error("Failed to save scenarios to file.") + messagebox.showerror("Error", "Could not save scenarios to file.", parent=self) + + def delete_scenario(self): + """Deletes the selected scenario.""" + scenario_name = self.selected_scenario.get() + if not scenario_name: + messagebox.showwarning("No Scenario Selected", "Please select a scenario to delete.", parent=self) + return + + if messagebox.askyesno("Delete Scenario", f"Are you sure you want to delete the scenario '{scenario_name}'?", parent=self): + if scenario_name in self.scenarios: + del self.scenarios[scenario_name] + if config_manager.save_scenarios(self.scenarios): + self.refresh_scenario_list() + self.log.info(f"Scenario '{scenario_name}' has been deleted.") + messagebox.showinfo("Scenario Deleted", f"Scenario '{scenario_name}' has been deleted.", parent=self) + else: + self.log.error("Failed to save scenarios to file after deletion.") + messagebox.showerror("Error", "Could not save scenarios to file.", parent=self) + + def refresh_scenario_list(self): + """Updates the scenario combobox with current saved scenarios.""" + self.scenario_combobox['values'] = sorted(list(self.scenarios.keys())) + self.selected_scenario.set('') # Clear selection def start_gui(): """Entry point to launch the Tkinter GUI application.""" diff --git a/scenario_simulator/utils/config_manager.py b/scenario_simulator/utils/config_manager.py index f01b2b0..724f682 100644 --- a/scenario_simulator/utils/config_manager.py +++ b/scenario_simulator/utils/config_manager.py @@ -1,12 +1,17 @@ """ -Handles loading and saving of radar configuration profiles from a JSON file. +Handles loading and saving of radar configuration profiles and target scenarios +from JSON files. """ import json import os +import numpy as np -# Use a file in the user's home directory to persist profiles +# Use files in the user's home directory to persist data # across different project locations or versions. PROFILES_FILE = os.path.join(os.path.expanduser("~"), ".radar_simulator_profiles.json") +SCENARIOS_FILE = os.path.join(os.path.expanduser("~"), ".radar_simulator_scenarios.json") + +# --- Profile Management (Radar Configuration) --- def load_profiles(): """ @@ -18,7 +23,6 @@ def load_profiles(): try: with open(PROFILES_FILE, 'r') as f: profiles = json.load(f) - # Basic validation if isinstance(profiles, dict): return profiles return {} @@ -36,3 +40,43 @@ def save_profiles(profiles): return True except IOError: return False + +# --- Scenario Management (Target Lists) - NUOVE FUNZIONI --- + +def load_scenarios(): + """ + Loads target scenarios (lists of targets) from the JSON file. + """ + if not os.path.exists(SCENARIOS_FILE): + return {} + try: + with open(SCENARIOS_FILE, 'r') as f: + scenarios = json.load(f) + # Basic validation to ensure it's a dictionary + if isinstance(scenarios, dict): + return scenarios + return {} + except (IOError, json.JSONDecodeError): + return {} + +def save_scenarios(scenarios): + """ + Saves the given scenarios dictionary to the JSON file. + + The data structure is: + { + "ScenarioName1": [ + {"initial_position": [x,y,z], "velocity": [vx,vy,vz], "rcs": rcs_val}, + ... + ], + ... + } + """ + try: + with open(SCENARIOS_FILE, 'w') as f: + # np.ndarray is not JSON serializable, so convert to lists if needed + # This is handled in the GUI layer before calling this function + json.dump(scenarios, f, indent=4) + return True + except (IOError, TypeError): + return False \ No newline at end of file diff --git a/scenario_simulator/utils/radar_math.py b/scenario_simulator/utils/radar_math.py index b4bbed2..d661ab8 100644 --- a/scenario_simulator/utils/radar_math.py +++ b/scenario_simulator/utils/radar_math.py @@ -4,11 +4,9 @@ Utility functions for radar-related mathematical calculations. import numpy as np from scipy.constants import c -# Conversion Constants +# --- Conversion Constants --- METERS_PER_NAUTICAL_MILE = 1852.0 METERS_PER_SECOND_TO_KNOTS = 1.94384 -DEGREES_TO_RADIANS = np.pi / 180.0 -RADIANS_TO_DEGREES = 180.0 / np.pi def calculate_max_unambiguous_range(prf: float) -> float: """ @@ -85,9 +83,10 @@ def calculate_gaussian_gain(angle_off_boresight_deg: float, beamwidth_deg: float # The standard deviation (sigma) of the Gaussian beam is related to the 3dB beamwidth sigma = beamwidth_deg / (2 * np.sqrt(2 * np.log(2))) - # We use angle^2 / (2 * sigma^2) for the one-way gain pattern. - # This provides a more realistic falloff than a squared (two-way) model. - gain = np.exp(-(angle_off_boresight_deg**2) / (2 * sigma**2)) + # We use angle^2 / (2 * sigma^2) for the one-way power gain pattern. + # The two-way voltage gain is the square of the one-way voltage gain, + # which is equivalent to the one-way power gain. + gain = np.exp(-(angle_off_boresight_deg**2) / (sigma**2)) return gain # --- Unit Conversion Functions --- @@ -108,14 +107,42 @@ def knots_to_mps(knots: float) -> float: """Converts knots to meters per second.""" return knots / METERS_PER_SECOND_TO_KNOTS -def mps_to_kmh(mps: float) -> float: - """Converts meters per second to kilometers per hour.""" - return mps * 3.6 +def cartesian_to_spherical(x: float, y: float, z: float) -> tuple[float, float, float]: + """ + Converts Cartesian coordinates (x, y, z) to Spherical coordinates. -def deg_to_rad(deg: float) -> float: - """Converts degrees to radians.""" - return deg * DEGREES_TO_RADIANS + Args: + x, y, z: Coordinates in meters. -def rad_to_deg(rad: float) -> float: - """Converts radians to degrees.""" - return rad * RADIANS_TO_DEGREES \ No newline at end of file + Returns: + A tuple containing (range_m, azimuth_deg, elevation_deg). + """ + range_m = np.sqrt(x**2 + y**2 + z**2) + if range_m == 0: + return 0.0, 0.0, 0.0 + + azimuth_deg = np.rad2deg(np.arctan2(y, x)) + elevation_deg = np.rad2deg(np.arcsin(z / range_m)) + + return range_m, azimuth_deg, elevation_deg + +def spherical_to_cartesian(range_m: float, azimuth_deg: float, elevation_deg: float) -> list[float]: + """ + Converts Spherical coordinates to Cartesian coordinates (x, y, z). + + Args: + range_m: Range in meters. + azimuth_deg: Azimuth in degrees. + elevation_deg: Elevation in degrees. + + Returns: + A list containing [x, y, z] coordinates in meters. + """ + az_rad = np.deg2rad(azimuth_deg) + el_rad = np.deg2rad(elevation_deg) + + x = range_m * np.cos(el_rad) * np.cos(az_rad) + y = range_m * np.cos(el_rad) * np.sin(az_rad) + z = range_m * np.sin(el_rad) + + return [x, y, z] \ No newline at end of file