add radar profile, add tkinter gui

This commit is contained in:
VALLONGOL 2025-09-30 08:27:22 +02:00
parent 4bec0e6d34
commit 5b32692d34
8 changed files with 512 additions and 53 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Module",
"type": "debugpy",
"request": "launch",
"module": "scenario_simulator"
}
]
}

25
GEMINI.md Normal file
View File

@ -0,0 +1,25 @@
Sono un ingegnere informatico che sviluppa principalmente in python e c++.
Quando mi proponi del codice ricordati di indicarmi le modifiche con il codice precedente, perchè le hai fatte, dove e come sono state fatte.
Utilizzando tutte le regole per scrivere in maniera migliore possibile codice in python.
Non dare nulla per scontato e spiega tutti i passi che segui nei tuoi ragionamenti.
Con me parla in italiano.
Come deve essere scritto il codice:
1) come standard di scrittura del codice Python, lo standard PEP8.
2) una istruzione per ogni riga di codice
3) nomi di funzioni, variabili, commenti, doc_string devono essere in inglese
4) codice più chiaro, ordinato, riutilizzabile
5) i commenti nel codice devono essere essenziali, stringati e chiari, non devono essere prolissi. dobbiamo cercare di mantenere il codice più pulito possibile, senza troppi fronzoli
6) Non indicare le modifche che fai come commento al codice, ma solo in linea generale in chat. il codice lascialo più pulito possibile
Per semplificare l'operazione di aggiornamento del codice:
1) se le modifiche che proponi interessano solo poche funzioni del modulo, allora indicami il contenuto di tutte le funzioni dove ci sono le modifiche.
2) se le modifiche impattano la maggior parte delle funzioni dello stesso modulo, allora ripeti per intero il codice del modulo senza omissioni.
3) se le modifiche che proponi interessano meno di 5 righe di una funzione, indicami quali sono le linee che cambiano e come modificarle
4) passami sempre un modulo alla volta e ti dico io quando passarmi il successivo, sempre in maniera completa e senza omissioni
Se vedi che il codice di un singolo modulo è più lungo di 1000 righe, prendi in considerazione il fatto di creare un nuovo modulo spostando quelle funzioni che sono omogenee per argomento in questo nuovo modulo e rendere più leggere il file che sta crescendo troppo.
Quando ti passo del codice da analizzare, cerca sempre di capirne le funzionalità e se hai da proporre dei miglioramenti o delle modifiche prima ne discuti con me e poi decidiamo se applicarlo oppure no.
Se noti che nel codice c'è qualcosa da migliorare, ne parli con me e poi vediamo se applicarlo oppure no, per evitare di mettere mano a funzioni che sono già state ottimizzate e funzionano come io voglio, e concentrarsi sulla risoluzione di problemi o l'introduzione di nuove funzioni.

View File

