SXXXXXXX_ScenarioSimulator/scenario_simulator/gui/gui.py
2025-09-30 13:06:27 +02:00

649 lines
35 KiB
Python

"""
Main GUI module for the Radar Scenario Simulator (Tkinter version).
This module is responsible for building the main application window, laying out
widgets, and coordinating the simulation and plotting managers.
"""
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from tkinter.scrolledtext import ScrolledText
import queue
import logging
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
# Import core simulation engine and utility functions
from ..core.simulation_engine import RadarConfig, AntennaConfig, ScanConfig, Target
from ..utils import radar_math, config_manager
# Import new manager classes and logger
from .plot_manager import PlotManager
from .simulation_manager import SimulationManager
from ..utils import logger
# --- Helper Dialog for Adding/Editing Targets ---
class AddTargetDialog(tk.Toplevel):
"""Dialog window for adding or editing target parameters."""
def __init__(self, parent, target_data=None):
super().__init__(parent)
self.title("Add New Target" if target_data is None else "Edit 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=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)
}
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"]
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)
button_frame = ttk.Frame(frame)
button_frame.grid(row=len(labels), 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):
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()
def show(self):
self.wait_window()
return self.result
# --- Main Application Class ---
class App(tk.Tk):
"""Main application window for the Radar Scenario Simulator."""
def __init__(self):
super().__init__()
self.title("Radar Scenario Simulator")
self.state('zoomed') # Maximize the window without borderless fullscreen
# --- Main Layout ---
main_paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_paned_window.pack(fill=tk.BOTH, expand=True)
# --- Left Column (Notebook and Logs) ---
left_column_frame = ttk.Frame(main_paned_window, width=600)
main_paned_window.add(left_column_frame, weight=1)
left_paned_window = ttk.PanedWindow(left_column_frame, orient=tk.VERTICAL)
left_paned_window.pack(fill=tk.BOTH, expand=True)
notebook_frame = ttk.Frame(left_paned_window, height=500)
left_paned_window.add(notebook_frame, weight=2)
log_frame = ttk.LabelFrame(left_paned_window, text="Log", height=200)
left_paned_window.add(log_frame, weight=1)
# --- Right Column (RD and PPI plots) ---
right_column_frame = ttk.Frame(main_paned_window)
main_paned_window.add(right_column_frame, weight=3)
right_paned_window = ttk.PanedWindow(right_column_frame, orient=tk.VERTICAL)
right_paned_window.pack(fill=tk.BOTH, expand=True)
rd_frame = ttk.LabelFrame(right_paned_window, text="Range-Doppler Map")
right_paned_window.add(rd_frame, weight=1)
ppi_frame = ttk.Frame(right_paned_window)
right_paned_window.add(ppi_frame, weight=1)
# --- Initialize UI Variables ---
self._init_vars()
# --- Create UI Components ---
notebook = ttk.Notebook(notebook_frame)
notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.config_tab = ttk.Frame(notebook)
self.target_tab = ttk.Frame(notebook)
self.scenario_tab = ttk.Frame(notebook)
notebook.add(self.config_tab, text="Configuration")
notebook.add(self.target_tab, text="Target")
notebook.add(self.scenario_tab, text="Scenario")
self._populate_config_tab(self.config_tab)
self._populate_target_tab(self.target_tab)
self._populate_scenario_tab(self.scenario_tab)
# --- Setup Logging Area ---
self.log_widget = ScrolledText(log_frame, state=tk.DISABLED, wrap=tk.WORD, font=("TkDefaultFont", 9))
self.log_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self._init_logging()
# --- Setup Plotting Area ---
ppi_plot_frame = ttk.LabelFrame(ppi_frame, text="PPI Display")
ppi_plot_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0), pady=5)
scan_controls_frame = ttk.LabelFrame(ppi_frame, text="Scan & PPI Control")
scan_controls_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 5), pady=5)
self._populate_scan_ppi_controls(scan_controls_frame)
self.rd_figure = Figure(figsize=(8, 6), dpi=100, facecolor='#3a3a3a')
self.rd_canvas = FigureCanvasTkAgg(self.rd_figure, master=rd_frame)
self.rd_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.ppi_figure = Figure(figsize=(4, 4), dpi=100, facecolor='#3a3a3a')
self.ppi_canvas = FigureCanvasTkAgg(self.ppi_figure, master=ppi_plot_frame)
self.ppi_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# --- Initialize Managers ---
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()
# --- Finalize Init ---
self.plot_manager.update_ppi_range() # Set initial PPI range
self.toggle_amplitude_controls()
self.update_derived_parameters()
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.log.info("Application initialized successfully.")
def _init_logging(self):
logging_config = {
"default_root_level": logging.INFO,
"format": "%(asctime)s [%(levelname)-8s] %(name)-20s: %(message)s",
"date_format": "%H:%M:%S",
"enable_console": True,
"colors": {
logging.DEBUG: "gray",
logging.INFO: "black",
logging.WARNING: "orange",
logging.ERROR: "red",
logging.CRITICAL: "red",
}
}
logger.setup_basic_logging(self, logging_config)
logger.add_tkinter_handler(self.log_widget, logging_config)
self.log = logger.get_logger(__name__)
def on_closing(self):
self.log.info("Shutdown sequence initiated.")
if self.simulation_manager.is_running():
self.stop_simulation()
logger.shutdown_logging_system()
self.destroy()
def _init_vars(self):
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),
"beamwidth_az_deg": tk.DoubleVar(value=3.0),
"beamwidth_el_deg": tk.DoubleVar(value=3.0),
"scan_mode": tk.StringVar(value='staring'),
"min_az_deg": tk.DoubleVar(value=-30.0),
"max_az_deg": tk.DoubleVar(value=30.0),
"scan_speed_deg_s": tk.DoubleVar(value=20.0),
"num_pulses_cpi": tk.IntVar(value=256),
"simulation_duration_s": tk.DoubleVar(value=10.0),
"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(),
"dwell_time_text": tk.StringVar(),
"pulses_on_target_text": tk.StringVar(),
"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")
}
self.profiles = config_manager.load_profiles()
self.selected_profile = 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)
def _create_labeled_spinbox(self, parent, text, var, from_, to, increment=1.0, is_db=False, scientific=False, command=None):
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=2)
ttk.Label(frame, text=text, width=25).pack(side=tk.LEFT)
if scientific:
spinbox = ttk.Spinbox(frame, from_=from_, to=to, increment=increment, textvariable=var, format="%.2e", command=command)
else:
spinbox = ttk.Spinbox(frame, from_=from_, to=to, increment=increment, textvariable=var, command=command)
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
return spinbox
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('<<ComboboxSelected>>', 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, scientific=True)
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, "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.pack(fill=tk.X, padx=5, pady=5)
self._create_labeled_spinbox(antenna_group, "Azimuth Beamwidth (deg):", self.vars["beamwidth_az_deg"], 0.1, 90.0, increment=0.1)
scan_group = ttk.LabelFrame(tab, text="Scan Strategy", padding=10)
scan_group.pack(fill=tk.X, padx=5, pady=5)
mode_frame = ttk.Frame(scan_group)
mode_frame.pack(fill=tk.X, pady=2)
ttk.Label(mode_frame, text="Scan Mode:", width=25).pack(side=tk.LEFT)
ttk.Radiobutton(mode_frame, text="Staring", variable=self.vars["scan_mode"], value="staring", command=self.update_scan_mode_controls).pack(side=tk.LEFT)
ttk.Radiobutton(mode_frame, text="Sector", variable=self.vars["scan_mode"], value="sector", command=self.update_scan_mode_controls).pack(side=tk.LEFT)
self.min_az_spinbox = self._create_labeled_spinbox(scan_group, "Min Azimuth (deg):", self.vars["min_az_deg"], -180, 180, increment=1.0)
self.max_az_spinbox = self._create_labeled_spinbox(scan_group, "Max Azimuth (deg):", self.vars["max_az_deg"], -180, 180, increment=1.0)
self.scan_speed_spinbox = self._create_labeled_spinbox(scan_group, "Scan Speed (deg/s):", self.vars["scan_speed_deg_s"], 0.1, 1000.0, increment=1.0)
self.update_scan_mode_controls()
derived_group = ttk.LabelFrame(tab, text="Derived & Calculated Values", padding=10)
derived_group.pack(fill=tk.X, padx=5, pady=5)
for key in ["pulse_width_text", "listening_time_text"]:
ttk.Label(derived_group, textvariable=self.vars[key]).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)
ttk.Separator(derived_group, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
for key in ["dwell_time_text", "pulses_on_target_text"]:
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)
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)
self.analysis_frame = ttk.LabelFrame(tab, text="Simulation Analysis & Warnings", padding=10)
self.analysis_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.analysis_text = tk.Text(self.analysis_frame, wrap=tk.WORD, height=10, state=tk.DISABLED, font=('TkDefaultFont', 10))
self.analysis_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
analysis_text_scroll = ttk.Scrollbar(self.analysis_frame, command=self.analysis_text.yview)
analysis_text_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.analysis_text.config(yscrollcommand=analysis_text_scroll.set)
def _create_target_table(self, parent):
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)")
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.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)
self.target_table.configure(yscrollcommand=scrollbar.set)
self.target_table.bind("<Double-1>", self.on_target_double_click)
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, "Pulses per CPI:", self.vars["num_pulses_cpi"], 1, 8192)
self._create_labeled_spinbox(parent, "Simulation Duration (s):", self.vars["simulation_duration_s"], 0.1, 300.0, increment=0.1)
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"], -200, 100, is_db=True)
self._create_labeled_spinbox(parent, "Max Display Amplitude (dB):", self.vars["max_db"], -200, 100, is_db=True)
self.generate_button = ttk.Button(parent, text="Start Simulation", command=self.start_simulation)
self.generate_button.pack(pady=5)
self.stop_button = ttk.Button(parent, text="Stop Simulation", command=self.stop_simulation, state=tk.DISABLED)
self.stop_button.pack(pady=5)
def _populate_scan_ppi_controls(self, parent):
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)
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):
if self.simulation_manager.is_running():
return
self.log.info("Starting simulation...")
self.generate_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
radar_cfg = self.get_radar_config_from_gui()
targets = self.get_targets_from_gui()
if not targets:
self.log.warning("Simulation start requested but no targets are defined.")
messagebox.showwarning("No Targets", "Please add at least one target to simulate.", parent=self)
self.stop_simulation()
return
self.plot_manager.setup_plots_for_simulation(
self.vars["num_pulses_cpi"].get(), self.vars["min_db"].get(), self.vars["max_db"].get(), targets, radar_cfg
)
self.simulation_manager.start(
radar_cfg, targets, self.vars["simulation_duration_s"].get(), self.vars["num_pulses_cpi"].get()
)
self.after(100, self._check_simulation_queue)
def stop_simulation(self):
self.log.info("Stopping simulation...")
self.simulation_manager.stop()
self.generate_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.plot_manager.clear_rd_plot()
self.analysis_text.config(state=tk.NORMAL)
self.analysis_text.delete(1.0, tk.END)
self.analysis_text.insert(tk.END, "Simulation stopped or not started.")
self.analysis_text.config(state=tk.DISABLED)
def _check_simulation_queue(self):
try:
frame_data = self.simulation_manager.data_queue.get_nowait()
if frame_data is None:
self.log.info("Simulation thread finished.")
self.stop_simulation()
return
radar_cfg = self.get_radar_config_from_gui()
self.plot_manager.update_plots(
frame_data, radar_cfg, self.vars["auto_scale"].get(), self.vars["min_db"].get(), self.vars["max_db"].get()
)
self._update_analysis_text(radar_cfg, self.get_targets_from_gui(), frame_data[0], frame_data[1])
except queue.Empty:
pass
finally:
if self.simulation_manager.is_running():
self.after(50, self._check_simulation_queue)
def on_ppi_range_change(self, *args):
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()])
self.check_target_warnings()
if self.plot_manager:
self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
def remove_selected_target(self):
selected_items = self.target_table.selection()
for i in selected_items:
self.target_table.delete(i)
self.check_target_warnings()
if self.plot_manager:
self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
def get_radar_config_from_gui(self) -> RadarConfig:
antenna_cfg = AntennaConfig(beamwidth_az_deg=self.vars["beamwidth_az_deg"].get(), beamwidth_el_deg=self.vars["beamwidth_el_deg"].get())
scan_cfg = ScanConfig(mode=self.vars["scan_mode"].get(), min_az_deg=self.vars["min_az_deg"].get(), max_az_deg=self.vars["max_az_deg"].get(), scan_speed_deg_s=self.vars["scan_speed_deg_s"].get())
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]:
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 = np.array(float_values[0:3])
vel = np.array(float_values[3:6])
rcs = float_values[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)
return targets
def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi):
report = []
max_range_m = radar_math.calculate_max_unambiguous_range(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(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: +/-{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")
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)
pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, radar_cfg.prf)
report.append(f"Calculated Dwell Time: {dwell_time * 1e3:,.2f} ms")
report.append(f"Calculated Pulses on Target: {pulses_on_target:,}")
if pulses_on_target < self.vars["num_pulses_cpi"].get():
report.append("WARNING: Pulses per CPI > Pulses on Target")
report.append("\n--- Target Analysis ---")
if not targets:
report.append("No targets defined.")
else:
for i, target in enumerate(targets):
report.append(f"\nTarget {i+1} (RCS={target.rcs:.1f} m^2) - Analysis based on initial state.")
self.analysis_text.config(state=tk.NORMAL)
self.analysis_text.delete(1.0, tk.END)
self.analysis_text.insert(tk.END, "\n".join(report))
self.analysis_text.config(state=tk.DISABLED)
self.analysis_text.see(tk.END)
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()
beamwidth = self.vars["beamwidth_az_deg"].get()
scan_mode = self.vars["scan_mode"].get()
scan_speed = self.vars["scan_speed_deg_s"].get()
if prf > 0:
pri = 1.0 / prf
pulse_width = pri * (duty_cycle / 100.0)
self.vars["pulse_width_text"].set(f"Pulse Width: {pulse_width * 1e6:,.2f} µs")
self.vars["listening_time_text"].set(f"Listening Window: {pri * 1e6:,.2f} µs (Max Range Time)")
else:
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)")
max_range_m = radar_math.calculate_max_unambiguous_range(prf)
if np.isinf(max_range_m):
self.vars["max_range_text"].set("Max Unambiguous Range: Infinite (PRF is zero)")
else:
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_mps = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf)
if np.isinf(max_vel_mps):
self.vars["max_velocity_text"].set("Max Unambiguous Velocity: Infinite (carrier freq is zero)")
else:
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:
self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)")
self.vars["pulses_on_target_text"].set("Pulses on Target: N/A (Staring)")
else:
dwell_time = radar_math.calculate_dwell_time(beamwidth, scan_speed)
pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, prf)
if np.isinf(dwell_time):
self.vars["dwell_time_text"].set("Dwell Time: Infinite (Scan Speed is zero)")
self.vars["pulses_on_target_text"].set("Pulses on Target: Infinite")
else:
self.vars["dwell_time_text"].set(f"Dwell Time: {dwell_time * 1e3:,.2f} ms")
self.vars["pulses_on_target_text"].set(f"Pulses on Target: {pulses_on_target:,}")
self.check_target_warnings()
self._update_scan_info_text()
# Ensure plot_manager exists before trying to update plots, as this can be called during init
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):
mode = self.vars['scan_mode'].get()
if mode == 'staring':
self.vars['scan_info_text'].set("Mode: Staring")
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()
self.vars['scan_info_text'].set(f"Mode: Sector Scan\nAz: [{min_az}° , {max_az}°] @ {speed}°/s")
def check_target_warnings(self):
if not hasattr(self, 'target_table'): return
try:
prf, carrier_freq = self.vars["prf"].get(), self.vars["carrier_frequency"].get()
max_range_m = radar_math.calculate_max_unambiguous_range(prf)
max_vel_mps = radar_math.calculate_max_unambiguous_velocity(carrier_freq, 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_initial_range_m = np.linalg.norm(values[0:3])
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_mps = 0
if not np.isinf(max_range_m) and target_initial_range_m > max_range_m: range_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_velocity_label.config(foreground='orange' if vel_warning else 'black')
except (tk.TclError, ValueError, ZeroDivisionError):
if hasattr(self, 'max_range_label'): self.max_range_label.config(foreground='black')
if hasattr(self, 'max_velocity_label'): self.max_velocity_label.config(foreground='black')
def open_add_target_dialog(self):
dialog = AddTargetDialog(self)
result = dialog.show()
if result: self.add_target_to_table(result)
def on_target_double_click(self, event):
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()])
self.check_target_warnings()
if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
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]
for key, value in profile_data.items():
if key in self.vars: self.vars[key].set(value)
self.update_scan_mode_controls()
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 = {key: var.get() for key, var in self.vars.items() if isinstance(var, (tk.DoubleVar, tk.StringVar, tk.IntVar))}
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 toggle_amplitude_controls(self):
state = tk.DISABLED if self.vars["auto_scale"].get() else tk.NORMAL
if hasattr(self, 'min_db_spinbox'): self.min_db_spinbox.config(state=state)
if hasattr(self, 'max_db_spinbox'): self.max_db_spinbox.config(state=state)
def update_scan_mode_controls(self, *args):
mode = self.vars["scan_mode"].get()
state = tk.NORMAL if mode != 'staring' else tk.DISABLED
if mode == 'staring': self.vars["scan_speed_deg_s"].set(0.0001)
elif self.vars["scan_speed_deg_s"].get() <= 0.0001: self.vars["scan_speed_deg_s"].set(20.0)
if hasattr(self, 'min_az_spinbox'): self.min_az_spinbox.config(state=state)
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 start_gui():
"""Entry point to launch the Tkinter GUI application."""
app = App()
app.mainloop()