FMCW Intro

You can open this workbook in Google Colab to experiment with mmWrt image0

Below is an intro to mmWrt for simple scatterers position estimation

For a generic introduction to mmWave sensors: Watch Here

linear chirps and IF frequencies

Signal model According to (Barriok 1973; Stove 1992; Komarov and Smolskiy 2003; Winkler 2007) the transmitted signal of an FMCW radar system can be modeled as where

\[y_T (t) = A_T \cdot cos\left( 2 \pi \cdot ( f_{0min} \cdot t +\int_0^tf_T({\tau}) d\tau)\right)\]

Given for a linear chirp that

\[f_T(\tau) = \frac{B}{T} \cdot \tau = s \cdot \tau\]

We derive the phase, given

\[\int_0^t f_T({\tau}) d\tau = \frac{s}{2} \cdot \tau^2 - K\]

which can be written as:

\[y_T (t) = A_T \cdot cos \left( 2 \pi \cdot ( f_{0min} \cdot t + \frac{s}{2} \cdot t^2 )+\Phi_0 \right)\]

Where:

  • \(f_{0min}\) is the start frequency at the begining of the raising frequency of the chirp.

  • s is the slope at which the frequency is ramped ( \(S = \frac{B}{T}\))

  • B is the total bandwdith of the chirp

  • T is the total time of the chirp

Considering a reflected signal with a time delay \(\delta = 2 · \frac{R0+ v\cdot t}{c}\) and Doppler shift \(f_D = −2 · \frac{f_c \cdot v}{c}\)

Where:

  • c is the speed of light

  • \(f_D\) is the doppler shift

  • R0 is the nominal distance to the target

  • \(\Delta\) is the time of flight (to and from the target)

  • v is the velocity of the target

The receive signal \(y_R(t)\) can be written as :

\[y_R(t) = A_R \cdot cos ( 2\pi \cdot (f_{0min} + \frac{s}{2} \cdot (t-\delta)) \cdot (t-\delta))\]

\(y_{IF}(t)\) is the IF signal (after mixer) which is obtained by multiplication in the time domain, and passed to a low-pass filter (LPF)

This can be done easily when remembering the trigonometric relation:

\[cos(\alpha) \cdot cos(\beta) = \frac{cos(\alpha + \beta) + cos(\alpha - \beta)}{2}\]
\[y_{IF}(t) = y_R(t) \cdot y_T(t)\]

Noticing that the element which sums the elements will be higher frequency and will be filtered by the LPF, it remains that:

\[y_{IF}(t) = \frac{A_t \cdot A_r}{2} \cdot cos(2 \pi \cdot [f_{0min} \cdot \delta + s \cdot \delta \cdot t - \frac{s}{2} \cdot \delta^2])\]

Where:

  • \(f_{0min}\) the starting frequency of the chirp

  • s is the slope of the chirp

  • \(\Delta\) is the total time of flight between antennas and target

  • At, Ar: Amplitude of the RX and TX waves

[8]:
# Install a pip package in the current Jupyter kernel
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:51:00.369680
[9]:
from os.path import abspath, join, pardir
import sys
import matplotlib.pyplot as plt
from matplotlib import colormaps
from matplotlib import colors
from numpy import where, expand_dims

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
from mmWrt import __version__ as mmWrt_version
[10]:
c = 3e8

debug_ON = False
test = 0
chirp_bandwidth = 2e9  # 1e9
chirp_slope0 = 5e12  #280e8
chirp_end_time0 = chirp_bandwidth/chirp_slope0

radar = Radar(transmitter=Transmitter(chirp_end_time=chirp_end_time0,
                                      chirp_slope=chirp_slope0),
              receiver=Receiver(adc_sample_rate=1e6,
                                adc_sample_count=128,
                                debug=debug_ON), debug=debug_ON)

# artificially define scatters so they fall in the
# middle of range bin for illustration purposes
# later of what happen when it is not the case
scatterer1 = Scatterer(5.0390625)
scatterer2 = Scatterer(2*5.0390625)
scatterers = [scatterer1, scatterer2]

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)

found_scatterers = [Scatterer(Distances[i]) for i in index_peaks]
error = rsp.error([scatterer1, scatterer2], found_scatterers)
print("synthetic scatterers", [t.distance() for t in scatterers])
print("found scatterers", [t.distance() for t in found_scatterers])
print("error is", error)

# 2D representation of the FFT and CFAR
# plot on X,Y axis the FFT and CFAR
plt.plot(Distances, mag_r)
plt.plot(Distances, mag_c)
plt.title("2D plots FFT w/ CFAR")
plt.show()
synthetic scatterers [np.float64(5.0390625), np.float64(10.078125)]
found scatterers [np.float64(4.921875), np.float64(5.15625), np.float64(10.078125)]
error is 5.0390625
_images/Intro_nb_4_1.png
[11]:
# 1D representation of the FFT
# useful later on to show the link between 1D FFT and 2D FFTs

# Select the color map named CMRmap_r
# cmap = cm.get_cmap(name='CMRmap_r')
cmap = colormaps['CMRmap_r']
# convert the 1D array in 2D array to plot using imshow
mag_r_2d = expand_dims(mag_r, axis=0)
# set aspect ratio to auto to have high enough pixels to see them in the y_axis
# change the norm to have a log color scale
# to better see the peaks in correlation with 2D FFT plot
vmin_mag = min(mag_r)
vmax_mag = max(mag_r)
color_col = colors.LogNorm(vmin=vmin_mag, vmax=vmax_mag)
plt.imshow(mag_r_2d, cmap,
           aspect='auto',
           norm=color_col)
plt.title("1D FFT")
# plt.savefig(fp_fft_1D)
plt.show()
_images/Intro_nb_5_0.png

NON REGRESSION

[12]:
from datetime import datetime as dt
assert index_peaks[0]==21
print(f"last run: {dt.now().date()} with {mmWrt_version}")
last run: 2026-07-17 with 0.0.14.dev1+g243a05bf3.d20260717