SXXXXXXX_ScenarioSimulator/scenario_simulator/gui/plot_manager.py
2025-09-30 10:21:49 +02:00

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