fix minor gui bug

This commit is contained in:
VALLONGOL 2025-09-30 13:06:27 +02:00
parent 0222b0e377
commit 07bbfdddc0
3 changed files with 177 additions and 64 deletions

View File

@ -38,17 +38,17 @@ class AddTargetDialog(tk.Toplevel):
frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.vars = { self.vars = {
"pos_x": tk.DoubleVar(value=target_data["pos_x"] if target_data else 5000.0), "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 0.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 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 -150.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 0.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 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) "rcs": tk.DoubleVar(value=target_data["rcs"] if target_data else 1.0)
} }
labels = ["Initial Position X (m):", "Initial Position Y (m):", "Initial Position Z (m):", labels = ["Initial Position X (NM):", "Initial Position Y (NM):", "Initial Position Z (NM):",
"Velocity X (m/s):", "Velocity Y (m/s):", "Velocity Z (m/s):", "RCS (m^2):"] "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"] keys = ["pos_x", "pos_y", "pos_z", "vel_x", "vel_y", "vel_z", "rcs"]
for i, (label_text, key) in enumerate(zip(labels, keys)): for i, (label_text, key) in enumerate(zip(labels, keys)):
@ -63,7 +63,16 @@ class AddTargetDialog(tk.Toplevel):
ttk.Button(button_frame, text="Cancel", command=self.destroy).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): def on_ok(self):
self.result = {key: var.get() for key, var in self.vars.items()} 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"]
}
self.destroy() self.destroy()
def show(self): def show(self):
@ -76,8 +85,7 @@ class App(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("Radar Scenario Simulator") self.title("Radar Scenario Simulator")
self.attributes('-fullscreen', True) self.state('zoomed') # Maximize the window without borderless fullscreen
self.bind("<Escape>", lambda event: self.attributes('-fullscreen', False))
# --- Main Layout --- # --- Main Layout ---
main_paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL) main_paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
@ -147,10 +155,11 @@ class App(tk.Tk):
self.ppi_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.ppi_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# --- Initialize Managers --- # --- Initialize Managers ---
self.plot_manager = PlotManager(self.rd_figure, self.rd_canvas, self.ppi_figure, self.ppi_canvas, self.vars['ppi_range_m']) self.plot_manager = PlotManager(self.rd_figure, self.rd_canvas, self.ppi_figure, self.ppi_canvas, self.vars['ppi_range_nm'])
self.simulation_manager = SimulationManager() self.simulation_manager = SimulationManager()
# --- Finalize Init --- # --- Finalize Init ---
self.plot_manager.update_ppi_range() # Set initial PPI range
self.toggle_amplitude_controls() self.toggle_amplitude_controls()
self.update_derived_parameters() self.update_derived_parameters()
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
@ -204,12 +213,13 @@ class App(tk.Tk):
"max_velocity_text": tk.StringVar(), "max_velocity_text": tk.StringVar(),
"dwell_time_text": tk.StringVar(), "dwell_time_text": tk.StringVar(),
"pulses_on_target_text": tk.StringVar(), "pulses_on_target_text": tk.StringVar(),
"ppi_range_m": tk.DoubleVar(value=15000.0), "max_range_nm": tk.DoubleVar(value=100.0), # New
"ppi_range_nm": tk.DoubleVar(value=100.0), # Renamed
"scan_info_text": tk.StringVar(value="Mode: Staring") "scan_info_text": tk.StringVar(value="Mode: Staring")
} }
self.profiles = config_manager.load_profiles() self.profiles = config_manager.load_profiles()
self.selected_profile = tk.StringVar() self.selected_profile = tk.StringVar()
for key in ["prf", "carrier_frequency", "duty_cycle", "beamwidth_az_deg", "scan_mode", "scan_speed_deg_s"]: 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) self.vars[key].trace_add("write", self.update_derived_parameters)
def _create_labeled_spinbox(self, parent, text, var, from_, to, increment=1.0, is_db=False, scientific=False, command=None): def _create_labeled_spinbox(self, parent, text, var, from_, to, increment=1.0, is_db=False, scientific=False, command=None):
@ -247,6 +257,7 @@ class App(tk.Tk):
self._create_labeled_spinbox(radar_group, "PRF (Hz):", self.vars["prf"], 1, 100000) 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.01, 99.99, increment=0.1) self._create_labeled_spinbox(radar_group, "Duty Cycle (%):", self.vars["duty_cycle"], 0.01, 99.99, increment=0.1)
self._create_labeled_spinbox(radar_group, "Sample Rate (Hz):", self.vars["sample_rate"], 1e3, 100e6, scientific=True) self._create_labeled_spinbox(radar_group, "Sample Rate (Hz):", self.vars["sample_rate"], 1e3, 100e6, scientific=True)
self._create_labeled_spinbox(radar_group, "Max Range (NM):", self.vars["max_range_nm"], 10, 1000, increment=10, command=self._on_max_range_config_change)
antenna_group = ttk.LabelFrame(tab, text="Antenna Configuration", padding=10) antenna_group = ttk.LabelFrame(tab, text="Antenna Configuration", padding=10)
antenna_group.pack(fill=tk.X, padx=5, pady=5) antenna_group.pack(fill=tk.X, padx=5, pady=5)
@ -298,7 +309,7 @@ class App(tk.Tk):
def _create_target_table(self, parent): def _create_target_table(self, parent):
frame = ttk.Frame(parent) frame = ttk.Frame(parent)
frame.pack(fill=tk.BOTH, expand=True) frame.pack(fill=tk.BOTH, expand=True)
cols = ("Pos X", "Pos Y", "Pos Z", "Vel X", "Vel Y", "Vel Z", "RCS") 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)")
self.target_table = ttk.Treeview(frame, columns=cols, show="headings") self.target_table = ttk.Treeview(frame, columns=cols, show="headings")
self.target_table.column("#0", width=0, stretch=tk.NO) self.target_table.column("#0", width=0, stretch=tk.NO)
for col in cols: for col in cols:
@ -327,9 +338,28 @@ class App(tk.Tk):
self.stop_button.pack(pady=5) self.stop_button.pack(pady=5)
def _populate_scan_ppi_controls(self, parent): def _populate_scan_ppi_controls(self, parent):
self._create_labeled_spinbox(parent, "PPI Range (m):", self.vars['ppi_range_m'], 100, 150000, command=self.on_ppi_range_change) frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=2)
ttk.Label(frame, text="PPI Range (NM):", width=25).pack(side=tk.LEFT)
self.ppi_range_combobox = ttk.Combobox(frame, textvariable=self.vars['ppi_range_nm'], state='readonly')
self.ppi_range_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.ppi_range_combobox.bind('<<ComboboxSelected>>', self.on_ppi_range_change)
self._update_ppi_range_options()
self.vars["max_range_nm"].trace_add("write", self._update_ppi_range_options)
ttk.Label(parent, textvariable=self.vars['scan_info_text'], wraplength=180, justify=tk.LEFT).pack(anchor=tk.W, padx=5, pady=5) ttk.Label(parent, textvariable=self.vars['scan_info_text'], wraplength=180, justify=tk.LEFT).pack(anchor=tk.W, padx=5, pady=5)
def _update_ppi_range_options(self, *args):
max_range = self.vars["max_range_nm"].get()
steps = list(range(int(max_range), 19, -20))
if 10 not in steps:
steps.append(10)
steps.sort(reverse=True)
self.ppi_range_combobox['values'] = steps
if self.vars['ppi_range_nm'].get() not in steps:
self.vars['ppi_range_nm'].set(steps[0] if steps else 10)
def start_simulation(self): def start_simulation(self):
if self.simulation_manager.is_running(): if self.simulation_manager.is_running():
return return
@ -385,6 +415,7 @@ class App(tk.Tk):
self.plot_manager.update_ppi_range() self.plot_manager.update_ppi_range()
def add_target_to_table(self, data): 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()]) self.target_table.insert("", tk.END, values=[f"{v:.2f}" for v in data.values()])
self.check_target_warnings() self.check_target_warnings()
if self.plot_manager: if self.plot_manager:
@ -420,11 +451,11 @@ class App(tk.Tk):
def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi): def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi):
report = [] report = []
max_range = radar_math.calculate_max_unambiguous_range(radar_cfg.prf) max_range_m = radar_math.calculate_max_unambiguous_range(radar_cfg.prf)
max_vel = radar_math.calculate_max_unambiguous_velocity(radar_cfg.carrier_frequency, radar_cfg.prf) max_vel_mps = radar_math.calculate_max_unambiguous_velocity(radar_cfg.carrier_frequency, radar_cfg.prf)
report.append("--- Radar Configuration Analysis ---") report.append("--- Radar Configuration Analysis ---")
report.append(f"Max Unambiguous Range: {max_range:,.0f} m") report.append(f"Max Unambiguous Range: {radar_math.meters_to_nm(max_range_m):,.1f} NM ({max_range_m:,.0f} m)")
report.append(f"Max Unambiguous Velocity: +/-{max_vel:,.1f} m/s") report.append(f"Max Unambiguous Velocity: +/-{radar_math.mps_to_knots(max_vel_mps):,.1f} knots ({max_vel_mps:,.1f} m/s)")
report.append(f"Current Antenna Azimuth: {current_az_deg:,.1f} deg") report.append(f"Current Antenna Azimuth: {current_az_deg:,.1f} deg")
if radar_cfg.scan_config.mode != 'staring' and radar_cfg.scan_config.scan_speed_deg_s > 0: if radar_cfg.scan_config.mode != 'staring' and radar_cfg.scan_config.scan_speed_deg_s > 0:
dwell_time = radar_math.calculate_dwell_time(radar_cfg.antenna_config.beamwidth_az_deg, radar_cfg.scan_config.scan_speed_deg_s) dwell_time = radar_math.calculate_dwell_time(radar_cfg.antenna_config.beamwidth_az_deg, radar_cfg.scan_config.scan_speed_deg_s)
@ -463,17 +494,19 @@ class App(tk.Tk):
self.vars["pulse_width_text"].set("Pulse Width: N/A (PRF is zero)") self.vars["pulse_width_text"].set("Pulse Width: N/A (PRF is zero)")
self.vars["listening_time_text"].set("Listening Window: N/A (PRF is zero)") self.vars["listening_time_text"].set("Listening Window: N/A (PRF is zero)")
max_range = radar_math.calculate_max_unambiguous_range(prf) max_range_m = radar_math.calculate_max_unambiguous_range(prf)
if np.isinf(max_range): if np.isinf(max_range_m):
self.vars["max_range_text"].set("Max Unambiguous Range: Infinite (PRF is zero)") self.vars["max_range_text"].set("Max Unambiguous Range: Infinite (PRF is zero)")
else: else:
self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range:,.0f} m") max_range_nm = radar_math.meters_to_nm(max_range_m)
self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range_nm:,.1f} NM ({max_range_m:,.0f} m)")
max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) max_vel_mps = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf)
if np.isinf(max_vel): if np.isinf(max_vel_mps):
self.vars["max_velocity_text"].set("Max Unambiguous Velocity: Infinite (carrier freq is zero)") self.vars["max_velocity_text"].set("Max Unambiguous Velocity: Infinite (carrier freq is zero)")
else: else:
self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: +/-{max_vel:,.1f} m/s") max_vel_knots = radar_math.mps_to_knots(max_vel_mps)
self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: +/-{max_vel_knots:,.1f} knots ({max_vel_mps:,.1f} m/s)")
if scan_mode == 'staring' or scan_speed <= 0: if scan_mode == 'staring' or scan_speed <= 0:
self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)") self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)")
@ -490,8 +523,18 @@ class App(tk.Tk):
self.check_target_warnings() self.check_target_warnings()
self._update_scan_info_text() self._update_scan_info_text()
except (tk.TclError, ValueError) as e: # Ensure plot_manager exists before trying to update plots, as this can be called during init
self.log.error(f"Error updating derived parameters: {e}") if hasattr(self, 'plot_manager') and self.plot_manager:
self.plot_manager.update_sector_lines(self.get_radar_config_from_gui())
except (tk.TclError, ValueError):
# This can happen when a user is typing in a spinbox and the value is
# temporarily invalid (e.g., empty or just '-'). Ignore and wait for a valid value.
pass
def _on_max_range_config_change(self, *args):
self._update_ppi_range_options()
new_max_range = self.vars["max_range_nm"].get()
self.vars["ppi_range_nm"].set(new_max_range)
def _update_scan_info_text(self): def _update_scan_info_text(self):
mode = self.vars['scan_mode'].get() mode = self.vars['scan_mode'].get()
@ -499,22 +542,22 @@ class App(tk.Tk):
self.vars['scan_info_text'].set("Mode: Staring") self.vars['scan_info_text'].set("Mode: Staring")
else: else:
min_az, max_az, speed = self.vars['min_az_deg'].get(), self.vars['max_az_deg'].get(), self.vars['scan_speed_deg_s'].get() min_az, max_az, speed = self.vars['min_az_deg'].get(), self.vars['max_az_deg'].get(), self.vars['scan_speed_deg_s'].get()
self.vars['scan_info_text'].set(f"Mode: Sector Scan\nAz: [{min_az}°, {max_az}°] @ {speed}°/s") self.vars['scan_info_text'].set(f"Mode: Sector Scan\nAz: [{min_az}° , {max_az}°] @ {speed}°/s")
def check_target_warnings(self): def check_target_warnings(self):
if not hasattr(self, 'target_table'): return if not hasattr(self, 'target_table'): return
try: try:
prf, carrier_freq = self.vars["prf"].get(), self.vars["carrier_frequency"].get() prf, carrier_freq = self.vars["prf"].get(), self.vars["carrier_frequency"].get()
max_range = radar_math.calculate_max_unambiguous_range(prf) max_range_m = radar_math.calculate_max_unambiguous_range(prf)
max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) max_vel_mps = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf)
range_warning, vel_warning = False, False range_warning, vel_warning = False, False
for item in self.target_table.get_children(): for item in self.target_table.get_children():
values = [float(v) for v in self.target_table.item(item)['values']] values = [float(v) for v in self.target_table.item(item)['values']]
target_initial_range = np.linalg.norm(values[0:3]) target_initial_range_m = np.linalg.norm(values[0:3])
if target_initial_range > 0: target_radial_vel = np.dot(values[3:6], values[0:3] / target_initial_range) if target_initial_range_m > 0: target_radial_vel_mps = np.dot(values[3:6], values[0:3] / target_initial_range_m)
else: target_radial_vel = 0 else: target_radial_vel_mps = 0
if not np.isinf(max_range) and target_initial_range > max_range: range_warning = True if not np.isinf(max_range_m) and target_initial_range_m > max_range_m: range_warning = True
if not np.isinf(max_vel) and abs(target_radial_vel) > max_vel: vel_warning = True if not np.isinf(max_vel_mps) and abs(target_radial_vel_mps) > max_vel_mps: vel_warning = True
self.max_range_label.config(foreground='orange' if range_warning else 'black') self.max_range_label.config(foreground='orange' if range_warning else 'black')
self.max_velocity_label.config(foreground='orange' if vel_warning else 'black') self.max_velocity_label.config(foreground='orange' if vel_warning else 'black')
except (tk.TclError, ValueError, ZeroDivisionError): except (tk.TclError, ValueError, ZeroDivisionError):
@ -530,7 +573,16 @@ class App(tk.Tk):
selected_item = self.target_table.selection() selected_item = self.target_table.selection()
if not selected_item: return if not selected_item: return
item_data = self.target_table.item(selected_item[0])['values'] item_data = self.target_table.item(selected_item[0])['values']
target_data = { "pos_x": float(item_data[0]), "pos_y": float(item_data[1]), "pos_z": float(item_data[2]), "vel_x": float(item_data[3]), "vel_y": float(item_data[4]), "vel_z": float(item_data[5]), "rcs": float(item_data[6]) } # 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) dialog = AddTargetDialog(self, target_data=target_data)
result = dialog.show() result = dialog.show()
if result: if result:

