add radar profile, add tkinter gui
This commit is contained in:
parent
4bec0e6d34
commit
5b32692d34
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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
25
GEMINI.md
Normal 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.
|
||||||
@ -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()
|
||||||
@ -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
|
||||||
|
|
||||||
|
# --- Loop over all targets for each pulse ---
|
||||||
|
for target in targets:
|
||||||
# Update target position based on its constant velocity
|
# Update target position based on its constant velocity
|
||||||
current_position = target.initial_position + target.velocity * current_slow_time
|
current_position = target.initial_position + target.velocity * current_slow_time
|
||||||
|
|
||||||
# Calculate range from radar (at origin) to target
|
|
||||||
range_to_target = np.linalg.norm(current_position)
|
range_to_target = np.linalg.norm(current_position)
|
||||||
|
|
||||||
# Calculate the two-way time delay for the echo
|
|
||||||
time_delay = 2 * range_to_target / c
|
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)
|
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)
|
phase_shift = np.exp(-1j * 4 * np.pi * range_to_target / wavelength)
|
||||||
|
|
||||||
signal_complex_amplitude = amplitude * phase_shift
|
signal_complex_amplitude = amplitude * phase_shift
|
||||||
|
|
||||||
# Model a simple rectangular pulse
|
|
||||||
pulse_shape = np.ones(num_samples_per_pulse)
|
pulse_shape = np.ones(num_samples_per_pulse)
|
||||||
received_pulse = signal_complex_amplitude * pulse_shape
|
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))
|
start_sample = int(round(time_delay * config.sample_rate))
|
||||||
|
|
||||||
if 0 <= start_sample < iq_matrix.shape[1]:
|
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)
|
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[
|
iq_matrix[
|
||||||
pulse_index, start_sample : start_sample + samples_to_write
|
pulse_index, start_sample : start_sample + samples_to_write
|
||||||
] = received_pulse[:samples_to_write]
|
] += received_pulse[:samples_to_write]
|
||||||
|
|
||||||
return iq_matrix
|
return iq_matrix
|
||||||
@ -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()
|
||||||
38
scenario_simulator/utils/config_manager.py
Normal file
38
scenario_simulator/utils/config_manager.py
Normal 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
|
||||||
35
scenario_simulator/utils/radar_math.py
Normal file
35
scenario_simulator/utils/radar_math.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user