Initial ARSS. Need to improve and add functions.

This commit is contained in:
2026-03-02 14:48:21 +09:00
commit 775668afdb
20 changed files with 1610 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
function [det_mask, threshold_map, detections] = detect_targets_cfar(rd_map, RadarParams)
% CFAR target detection for range-doppler map
% - method: 'CA' or 'OS'
% - dimension: '1D' or '2D'
% - axis (for 1D): 'range' or 'doppler'
%
% Input:
% rd_map : [Ndoppler x Nrange] complex or real map
% RadarParams : struct containing SP.CFAR options
%
% Output:
% det_mask : logical detection mask [Ndoppler x Nrange]
% threshold_map : threshold map [Ndoppler x Nrange]
% detections : [Ndet x 2] = [doppler_bin, range_bin]
cfg = RadarParams.SP.CFAR;
method = upper(string(cfg.method));
dim_mode = upper(string(cfg.dimension));
axis_mode = lower(string(cfg.axis));
pfa = cfg.pfa;
train = cfg.train;
guard = cfg.guard;
if numel(train) == 1
train = [train, train];
end
if numel(guard) == 1
guard = [guard, guard];
end
os_rank_ratio = cfg.rank;
os_scale = cfg.os_scale;
rd_power = abs(rd_map).^2;
[n_dop, n_rng] = size(rd_power);
det_mask = false(n_dop, n_rng);
threshold_map = nan(n_dop, n_rng);
switch dim_mode
case "2D"
td = train(1); tr = train(2);
gd = guard(1); gr = guard(2);
for d = (td+gd+1):(n_dop-(td+gd))
for r = (tr+gr+1):(n_rng-(tr+gr))
d_idx = (d-(td+gd)):(d+(td+gd));
r_idx = (r-(tr+gr)):(r+(tr+gr));
win = rd_power(d_idx, r_idx);
cut_d = td+gd+1;
cut_r = tr+gr+1;
guard_mask = false(size(win));
guard_mask((cut_d-gd):(cut_d+gd), (cut_r-gr):(cut_r+gr)) = true;
train_cells = win(~guard_mask);
th = local_cfar_threshold(train_cells, method, pfa, os_rank_ratio, os_scale);
threshold_map(d, r) = th;
det_mask(d, r) = rd_power(d, r) > th;
end
end
case "1D"
switch axis_mode
case "range"
tr = train(2); gr = guard(2);
for d = 1:n_dop
[det_row, th_row] = cfar_1d_line(rd_power(d, :), tr, gr, method, pfa, os_rank_ratio, os_scale);
det_mask(d, :) = det_row;
threshold_map(d, :) = th_row;
end
case "doppler"
td = train(1); gd = guard(1);
for r = 1:n_rng
[det_col, th_col] = cfar_1d_line(rd_power(:, r).', td, gd, method, pfa, os_rank_ratio, os_scale);
det_mask(:, r) = det_col.';
threshold_map(:, r) = th_col.';
end
otherwise
error('CFAR axis must be ''range'' or ''doppler'' when dimension is 1D.');
end
otherwise
error('CFAR dimension must be ''1D'' or ''2D''.');
end
[d_idx, r_idx] = find(det_mask);
detections = [d_idx, r_idx];
end
function [det_line, th_line] = cfar_1d_line(x, t, g, method, pfa, rank_ratio, os_scale)
n = numel(x);
det_line = false(1, n);
th_line = nan(1, n);
left = t + g;
right = t + g;
for i = (left+1):(n-right)
l_train = x((i-g-t):(i-g-1));
r_train = x((i+g+1):(i+g+t));
train_cells = [l_train, r_train];
th = local_cfar_threshold(train_cells, method, pfa, rank_ratio, os_scale);
th_line(i) = th;
det_line(i) = x(i) > th;
end
end
function th = local_cfar_threshold(train_cells, method, pfa, rank_ratio, os_scale)
train_cells = train_cells(:);
n_train = numel(train_cells);
if n_train == 0
th = inf;
return;
end
switch method
case "CA"
noise_hat = mean(train_cells);
alpha = n_train * (pfa^(-1/n_train) - 1);
th = alpha * noise_hat;
case "OS"
sorted_cells = sort(train_cells, 'ascend');
k = max(1, min(n_train, round(rank_ratio * n_train)));
noise_hat = sorted_cells(k);
th = os_scale * noise_hat;
otherwise
error('CFAR method must be ''CA'' or ''OS''.');
end
end
+18
View File
@@ -0,0 +1,18 @@
function target_rd_map = integrate_nci_rdm(rd_cube, RadarParams)
% NCI (Noncoherent Integration) 2D RDM
% :
% rd_cube: [RX, TX, Doppler, Range]
% RadarParams.Waveform.mimoMode: 'TDM'
% :
% target_rd_map: [Doppler, Range]
%
% :
% - TDM : TX-RX (|.|^2)
% - : TX=1 RX
if strcmpi(RadarParams.Waveform.mimoMode, 'TDM')
target_rd_map = squeeze(sum(sum(abs(rd_cube).^2, 1), 2));
else
target_rd_map = squeeze(sum(abs(rd_cube(:, 1, :, :)).^2, 1));
end
end
@@ -0,0 +1,45 @@
function [rd_map, doppler_axis] = process_doppler_fft(range_profile, RadarParams)
% :
% - range_profile: [NumRx, NumTx, NumChirps, NumRangeBins]
% - RadarParams:
NumChirps = RadarParams.Waveform.NumChirps;
window_type = RadarParams.SP.RDM.window_type_doppler;
[~, ~, ~, ~] = size(range_profile);
%
lambda = RadarParams.Waveform.lambda_c; % = c/fc
% 1. (PRI, Pulse Repetition Interval)
T_pri = RadarParams.Waveform.PRI;
% 2. Doppler-FFT용 ( )
% (3 ) .
if strcmpi(window_type, 'none')
win_doppler = ones(1, NumChirps);
elseif strcmpi(window_type, 'hamming')
win_doppler = hamming(NumChirps)';
elseif strcmpi(window_type, 'blackman')
win_doppler = blackman(NumChirps)';
elseif strcmpi(window_type, 'hann')
win_doppler = hann(NumChirps)';
else % default: 'chebwin'
win_doppler = chebwin(NumChirps, 60)'; % 60dB
end
win_data = range_profile .* reshape(win_doppler, [1, 1, NumChirps, 1]);
% 3. Doppler-FFT (3 : Chirp)
% NFFT를 NumChirps보다 .
rd_fft = fft(win_data, NumChirps, 3);
% 4. fftshift ( 0 )
% [-, 0, +] .
rd_map = fftshift(rd_fft, 3);
% 5. (Velocity Axis)
% Vmax = lambda / (4 * T_pri)
v_max = lambda / (4 * T_pri);
% dv = lambda / (2 * NumChirps * T_pri)
doppler_axis = linspace(-v_max, v_max, NumChirps);
end
@@ -0,0 +1,48 @@
function [range_profile, range_axis] = process_range_fft_lpf(adc_raw_data, RadarParams)
% :
% - adc_raw_data: [NumRx, NumTx, NumChirps, N_samples]
% - RadarParams:
fs_adc = RadarParams.Waveform.fs_adc;
Slope = RadarParams.Waveform.Slope;
fc_lpf_Hz = RadarParams.Rxpath.fc_lpf;
window_type = RadarParams.SP.RDM.window_type_range;
[~, ~, ~, N_samples] = size(adc_raw_data);
c = RadarParams.Basic.c;
% 1.
if strcmpi(window_type, 'none')
win = ones(1, N_samples);
elseif strcmpi(window_type, 'hamming')
win = hamming(N_samples)';
elseif strcmpi(window_type, 'blackman')
win = blackman(N_samples)';
elseif strcmpi(window_type, 'chebwin')
win = chebwin(N_samples, 60)';
else % default: 'hann'
win = hann(N_samples)';
end
win_data = adc_raw_data .* reshape(win, [1, 1, 1, N_samples]);
% 2. Range-FFT
range_fft = fft(win_data, N_samples, 4);
% 3.
df = fs_adc / N_samples;
freq_axis = (0 : N_samples-1) * df;
full_range_axis = (freq_axis * c) / (2 * Slope);
% 4. Ideal LPF
cutoff_idx = floor(fc_lpf_Hz / df);
%
half_idx = floor(N_samples/2);
final_idx = min(cutoff_idx, half_idx);
% 5. [ ] (Truncation)
% 1 final_idx까지만
range_profile = range_fft(:, :, :, 1:final_idx);
range_axis = full_range_axis(1:final_idx);
end
@@ -0,0 +1,159 @@
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
fig = figure('Name', '2D Range-Doppler Map');
imagesc(r_axis, v_axis, 20*log10(abs(target_rd_map)));
axis xy; % y축 ()
colorbar;
xlabel('Range (m)');
ylabel('Velocity (m/s)');
title('Range-Doppler Map (Single Channel)');
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;
enable_datasheet_phase_noise = 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
if isfield(ni,'phaseNoise') && isstruct(ni.phaseNoise) && isfield(ni.phaseNoise,'enabled')
enable_datasheet_phase_noise = logical(ni.phaseNoise.enabled);
end
show_nonideal_overlay = show_nonideal_overlay && ...
(enable_phase_noise || enable_nonlinearity || enable_datasheet_phase_noise);
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