This commit is contained in:
VALLONGOL 2025-09-30 15:10:50 +02:00
parent 07bbfdddc0
commit e5895f1163
3 changed files with 407 additions and 81 deletions

View File

@ -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('<<ComboboxSelected>>', 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."""

View File

@ -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

View File

@ -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
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]