171 lines
7.4 KiB
Python
171 lines
7.4 KiB
Python
"""
|
|
Manages all matplotlib plotting operations for the Radar Scenario Simulator GUI.
|
|
|
|
This module encapsulates the logic for initializing, clearing, and updating
|
|
the Range-Doppler (RD) and Plan Position Indicator (PPI) plots.
|
|
"""
|
|
import numpy as np
|
|
from scipy.constants import c
|
|
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):
|
|
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
|
|
|
|
# Artists that will be updated
|
|
self.ax_rd = None
|
|
self.im_rd = None
|
|
self.cbar_rd = None
|
|
self.ax_ppi = None
|
|
self.ppi_targets_plot = []
|
|
self.beam_patch = None
|
|
self.beam_line = None
|
|
|
|
self._init_base_plots()
|
|
|
|
def _init_base_plots(self):
|
|
"""Initializes the plots with titles and labels, called once."""
|
|
self.init_ppi_plot()
|
|
self.clear_rd_plot()
|
|
|
|
def init_ppi_plot(self):
|
|
"""Clears and initializes the polar axes for the PPI plot."""
|
|
self.ppi_figure.clear()
|
|
self.ax_ppi = self.ppi_figure.add_subplot(111, polar=True)
|
|
self.ax_ppi.set_title('PPI Display', color='white', fontsize=10)
|
|
self.ax_ppi.set_facecolor('#3a3a3a')
|
|
self.ax_ppi.set_theta_zero_location("N")
|
|
self.ax_ppi.set_theta_direction(-1)
|
|
self.ax_ppi.set_rlabel_position(-22.5)
|
|
self.ax_ppi.tick_params(axis='x', colors='white', labelsize=8)
|
|
self.ax_ppi.tick_params(axis='y', colors='lightgray', labelsize=8)
|
|
self.ax_ppi.grid(color='gray', linestyle='--')
|
|
self.ppi_figure.tight_layout()
|
|
|
|
if hasattr(self, 'ppi_targets_plot'):
|
|
self.ppi_targets_plot.clear()
|
|
self.ppi_canvas.draw()
|
|
|
|
def clear_rd_plot(self):
|
|
"""Clears the Range-Doppler plot."""
|
|
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.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')
|
|
self.rd_figure.tight_layout()
|
|
self.rd_canvas.draw()
|
|
|
|
def setup_plots_for_simulation(self, num_pulses_cpi, min_db, max_db, targets, radar_cfg):
|
|
"""Prepares the plots at the start of a simulation."""
|
|
self.clear_rd_plot()
|
|
self.im_rd = self.ax_rd.imshow(np.zeros((num_pulses_cpi, 100)), aspect='auto', cmap='jet', vmin=min_db, vmax=max_db)
|
|
self.cbar_rd = self.rd_figure.colorbar(self.im_rd, ax=self.ax_rd)
|
|
self.cbar_rd.ax.tick_params(colors='white', labelsize=8)
|
|
self.cbar_rd.set_label('Amplitude (dB)', color='white', fontsize=8)
|
|
self.rd_figure.tight_layout()
|
|
|
|
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)
|
|
|
|
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
|
|
)
|
|
self.beam_line, = self.ax_ppi.plot([0, 0], [0, max_plot_range], color='cyan', linewidth=2)
|
|
|
|
self.ppi_canvas.draw()
|
|
self.rd_canvas.draw()
|
|
|
|
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
|
|
|
|
# --- Update Range-Doppler Map ---
|
|
if iq_data_cpi.size == 0:
|
|
self.im_rd.set_data(np.zeros((iq_data_cpi.shape[0], 100)))
|
|
self.ax_rd.set_title(f'RD Map (Frame {frame_num}) - No Data', color='white', fontsize=10)
|
|
else:
|
|
window = np.hanning(iq_data_cpi.shape[0])[:, np.newaxis]
|
|
iq_data_windowed = iq_data_cpi * 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 = min_db, max_db
|
|
if auto_scale:
|
|
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)
|
|
else:
|
|
vmin, vmax = -100, 0
|
|
|
|
self.im_rd.set_data(range_doppler_map_db)
|
|
self.im_rd.set_clim(vmin=vmin, vmax=vmax)
|
|
|
|
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
|
|
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]])
|
|
self.ax_rd.set_title(f'RD Map (Frame {frame_num})', color='white', fontsize=10)
|
|
|
|
# --- Update PPI Plot ---
|
|
current_az_rad = np.deg2rad(current_az_deg)
|
|
beamwidth_rad = np.deg2rad(radar_cfg.antenna_config.beamwidth_az_deg)
|
|
|
|
theta_beam = np.linspace(current_az_rad - beamwidth_rad / 2, current_az_rad + beamwidth_rad / 2, 50)
|
|
max_plot_range = 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_line.set_xdata([current_az_rad, current_az_rad])
|
|
|
|
self.rd_canvas.draw_idle()
|
|
self.ppi_canvas.draw_idle()
|
|
|
|
def redraw_ppi_targets(self, targets):
|
|
"""Clears and redraws all targets on the PPI plot."""
|
|
if not hasattr(self, 'ax_ppi') or not self.ax_ppi:
|
|
return
|
|
|
|
for plot in self.ppi_targets_plot:
|
|
plot.remove()
|
|
self.ppi_targets_plot.clear()
|
|
|
|
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)
|
|
self.ppi_targets_plot.append(target_plot)
|
|
except (ValueError, IndexError):
|
|
continue
|
|
self.ppi_canvas.draw()
|
|
|
|
def update_ppi_range(self):
|
|
"""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)
|
|
self.ppi_canvas.draw()
|
|
except Exception:
|
|
pass # Ignore errors if entry is invalid |