Sub mm Precision

image0

The problem

Multiple systems exhibit sub-mm precision (i.e. precision lower than 1 mm), yet the FMCW basics is that resolution is a function of the chirp bandwidth.

Antenna designs often limits bandwidth to sub 10-GHz which effectively limits resolution to 3 cm.

The solution

Instead of using the frequency of the FFT bin, use a frequency estimator with higher precision. While it will not be possible to resolve two targets with better than the FFT range resolution, it will be possible to increase precision of the estimated frequency.

Small list of possible algorithms are:

  • simple FFT

  • FFT with zero padding

  • Quinn’s second interpolation [1]

  • Phase based interpolation

[1] Quinn, BG, “Estimation of frequency, amplitude and phase from the DFT of a time series,” IEEE Trans. Sig. Proc. Vol 45, No 3, Mar 1997, pp814-817.

[1]:
# 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:52:04.873208
[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

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
print(mmWrt_version)
0.0.14.dev1+g243a05bf3.d20260717

Frequency Estimator for sub-mm precision

Below is just an example with nominal values. For a more comprehensive analysis, noise sensitivity (phase noise in RX channel), channel noise and other aspects would need to be considered.

For embedded systems, a zoom FFT, can be used to reduce the memory needed for the full FFT to be computed.

Overview - Comparing

FFT estimator (\(\approx 9 mm\))

vs

Quinn2 estimator (\(\approx 0.4 \mu m\))

[3]:
from numpy import linspace
from numpy import complex128 as complex

debug_ON = False
c = 3e8
test = 0
fs = 10.24e3
k = 50e8
BW = 4e9
chirp_end_time0 = BW/k
radar = Radar(transmitter=Transmitter(chirp_end_time=chirp_end_time0,
                                      chirp_slope=k,
                                      chirp_count=1),
              receiver=Receiver(adc_sample_rate=fs,
                                adc_sample_count=8192,
                                adc_sample_count_max=9000,
                                debug=debug_ON), debug=debug_ON)

range_bin_index = 100
delta_R = c/2/BW
d_start = delta_R * (range_bin_index - 0.25)
d_end = d_start + delta_R/2
increments_count = 20
ds = linspace(d_start, d_end, increments_count)
ds = linspace((d_start+d_end)/2, (d_start+d_end)/2+increments_count, 1)
print("ds", ds)
print("ic", increments_count)
print("radar range resolution", radar.range_resolution)
print(f"d_end-dstart: {d_end-d_start}")

# compare FFT to quinn2
errors = []
for est in ["fft", "quinn2"]:
    errors_in_mm = []
    deltas = []
    for idx, d1 in enumerate(ds):
        print(idx)
        delta = idx/increments_count
        deltas.append(delta*100)

        scatterers = [Scatterer(d1)]
        bb = rt_points([radar], scatterers,
                       radar,
                       datatype=complex, 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> 10
        target_filter = ((mag_r > mag_c) & (mag_r > 20))

        index_peaks = where(target_filter)[0]
        # note: grouped_peaks only returns integer values
        # an improvement could be to return float values here with
        # simple interpolator
        grouped_peaks = rsp.peak_grouping_1d(index_peaks, mag_r)
        ipeaks = rsp.frequency_estimator(range_profile, grouped_peaks,
                                         estimator_name=est)
        f2d = rsp.if2d(radar)
        distances = f2d  * (radar.adc_sample_rate* ipeaks/radar.adc_sample_count)
        found_scatterer = [Scatterer(d) for d in distances]
        error = rsp.error(scatterers, found_scatterer)
        if error>2:
            print("errr",idx, error)
        else:
            pass
        errors_in_mm.append(error*1e3)
    max_error = max(errors_in_mm)
    errors.append(max_error)
    plt.plot(deltas, errors_in_mm)
    plt.title(f"max error: {max_error:.2g}mm in one range bin for: {est}")
    plt.xlabel('% of one range bin')
    plt.ylabel('rms error in mm')
    plt.show()
ds [3.75]
ic 20
radar range resolution 0.0375
d_end-dstart: 0.018749999999999822
0
_images/Precision_5_1.png
0
_images/Precision_5_3.png
[11]:
from numpy.testing import assert_almost_equal
print(errors[0])
print(errors[1])
assert_almost_equal(errors[0]*1e-3, 9.375e-3)
assert_almost_equal(errors[1]*1e-3, 4.309e-10)
215.62499999999974
18.751936479492137
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[11], line 4
      2 print(errors[0])
      3 print(errors[1])
----> 4 assert_almost_equal(errors[0]*1e-3, 9.375e-3)
      5 assert_almost_equal(errors[1]*1e-3, 4.309e-10)

File c:\dvpt_tools\venv_mmWrt\lib\site-packages\numpy\testing\_private\utils.py:627, in assert_almost_equal(actual, desired, decimal, err_msg, verbose)
    625     pass
    626 if abs(desired - actual) >= np.float64(1.5 * 10.0**(-decimal)):
--> 627     raise AssertionError(_build_err_msg())

AssertionError:
Arrays are not almost equal to 7 decimals
 ACTUAL: np.float64(0.21562499999999976)
 DESIRED: 0.009375
[ ]: