FMCW Radar Resolution

image0

The problem

Definition of resolution can vary in industries. For radar systems and most contactless distance measurements systems, resolution refers to the ability to resolve two distincts objects apart from each other.

The solution

Experimenting with FFT to distinguish two targets apart from each other.

[1]:
# Install a pip package in the current Jupyter kernel
# Recommended when running from Google Colab or similar environment
%load_ext autoreload
%autoreload 2
import sys
from os.path import abspath, basename, join, pardir
import datetime

# hack to handle if running from git cloned folder or stand alone (like Google Colab)
cw = basename(abspath(join(".")))
dp = abspath(join(".",pardir))
if cw=="docs" and basename(dp) == "mmWrt":
    # running from cloned folder
    print("running from git folder, using local path (latest) mmWrt code", dp)
    sys.path.insert(0, dp)
else:
    print("running standalone, need to ensure mmWrt is installed")
    !{sys.executable} -m pip install mmWrt
print(datetime.datetime.now())

running from git folder, using local path (latest) mmWrt code c:\git\mmWrt
2026-07-17 18:53:18.951229
[2]:
from os.path import abspath, join, pardir
import sys
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib import colors
from numpy import arange, where, expand_dims

# uncomment below if the notebook is launched from project's root folder
# dp = abspath(join(".",pardir))
# sys.path.insert(0, dp)
from mmWrt import __version__ as mmWrt_ver
print(mmWrt_ver)

from mmWrt.Raytracing import rt_points  # noqa: E402
from mmWrt.Scene import Radar, Transmitter, Receiver, Scatterer  # noqa: E402
from mmWrt import RadarSignalProcessing as rsp  # noqa: E402

0.0.14.dev1+g243a05bf3.d20260717

Two targets which can be separated

[3]:
c = 3e8

debug_ON = False
test = 0
bw = 1e9
chirp_slope0 = 70e8
chirp_end_time0 = bw/chirp_slope0
fs = 1e3
NA = 64
radar = Radar(transmitter=Transmitter(chirp_end_time=chirp_end_time0,
                                      chirp_slope=chirp_slope0, chirp_count=64),
              receiver=Receiver(adc_sample_rate=fs, adc_sample_count=NA,
                                debug=debug_ON), debug=debug_ON)

target1 = Scatterer(3)
target2 = Scatterer(4)  #, 0, 0, xt=lambda t: 2.*t+3.)

scatterers = [target1, target2]

bb = rt_points([radar], scatterers,
               radar, debug=debug_ON)

Distances, range_profile = rsp.range_fft(bb["adc_cube"][0, 0, 0, :], bb)
mag_r = abs(range_profile)
ca_cfar = rsp.cfar_ca(mag_r, train_cell_count=10,
                      guard_cell_count=1, pfa=1e-1)
mag_c = abs(ca_cfar)
# little hack to remove small FFT ripples : mag_r> 5
target_filter = ((mag_r > mag_c) & (mag_r > 5))

index_peaks = where(target_filter)[0]
grouped_peaks = rsp.peak_grouping_1d(index_peaks, mag_r)

found_scatterer = [Scatterer(Distances[i]) for i in grouped_peaks]
error = rsp.error([target1, target2], found_scatterer)
print("synthetic targets", [t.pos_t(t=0) for t in scatterers])
print("found targets", [t.pos_t(t=0) for t in found_scatterer])
print("error is", error)

# 2D representation of the FFT and CFAR
# plot on X,Y axis the FFT and CFAR
figure, axes = plt.subplots()
plt.plot(Distances, mag_r, '-o')
plt.plot(Distances, mag_c, '-r')
plt.title("Two targets separated in range bins")

# Add illustration of target separation
circle1_xy = (target1.x, mag_r[grouped_peaks[0]])
circle_1 = plt.Circle( circle1_xy, 1, fill=False)
circle2_xy = (target2.x, mag_r[grouped_peaks[1]])
circle_2 = plt.Circle( circle2_xy, 1, fill=False)