View File

@ -10,12 +10,12 @@ from ..utils import radar_math
class PlotManager: class PlotManager:
"""Handles the state and drawing of matplotlib plots.""" """Handles the state and drawing of matplotlib plots."""
def __init__(self, rd_figure, rd_canvas, ppi_figure, ppi_canvas, ppi_range_var): def __init__(self, rd_figure, rd_canvas, ppi_figure, ppi_canvas, ppi_range_nm_var):
self.rd_figure = rd_figure self.rd_figure = rd_figure
self.rd_canvas = rd_canvas self.rd_canvas = rd_canvas
self.ppi_figure = ppi_figure self.ppi_figure = ppi_figure
self.ppi_canvas = ppi_canvas self.ppi_canvas = ppi_canvas
self.ppi_range_var = ppi_range_var self.ppi_range_nm_var = ppi_range_nm_var
# Artists that will be updated # Artists that will be updated
self.ax_rd = None self.ax_rd = None
@ -25,6 +25,8 @@ class PlotManager:
self.ppi_targets_plot = [] self.ppi_targets_plot = []
self.beam_patch = None self.beam_patch = None
self.beam_line = None self.beam_line = None
self.sector_line_min = None
self.sector_line_max = None
self._init_base_plots() self._init_base_plots()
@ -47,8 +49,11 @@ class PlotManager:
self.ax_ppi.grid(color='gray', linestyle='--') self.ax_ppi.grid(color='gray', linestyle='--')
self.ppi_figure.tight_layout() self.ppi_figure.tight_layout()
if hasattr(self, 'ppi_targets_plot'): # Artists were removed by ppi_figure.clear(). Just clear the reference lists.
self.ppi_targets_plot.clear() self.ppi_targets_plot.clear()
self.sector_line_min = None
self.sector_line_max = None
self.ppi_canvas.draw() self.ppi_canvas.draw()
def clear_rd_plot(self): def clear_rd_plot(self):
@ -56,8 +61,8 @@ class PlotManager:
self.rd_figure.clear() self.rd_figure.clear()
self.ax_rd = self.rd_figure.add_subplot(111) self.ax_rd = self.rd_figure.add_subplot(111)
self.ax_rd.set_title('Range-Doppler Map', color='white', fontsize=10) self.ax_rd.set_title('Range-Doppler Map', color='white', fontsize=10)
self.ax_rd.set_xlabel('Range (m)', color='white', fontsize=8) self.ax_rd.set_xlabel('Range (NM)', color='white', fontsize=8)
self.ax_rd.set_ylabel('Velocity (m/s)', color='white', fontsize=8) self.ax_rd.set_ylabel('Velocity (knots)', color='white', fontsize=8)
self.ax_rd.tick_params(axis='x', colors='white', labelsize=8) self.ax_rd.tick_params(axis='x', colors='white', labelsize=8)
self.ax_rd.tick_params(axis='y', colors='white', labelsize=8) self.ax_rd.tick_params(axis='y', colors='white', labelsize=8)
self.ax_rd.set_facecolor('#3a3a3a') self.ax_rd.set_facecolor('#3a3a3a')
@ -76,21 +81,39 @@ class PlotManager:
self.init_ppi_plot() self.init_ppi_plot()
self.redraw_ppi_targets(targets) self.redraw_ppi_targets(targets)
max_target_range = max(np.linalg.norm(t.initial_position) for t in targets) max_plot_range_nm = self.ppi_range_nm_var.get()
max_unamb_range = radar_math.calculate_max_unambiguous_range(radar_cfg.prf) self.ax_ppi.set_ylim(0, max_plot_range_nm)
max_plot_range = max(max_unamb_range, max_target_range) * 1.2
self.ax_ppi.set_ylim(0, max_plot_range)
self.ppi_range_var.set(max_plot_range)
beamwidth_rad = np.deg2rad(radar_cfg.antenna_config.beamwidth_az_deg)
self.beam_patch = self.ax_ppi.fill_between( self.beam_patch = self.ax_ppi.fill_between(
np.radians([-0.5 * radar_cfg.antenna_config.beamwidth_az_deg, 0.5 * radar_cfg.antenna_config.beamwidth_az_deg]), [-0.5 * beamwidth_rad, 0.5 * beamwidth_rad],
0, max_plot_range, color='cyan', alpha=0.2, linewidth=0 0, max_plot_range_nm, color='cyan', alpha=0.2, linewidth=0
) )
self.beam_line, = self.ax_ppi.plot([0, 0], [0, max_plot_range], color='cyan', linewidth=2) self.beam_line, = self.ax_ppi.plot([0, 0], [0, max_plot_range_nm], color='cyan', linewidth=2)
self.update_sector_lines(radar_cfg)
self.ppi_canvas.draw() self.ppi_canvas.draw()
self.rd_canvas.draw() self.rd_canvas.draw()
def update_sector_lines(self, radar_cfg):
"""Draws or removes the sector scan limit lines on the PPI plot."""
if self.sector_line_min:
self.sector_line_min.remove()
self.sector_line_min = None
if self.sector_line_max:
self.sector_line_max.remove()
self.sector_line_max = None
if radar_cfg.scan_config.mode == 'sector':
max_plot_range_nm = self.ax_ppi.get_ylim()[1]
min_az_rad = np.deg2rad(radar_cfg.scan_config.min_az_deg)
max_az_rad = np.deg2rad(radar_cfg.scan_config.max_az_deg)
self.sector_line_min, = self.ax_ppi.plot([min_az_rad, min_az_rad], [0, max_plot_range_nm], color='red', linestyle='--')
self.sector_line_max, = self.ax_ppi.plot([max_az_rad, max_az_rad], [0, max_plot_range_nm], color='red', linestyle='--')
self.ppi_canvas.draw_idle()
def update_plots(self, frame_data, radar_cfg, auto_scale, min_db, max_db): def update_plots(self, frame_data, radar_cfg, auto_scale, min_db, max_db):
"""Update plots with new frame data from the simulation.""" """Update plots with new frame data from the simulation."""
current_az_deg, iq_data_cpi, frame_num = frame_data current_az_deg, iq_data_cpi, frame_num = frame_data
@ -119,7 +142,6 @@ class PlotManager:
self.im_rd.set_data(range_doppler_map_db) self.im_rd.set_data(range_doppler_map_db)
self.im_rd.set_clim(vmin=vmin, vmax=vmax) self.im_rd.set_clim(vmin=vmin, vmax=vmax)
# Re-create colorbar to prevent state issues
if self.cbar_rd: if self.cbar_rd:
self.cbar_rd.remove() self.cbar_rd.remove()
self.cbar_rd = self.rd_figure.colorbar(self.im_rd, ax=self.ax_rd) self.cbar_rd = self.rd_figure.colorbar(self.im_rd, ax=self.ax_rd)
@ -127,10 +149,12 @@ class PlotManager:
self.cbar_rd.set_label('Amplitude (dB)', color='white', fontsize=8) self.cbar_rd.set_label('Amplitude (dB)', color='white', fontsize=8)
doppler_freq_axis = np.fft.fftshift(np.fft.fftfreq(iq_data_cpi.shape[0], d=1.0/radar_cfg.prf)) doppler_freq_axis = np.fft.fftshift(np.fft.fftfreq(iq_data_cpi.shape[0], d=1.0/radar_cfg.prf))
velocity_axis = doppler_freq_axis * (c / radar_cfg.carrier_frequency) / 2 velocity_axis_mps = doppler_freq_axis * (c / radar_cfg.carrier_frequency) / 2
velocity_axis_knots = radar_math.mps_to_knots(velocity_axis_mps)
range_axis_samples = iq_data_cpi.shape[1] range_axis_samples = iq_data_cpi.shape[1]
range_axis_m = np.arange(range_axis_samples) * c / (2 * radar_cfg.sample_rate) range_axis_m = np.arange(range_axis_samples) * c / (2 * radar_cfg.sample_rate)
self.im_rd.set_extent([range_axis_m[0], range_axis_m[-1], velocity_axis[0], velocity_axis[-1]]) range_axis_nm = radar_math.meters_to_nm(range_axis_m)
self.im_rd.set_extent([range_axis_nm[0], range_axis_nm[-1], velocity_axis_knots[0], velocity_axis_knots[-1]])
self.ax_rd.set_title(f'RD Map (Frame {frame_num})', color='white', fontsize=10) self.ax_rd.set_title(f'RD Map (Frame {frame_num})', color='white', fontsize=10)
# --- Update PPI Plot --- # --- Update PPI Plot ---
@ -138,9 +162,9 @@ class PlotManager:
beamwidth_rad = np.deg2rad(radar_cfg.antenna_config.beamwidth_az_deg) beamwidth_rad = np.deg2rad(radar_cfg.antenna_config.beamwidth_az_deg)
theta_beam = np.linspace(current_az_rad - beamwidth_rad / 2, current_az_rad + beamwidth_rad / 2, 50) theta_beam = np.linspace(current_az_rad - beamwidth_rad / 2, current_az_rad + beamwidth_rad / 2, 50)
max_plot_range = self.ax_ppi.get_ylim()[1] max_plot_range_nm = self.ax_ppi.get_ylim()[1]
self.beam_patch.remove() self.beam_patch.remove()
self.beam_patch = self.ax_ppi.fill_between(theta_beam, 0, max_plot_range, color='cyan', alpha=0.2, linewidth=0) self.beam_patch = self.ax_ppi.fill_between(theta_beam, 0, max_plot_range_nm, color='cyan', alpha=0.2, linewidth=0)
self.beam_line.set_xdata([current_az_rad, current_az_rad]) self.beam_line.set_xdata([current_az_rad, current_az_rad])
self.rd_canvas.draw_idle() self.rd_canvas.draw_idle()
@ -157,10 +181,11 @@ class PlotManager:
for target in targets: for target in targets:
try: try:
x, y = target.initial_position[0], target.initial_position[1] x_m, y_m = target.initial_position[0], target.initial_position[1]
r = np.linalg.norm([x, y]) r_m = np.linalg.norm([x_m, y_m])
theta = np.arctan2(y, x) r_nm = radar_math.meters_to_nm(r_m)
target_plot, = self.ax_ppi.plot(theta, r, 'o', color='red', markersize=6) theta = np.arctan2(y_m, x_m)
target_plot, = self.ax_ppi.plot(theta, r_nm, 'o', color='red', markersize=6)
self.ppi_targets_plot.append(target_plot) self.ppi_targets_plot.append(target_plot)
except (ValueError, IndexError): except (ValueError, IndexError):
continue continue
@ -170,9 +195,9 @@ class PlotManager:
"""Callback to update the PPI plot's range limit from the tk var.""" """Callback to update the PPI plot's range limit from the tk var."""
if not hasattr(self, 'ax_ppi'): return if not hasattr(self, 'ax_ppi'): return
try: try:
new_range = self.ppi_range_var.get() new_range_nm = self.ppi_range_nm_var.get()
if new_range > 0: if new_range_nm > 0:
self.ax_ppi.set_ylim(0, new_range) self.ax_ppi.set_ylim(0, new_range_nm)
self.ppi_canvas.draw() self.ppi_canvas.draw()
except Exception: except Exception:
pass # Ignore errors if entry is invalid pass # Ignore errors if entry is invalid