@ -1,17 +1,15 @@
# scenario_simulator/__main__.py """
Main entry point for the Radar Scenario Simulator application.
# Example import assuming your main logic is in a 'main' function This script launches the main graphical user interface (GUI).
# within a 'app' module in your 'scenario_simulator.core' package. """
# from scenario_simulator.core.app import main as start_application
# from .gui.gui import start_gui
# Or, if you have a function in scenario_simulator.core.core:
# from scenario_simulator.core.core import main_function
def main(): def main():
print(f"Running scenario_simulator...") """Main function to launch the GUI."""
# Placeholder: Replace with your application's entry point print("--- Launching Radar Scenario Simulator GUI (Tkinter) ---")
# Example: start_application() start_gui()
print("To customize, edit 'scenario_simulator/__main__.py' and your core modules.")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -39,17 +39,16 @@ class Target:
velocity: np.ndarray velocity: np.ndarray
rcs: float rcs: float
def generate_iq_data(config: RadarConfig, target: Target, num_pulses: int) -> np.ndarray: def generate_iq_data(config: RadarConfig, targets: list[Target], num_pulses: int) -> np.ndarray:
""" """
Generates the I/Q data matrix for a single target scenario. Generates the I/Q data matrix for a list of targets.
This function simulates the received echo from a single point target with This function simulates the received echo from multiple point targets, each with
constant velocity over a specified number of pulses (Coherent Processing its own constant velocity, over a specified number of pulses.
Interval - CPI).
Args: Args:
config: The radar configuration parameters. config: The radar configuration parameters.
target: The target's properties. targets: A list of Target objects to simulate.
num_pulses: The total number of pulses to simulate. num_pulses: The total number of pulses to simulate.
Returns: Returns:
@ -60,50 +59,41 @@ def generate_iq_data(config: RadarConfig, target: Target, num_pulses: int) -> np
wavelength = c / config.carrier_frequency wavelength = c / config.carrier_frequency
pri = 1.0 / config.prf # Pulse Repetition Interval pri = 1.0 / config.prf # Pulse Repetition Interval
# Calculate the number of samples within one pulse's duration
num_samples_per_pulse = int(config.pulse_width * config.sample_rate) num_samples_per_pulse = int(config.pulse_width * config.sample_rate)
# --- Initialize output data structure # --- Initialize output data structure
# This matrix will store the complex I/Q data for each pulse
iq_matrix = np.zeros((num_pulses, num_samples_per_pulse), dtype=np.complex128) iq_matrix = np.zeros((num_pulses, num_samples_per_pulse), dtype=np.complex128)
# --- Main simulation loop (over slow-time) # --- Main simulation loop (over slow-time)
for pulse_index in range(num_pulses): for pulse_index in range(num_pulses):
# Current time relative to the start of the CPI
current_slow_time = pulse_index * pri current_slow_time = pulse_index * pri
# Update target position based on its constant velocity # --- Loop over all targets for each pulse ---
current_position = target.initial_position + target.velocity * current_slow_time for target in targets:
# Update target position based on its constant velocity
# Calculate range from radar (at origin) to target current_position = target.initial_position + target.velocity * current_slow_time
range_to_target = np.linalg.norm(current_position)
# Calculate the two-way time delay for the echo
time_delay = 2 * range_to_target / c
# Simplified signal amplitude based on radar range equation (power ~ 1/R^4)
# We use sqrt(rcs) because we are modeling voltage/amplitude, not power.
amplitude = np.sqrt(target.rcs) / (range_to_target**2)
# The core of the simulation: calculate the complex phase shift
# This phase is determined by the round-trip path length in wavelengths
phase_shift = np.exp(-1j * 4 * np.pi * range_to_target / wavelength)
signal_complex_amplitude = amplitude * phase_shift
# Model a simple rectangular pulse
pulse_shape = np.ones(num_samples_per_pulse)
received_pulse = signal_complex_amplitude * pulse_shape
# --- Place the received pulse into the I/Q matrix at the correct time delay
start_sample = int(round(time_delay * config.sample_rate))
if 0 <= start_sample < iq_matrix.shape[1]:
# This handles cases where the pulse might partially go out of the sampling window
samples_to_write = min(num_samples_per_pulse, iq_matrix.shape[1] - start_sample)
iq_matrix[ range_to_target = np.linalg.norm(current_position)
pulse_index, start_sample : start_sample + samples_to_write
] = received_pulse[:samples_to_write] time_delay = 2 * range_to_target / c
amplitude = np.sqrt(target.rcs) / (range_to_target**2)
phase_shift = np.exp(-1j * 4 * np.pi * range_to_target / wavelength)
signal_complex_amplitude = amplitude * phase_shift
pulse_shape = np.ones(num_samples_per_pulse)
received_pulse = signal_complex_amplitude * pulse_shape
start_sample = int(round(time_delay * config.sample_rate))
if 0 <= start_sample < iq_matrix.shape[1]:
samples_to_write = min(num_samples_per_pulse, iq_matrix.shape[1] - start_sample)
# Coherently add the pulse from this target to the IQ matrix
iq_matrix[
pulse_index, start_sample : start_sample + samples_to_write
] += received_pulse[:samples_to_write]
return iq_matrix return iq_matrix

View File

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

View File

@ -0,0 +1,38 @@
"""
Handles loading and saving of radar configuration profiles from a JSON file.
"""
import json
import os
# Use a file in the user's home directory to persist profiles
# across different project locations or versions.
PROFILES_FILE = os.path.join(os.path.expanduser("~"), ".radar_simulator_profiles.json")
def load_profiles():
"""
Loads radar configuration profiles from the JSON file.
If the file doesn't exist or is corrupted, returns an empty dictionary.
"""
if not os.path.exists(PROFILES_FILE):
return {}
try:
with open(PROFILES_FILE, 'r') as f:
profiles = json.load(f)
# Basic validation
if isinstance(profiles, dict):
return profiles
return {}
except (IOError, json.JSONDecodeError):
return {}
def save_profiles(profiles):
"""
Saves the given profiles dictionary to the JSON file.
Returns True on success, False on failure.
"""
try:
with open(PROFILES_FILE, 'w') as f:
json.dump(profiles, f, indent=4)
return True
except IOError:
return False

View File

@ -0,0 +1,35 @@
"""
Utility functions for radar-related mathematical calculations.
"""
from scipy.constants import c
def calculate_max_unambiguous_range(prf: float) -> float:
"""
Calculates the maximum unambiguous range for a given PRF.
Args:
prf: Pulse Repetition Frequency in Hertz (Hz).
Returns:
The maximum unambiguous range in meters (m).
"""
if prf == 0:
return float('inf')
return c / (2 * prf)
def calculate_max_unambiguous_velocity(carrier_frequency: float, prf: float) -> float:
"""
Calculates the maximum unambiguous velocity for a given radar configuration.
Args:
carrier_frequency: Carrier frequency in Hertz (Hz).
prf: Pulse Repetition Frequency in Hertz (Hz).
Returns:
The maximum unambiguous (Nyquist) velocity in m/s.
"""
if carrier_frequency == 0:
return float('inf')
wavelength = c / carrier_frequency
return (prf * wavelength) / 4