axes.add_artist(circle_1)
axes.add_artist(circle_2)
plt.annotate("two targets which *CAN* be resolved", xy=circle1_xy, xytext=(8,0.9*NA//2),
             horizontalalignment="center",
             # Custom arrow
             arrowprops=dict(arrowstyle='->',lw=1)
             )

plt.annotate("", xy=circle2_xy,xytext=(8,0.9*NA//2),
             horizontalalignment="center",
             # Custom arrow
             arrowprops=dict(arrowstyle='->',lw=1)
             )

plt.show()
synthetic targets [array([3., 0., 0.]), array([4., 0., 0.])]
found targets [array([3.01339286, 0.        , 0.        ]), array([4.01785714, 0.        , 0.        ])]
error is 0.031250000000000444
_images/Resolution_5_1.png

Two targets which cannot be separated

[4]:
c = 3e8

target3 = Scatterer(3)
target4 = Scatterer(3.4)  #, 0, 0, vx=lambda t: 2*t+0)
print("radar.range_resolution", radar.range_resolution)
scatterers2 = [target3, target4]

bb2 = rt_points([radar], scatterers2,
               radar, debug=debug_ON)

Distances, range_profile = rsp.range_fft(bb2["adc_cube"][0, 0, 0, :], bb)
mag_r = abs(range_profile)
ca_cfar = rsp.cfar_ca(mag_r, train_cell_count=10,
                      guard_cell_count=1, pfa=1e-1)
mag_c = abs(ca_cfar)
# little hack to remove small FFT ripples : mag_r> 5
# target_filter = ((mag_r > mag_c) & (mag_r > 5))

index_peak = where(target_filter)[0]
print(index_peaks.shape)
grouping = False
if grouping:
    grouped_peaks = rsp.peak_grouping_1d(index_peaks)
    found_targets = [Scatterer(Distances[i]) for i in grouped_peaks]
else:
    found_targets = [Scatterer(Distances[i]) for i in index_peaks]
error2 = rsp.error([target3, target4], found_targets)
print("synthetic targets", [t.pos_t(t=0) for t in scatterers2])
print("found targets", [t.pos_t(t=0) for t in found_targets])
print("error2 is", error2)

# 2D representation of the FFT and CFAR
# plot on X,Y axis the FFT and CFAR

figure, axes = plt.subplots()
m, M = 100000-20000, 100000+20000
m, M = 0, -1
R = range(len(mag_r))
plt.plot(R[m:M], mag_r[m:M], 'o-')
plt.plot(R[m:M], mag_c[m:M], 'r-')
plt.title("Two targets in adjacent range bins")

# Add illustration of spectral leakage
circle1_xy = (index_peak[0], mag_r[index_peak[0]]-0.5)
circle_1 = plt.Circle( circle1_xy, 2, fill=False)
circle2_xy = (index_peak[0]+1, mag_r[index_peak[0]]-1.5)
circle_2 = plt.Circle( circle2_xy, 2, fill=False)

axes.add_artist(circle_1)
axes.add_artist(circle_2)
plt.annotate("Targets which *CAN-NOT* be resolved", xy=circle1_xy,
             xytext=(16,0.8*NA//2),
             horizontalalignment="center",
             # Custom arrow
             arrowprops=dict(arrowstyle='->',lw=1)
             )

plt.annotate("                                   ", xy=circle2_xy,
             xytext=(16,0.8*NA//2),
             horizontalalignment="center",
             # Custom arrow
             arrowprops=dict(arrowstyle='->',lw=1)
             )

plt.show()
radar.range_resolution 0.33482142857142855
(2,)
synthetic targets [array([3., 0., 0.]), array([3.4, 0. , 0. ])]
found targets [array([3.01339286, 0.        , 0.        ]), array([4.01785714, 0.        , 0.        ])]
error2 is 0.6312500000000005
_images/Resolution_7_1.png
[7]:
print(mmWrt_ver)
assert error==0.031250000000000444
assert error2 == 0.6312500000000005
0.0.14.dev1+g243a05bf3.d20260717