203 lines
8.9 KiB
Python
203 lines
8.9 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_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_nm_var = ppi_range_nm_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.sector_line_min = None
|
|
self.sector_line_max = 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()
|
|
|
|
# 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):
|
|
"""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 (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')
|
|
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_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(
|
|
[-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_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
|
|
|
|
# --- 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)
|
|
|
|
if self.cbar_rd:
|
|
self.cbar_rd.remove()
|
|
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)
|
|
|
|
doppler_freq_axis = np.fft.fftshift(np.fft.fftfreq(iq_data_cpi.shape[0], d=1.0/radar_cfg.prf))
|
|
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)
|
|
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 ---
|
|
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_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_nm, 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_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
|
|
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_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 |