View File

@ -4,6 +4,12 @@ Utility functions for radar-related mathematical calculations.
import numpy as np import numpy as np
from scipy.constants import c from scipy.constants import c
# 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: def calculate_max_unambiguous_range(prf: float) -> float:
""" """
Calculates the maximum unambiguous range for a given PRF. Calculates the maximum unambiguous range for a given PRF.
@ -82,4 +88,34 @@ def calculate_gaussian_gain(angle_off_boresight_deg: float, beamwidth_deg: float
# We use angle^2 / (2 * sigma^2) for the one-way gain pattern. # 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. # This provides a more realistic falloff than a squared (two-way) model.
gain = np.exp(-(angle_off_boresight_deg**2) / (2 * sigma**2)) gain = np.exp(-(angle_off_boresight_deg**2) / (2 * sigma**2))
return gain return gain
# --- Unit Conversion Functions ---
def meters_to_nm(meters: float) -> float:
"""Converts meters to nautical miles."""
return meters / METERS_PER_NAUTICAL_MILE
def nm_to_meters(nm: float) -> float:
"""Converts nautical miles to meters."""
return nm * METERS_PER_NAUTICAL_MILE
def mps_to_knots(mps: float) -> float:
"""Converts meters per second to knots."""
return mps * METERS_PER_SECOND_TO_KNOTS
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 deg_to_rad(deg: float) -> float:
"""Converts degrees to radians."""
return deg * DEGREES_TO_RADIANS
def rad_to_deg(rad: float) -> float:
"""Converts radians to degrees."""
return rad * RADIANS_TO_DEGREES