diff --git a/scenario_simulator/gui/gui.py b/scenario_simulator/gui/gui.py index dd5dd13..68c90cc 100644 --- a/scenario_simulator/gui/gui.py +++ b/scenario_simulator/gui/gui.py @@ -38,17 +38,17 @@ class AddTargetDialog(tk.Toplevel): 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 5000.0), - "pos_y": tk.DoubleVar(value=target_data["pos_y"] if target_data else 0.0), - "pos_z": tk.DoubleVar(value=target_data["pos_z"] if target_data else 0.0), - "vel_x": tk.DoubleVar(value=target_data["vel_x"] if target_data else -150.0), - "vel_y": tk.DoubleVar(value=target_data["vel_y"] if target_data else 0.0), - "vel_z": tk.DoubleVar(value=target_data["vel_z"] if target_data else 0.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 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 (m):", "Initial Position Y (m):", "Initial Position Z (m):", - "Velocity X (m/s):", "Velocity Y (m/s):", "Velocity Z (m/s):", "RCS (m^2):"] + 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)): @@ -63,7 +63,16 @@ class AddTargetDialog(tk.Toplevel): 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()} + 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): @@ -76,8 +85,7 @@ class App(tk.Tk): def __init__(self): super().__init__() self.title("Radar Scenario Simulator") - self.attributes('-fullscreen', True) - self.bind("", lambda event: self.attributes('-fullscreen', False)) + self.state('zoomed') # Maximize the window without borderless fullscreen # --- Main Layout --- 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) # --- 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() # --- 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) @@ -204,12 +213,13 @@ class App(tk.Tk): "max_velocity_text": tk.StringVar(), "dwell_time_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") } 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"]: + 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): @@ -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, "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) @@ -298,7 +309,7 @@ class App(tk.Tk): 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") + 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: @@ -327,9 +338,28 @@ class App(tk.Tk): self.stop_button.pack(pady=5) 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('<>', 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 @@ -385,6 +415,7 @@ class App(tk.Tk): 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: @@ -420,11 +451,11 @@ class App(tk.Tk): def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi): report = [] - max_range = 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_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: {max_range:,.0f} m") - report.append(f"Max Unambiguous Velocity: +/-{max_vel:,.1f} m/s") + 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) @@ -463,17 +494,19 @@ class App(tk.Tk): 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 = radar_math.calculate_max_unambiguous_range(prf) - if np.isinf(max_range): + 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: - 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) - if np.isinf(max_vel): + 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: - 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: self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)") @@ -490,8 +523,18 @@ class App(tk.Tk): self.check_target_warnings() self._update_scan_info_text() - except (tk.TclError, ValueError) as e: - self.log.error(f"Error updating derived parameters: {e}") + # 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() @@ -499,22 +542,22 @@ class App(tk.Tk): 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") + 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 = radar_math.calculate_max_unambiguous_range(prf) - max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) + 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 = 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) - else: target_radial_vel = 0 - if not np.isinf(max_range) and target_initial_range > max_range: range_warning = True - if not np.isinf(max_vel) and abs(target_radial_vel) > max_vel: vel_warning = True + 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): @@ -530,7 +573,16 @@ class App(tk.Tk): selected_item = self.target_table.selection() if not selected_item: return 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) result = dialog.show() if result: diff --git a/scenario_simulator/gui/plot_manager.py b/scenario_simulator/gui/plot_manager.py index 07d7837..e119069 100644 --- a/scenario_simulator/gui/plot_manager.py +++ b/scenario_simulator/gui/plot_manager.py @@ -10,12 +10,12 @@ from ..utils import radar_math class PlotManager: """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_canvas = rd_canvas self.ppi_figure = ppi_figure 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 self.ax_rd = None @@ -25,6 +25,8 @@ class PlotManager: self.ppi_targets_plot = [] self.beam_patch = None self.beam_line = None + self.sector_line_min = None + self.sector_line_max = None self._init_base_plots() @@ -47,8 +49,11 @@ class PlotManager: self.ax_ppi.grid(color='gray', linestyle='--') self.ppi_figure.tight_layout() - if hasattr(self, 'ppi_targets_plot'): - self.ppi_targets_plot.clear() + # Artists were removed by ppi_figure.clear(). Just clear the reference lists. + self.ppi_targets_plot.clear() + self.sector_line_min = None + self.sector_line_max = None + self.ppi_canvas.draw() def clear_rd_plot(self): @@ -56,8 +61,8 @@ class PlotManager: self.rd_figure.clear() 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_xlabel('Range (m)', color='white', fontsize=8) - self.ax_rd.set_ylabel('Velocity (m/s)', color='white', fontsize=8) + self.ax_rd.set_xlabel('Range (NM)', 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='y', colors='white', labelsize=8) self.ax_rd.set_facecolor('#3a3a3a') @@ -76,21 +81,39 @@ class PlotManager: self.init_ppi_plot() self.redraw_ppi_targets(targets) - max_target_range = max(np.linalg.norm(t.initial_position) for t in targets) - max_unamb_range = radar_math.calculate_max_unambiguous_range(radar_cfg.prf) - 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) + max_plot_range_nm = self.ppi_range_nm_var.get() + self.ax_ppi.set_ylim(0, max_plot_range_nm) + beamwidth_rad = np.deg2rad(radar_cfg.antenna_config.beamwidth_az_deg) 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, max_plot_range, color='cyan', alpha=0.2, linewidth=0 + [-0.5 * beamwidth_rad, 0.5 * beamwidth_rad], + 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.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): """Update plots with new frame data from the simulation.""" 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_clim(vmin=vmin, vmax=vmax) - # Re-create colorbar to prevent state issues if self.cbar_rd: self.cbar_rd.remove() 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) 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_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) # --- Update PPI Plot --- @@ -138,9 +162,9 @@ class PlotManager: 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) - 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 = 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.rd_canvas.draw_idle() @@ -157,10 +181,11 @@ class PlotManager: for target in targets: try: - x, y = target.initial_position[0], target.initial_position[1] - r = np.linalg.norm([x, y]) - theta = np.arctan2(y, x) - target_plot, = self.ax_ppi.plot(theta, r, 'o', color='red', markersize=6) + x_m, y_m = target.initial_position[0], target.initial_position[1] + r_m = np.linalg.norm([x_m, y_m]) + r_nm = radar_math.meters_to_nm(r_m) + 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) except (ValueError, IndexError): continue @@ -170,9 +195,9 @@ class PlotManager: """Callback to update the PPI plot's range limit from the tk var.""" if not hasattr(self, 'ax_ppi'): return try: - new_range = self.ppi_range_var.get() - if new_range > 0: - self.ax_ppi.set_ylim(0, new_range) + new_range_nm = self.ppi_range_nm_var.get() + if new_range_nm > 0: + self.ax_ppi.set_ylim(0, new_range_nm) self.ppi_canvas.draw() except Exception: pass # Ignore errors if entry is invalid \ No newline at end of file diff --git a/scenario_simulator/utils/radar_math.py b/scenario_simulator/utils/radar_math.py index 452011a..b4bbed2 100644 --- a/scenario_simulator/utils/radar_math.py +++ b/scenario_simulator/utils/radar_math.py @@ -4,6 +4,12 @@ Utility functions for radar-related mathematical calculations. import numpy as np 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: """ 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. # This provides a more realistic falloff than a squared (two-way) model. gain = np.exp(-(angle_off_boresight_deg**2) / (2 * sigma**2)) - return gain \ No newline at end of file + 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 \ No newline at end of file