High Speed

Classical FMCW chirp processing computes the speed over multiple chirps.

However in the case where the range bin changes within a chirp, it is also possible to measure the speed via rate of range bin change.

The following is a simple illustration leveraging a STFT for illustration purposes

image0

[2]:
# 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

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 where, expand_dims
from numpy import complex128 as complex

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_ver
print(mmWrt_ver)
print(datetime.datetime.now())
running from git folder, using local path (latest) mmWrt code c:\git\mmWrt
0.0.11-pre.3
2026-06-27 09:42:51.071669
[6]:
from scipy.fft import fft, fft2
from scipy.signal import stft

c = 3e8

debug_ON = False
test = 0
NC=1
n=16
NA=512*n*2
fs0 = 10e5*n
chirp_slope0 = 2e12
tic0 = 1.2e-3
raytracing_opt = {"logging_level":10}
chirp_bandwidth = 4e9
chirp_end_time0 = chirp_bandwidth/chirp_slope0
adc_sample_rate0=fs0
radar = Radar(transmitter=Transmitter(chirp_end_time=chirp_end_time0,
                                      chirp_slope=chirp_slope0,
                                      chirp_period=tic0,
                                      chirps_count=NC),
              receiver=Receiver(adc_sample_rate=adc_sample_rate0,
                                adc_sample_count=NA,
                                adc_sample_count_max=NA*2,
                                adc_sample_rate_max=fs0*2,
                                debug=debug_ON), debug=debug_ON)

x1, v1 = 5, 10000*2
scatterer1 = Scatterer(xt=lambda t: v1*t+x1)

scatterers = [scatterer1]

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

cube = bb["adc_cube"][0,0,0,:]

seg_n = 512
# nperseg: Length of each segment
# noverlapint: Number of points to overlap between segments. If None, noverlap = nperseg // 2
# return_onesidedbool, optional - If True, return a one-sided spectrum for real data.
_, _, fft_st = stft(cube, nperseg=seg_n, return_onesided=False)
plt.title("STFT based range bin changes over time")
plt.xlabel("Time base")
plt.ylabel("Rang bin (idx)")
plt.imshow(abs(fft_st[:,:]))
plt.show()
_images/High-Speed_2_0.png

compute speed after STFT

[10]:
from scipy.signal import find_peaks
# find the range bin where target is at the begining of STFT
peak_at_start = find_peaks(abs(fft_st[:,0]))[0][0]
# find the range bin where the target is at the end of STFT
peak_at_end = find_peaks(abs(fft_st[:,-1]))[0][0]

# speed = Delta D/Delta T

# Range bin resolution from samples
R_bin_fft = fs0*c/2/chirp_slope0/NA
# Range bin resolution for STFT is scaled up by nperseg
R_bin_stft = R_bin_fft*NA/seg_n
# compute the chirp time, which is also the divider for the speed
Tc = NA*1/fs0
# The target distance grows 2x (there and back) so speed needs to be divided by 2
speed_estimate = (peak_at_end-peak_at_start)*R_bin_stft/Tc/2
print(f"speed estimate: {speed_estimate}")

speed estimate: 20599.365234375

non regression

[11]:
from datetime import datetime as dt
assert speed_estimate==20599.365234375
print(f"last run: {dt.now().date()}")
last run: 2026-06-27