function fig = visualize_rd_map_with_spurs(target_rd_map, r_axis, v_axis, Target, SpurParams, RadarParams, Slope) % 2D Range-Doppler 맵을 시각화하고 스퍼 및 타겟 위치를 표시합니다. % % 입력: % - target_rd_map: 2D RD 맵 데이터 [Doppler x Range] % - r_axis: 거리 축 벡터 % - v_axis: 속도 축 벡터 % - Target: 타겟 정보 구조체 (R, v, NumTargets 필드 포함) % - SpurParams: 스퍼 파라미터 구조체 (lo, adc, mixer, pulse, lo_leak, clip 등) % - RadarParams: 레이더 파라미터 구조체 % - Slope: 스윕 속도 (Hz/s) % % 출력: % - fig: 생성된 figure 핸들 if ~isfield(RadarParams, 'SP') || ~isfield(RadarParams.SP, 'RDM') || ... ~isfield(RadarParams.SP.RDM, 'window_range') || ~isfield(RadarParams.SP.RDM, 'window_doppler') error('RadarParams.SP.RDM.window_range / window_doppler 정보가 필요합니다.'); end win_r = RadarParams.SP.RDM.window_range; win_d = RadarParams.SP.RDM.window_doppler; normal_factor = RadarParams.Rxpath.fc_lpf * RadarParams.Waveform.PRF * sum(win_r.^2) * sum(win_d.^2); if ~isfield(RadarParams, 'Rxpath') || ~isfield(RadarParams.Rxpath, 'load_impedance_ohm') error('RadarParams.Rxpath.load_impedance_ohm 값이 필요합니다.'); end load_impedance_ohm = RadarParams.Rxpath.load_impedance_ohm; psd2d_w_hz2 = (abs(target_rd_map).^2) / (load_impedance_ohm * max(normal_factor, eps)); rbw_range_hz = RadarParams.Waveform.fs_adc * sum(win_r.^2) / max(eps, (sum(win_r)^2)); rbw_doppler_hz = RadarParams.Waveform.PRF * sum(win_d.^2) / max(eps, (sum(win_d)^2)); rd_map_dbm_bin = 10*log10(max(psd2d_w_hz2, realmin) / 1e-3) + 10*log10(max(rbw_range_hz * rbw_doppler_hz, eps)); fig = figure('Name', '2D Range-Doppler Map'); imagesc(r_axis, v_axis, rd_map_dbm_bin); axis xy; % y축 방향(속도)을 위로 정렬 cb = colorbar; cb.Label.String = 'Power (dBm/bin)'; xlabel('Range (m)'); ylabel('Velocity (m/s)'); title('Range-Doppler Map (dBm/bin)'); colormap(jet); % 범례용 핸들 및 레이블 수집 hLines = gobjects(0); hLabels = {}; % --- 1. 실제 타겟 위치 표시 --- hold on; hTarget = gobjects(0); if exist('Target','var') && isstruct(Target) for k = 1:Target.NumTargets % 범위/속도 축 내에 있는지 확인 if Target.R(k) < min(r_axis) || Target.R(k) > max(r_axis) || ... Target.v(k) < min(v_axis) || Target.v(k) > max(v_axis) continue; % 지도 밖이면 생략 end [~, ix] = min(abs(r_axis - Target.R(k))); [~, iy] = min(abs(v_axis - Target.v(k))); hTarget(end+1) = plot(r_axis(ix), v_axis(iy), 'ro', 'MarkerSize',8, 'LineWidth',1.5); text(r_axis(ix), v_axis(iy), sprintf(' T%d', k), 'Color','r','FontSize',8); end end hold off; % 범례에 타겟 추가 if exist('hTarget','var') && ~isempty(hTarget) hLines(end+1) = hTarget(1); hLabels{end+1} = 'actual target'; end % --- nonideal/spur 오버레이 표시 여부 판단 --- show_nonideal_overlay = true; if exist('SpurParams','var') && isstruct(SpurParams) && isfield(SpurParams,'enabled') show_nonideal_overlay = logical(SpurParams.enabled); end if exist('RadarParams','var') && isstruct(RadarParams) && ... isfield(RadarParams,'Waveform') && isfield(RadarParams.Waveform,'nonideal') ni = RadarParams.Waveform.nonideal; enable_phase_noise = false; enable_nonlinearity = false; if isfield(ni,'enable_phase_noise') enable_phase_noise = logical(ni.enable_phase_noise); end if isfield(ni,'enable_nonlinearity') enable_nonlinearity = logical(ni.enable_nonlinearity); end show_nonideal_overlay = show_nonideal_overlay && ... (enable_phase_noise || enable_nonlinearity); end % --- 2. LO/ADC 톤 스퍼 표시 --- if show_nonideal_overlay && exist('SpurParams','var') && isstruct(SpurParams) spur_freqs = []; spur_names = {}; if isfield(SpurParams,'lo') && ~isempty(SpurParams.lo) spur_freqs(end+1) = SpurParams.lo.freq; spur_names{end+1} = 'LO spur'; end if isfield(SpurParams,'adc') && ~isempty(SpurParams.adc) spur_freqs(end+1) = SpurParams.adc.freq; spur_names{end+1} = 'ADC spur'; end hold on; for idx = 1:length(spur_freqs) f = spur_freqs(idx); name = sprintf('%s @ %.2f MHz', spur_names{idx}, f/1e6); r_spur = (RadarParams.Basic.c * f) / (2 * Slope); hLines(end+1) = xline(r_spur, '--m', name); hLabels{end+1} = name; end % --- 3. Pulse comb 스퍼 표시 --- if isfield(SpurParams,'pulse') && ~isempty(SpurParams.pulse) p = SpurParams.pulse; num_harm = 5; for n = 1:num_harm f_n = n * p.freq; r_n = (RadarParams.Basic.c * f_n) / (2 * Slope); if r_n <= max(r_axis) lbl = sprintf('pulse comb %d', n); hLines(end+1) = xline(r_n, '--g', lbl); hLabels{end+1} = lbl; end end end % --- 4. Mixer 비선형 스퍼 표시 --- if isfield(SpurParams,'mixer') && ~isempty(SpurParams.mixer) && exist('Target','var') base_ranges = Target.R; harmonics = [2,3]; colors = {'--c','--y'}; for hi = 1:length(harmonics) mul = harmonics(hi); for r0 = base_ranges r_spur2 = r0 * mul; if r_spur2 <= max(r_axis) lbl = sprintf('mixer x%d', mul); hLines(end+1) = xline(r_spur2, colors{hi}, lbl); hLabels{end+1} = lbl; end end end end % --- 5. DC offset / LO leakage 표시 --- if isfield(SpurParams,'lo_leak') && ~isempty(SpurParams.lo_leak) hLines(end+1) = line([min(r_axis), max(r_axis)], [0, 0], 'Color','r','LineStyle',':', 'LineWidth',1.5); hLabels{end+1} = 'DC offset/LO leak'; end % --- 6. Clipping spur 표시 --- if isfield(SpurParams,'clip') && ~isempty(SpurParams.clip) r_clip = SpurParams.clip.range; if r_clip >= min(r_axis) && r_clip <= max(r_axis) hLines(end+1) = line([r_clip, r_clip], [min(v_axis), max(v_axis)], 'Color', 'r', 'LineStyle', ':', 'LineWidth', 1.2); hLabels{end+1} = sprintf('clipping @ %.1f m', r_clip); end end hold off; end % --- 7. 범례 추가 --- if ~isempty(hLines) legend(hLines, hLabels, 'Location', 'northeastoutside'); end end