import radarsimpy
print("`RadarSimPy` used in this example is version: " + str(radarsimpy.__version__))
`RadarSimPy` used in this example is version: 15.2.0
CFAR Detection¶
CFAR (Constant False Alarm Rate) adjusts the detection threshold based on local noise, keeping the false alarm probability constant regardless of clutter or interference level.
How it works:
- Estimate background noise power from neighboring training cells
- Scale estimate by a threshold factor derived from
pfa - Declare detection if the test cell exceeds the threshold
RadarSimPy CFAR algorithms:
| Algorithm | Function | Method | Best for |
|---|---|---|---|
| CA-CFAR | cfar_ca_1d / cfar_ca_2d |
Averages all training cells | Homogeneous noise |
| OS-CFAR | cfar_os_1d / cfar_os_2d |
Uses k-th ordered statistic | Cluttered / multi-target environments |
Key parameters: guard — cells adjacent to test cell (excluded from estimate); trailing — training cells each side; pfa — false alarm probability; detector — "squarelaw" or "linear"; k — OS-CFAR rank.
Processing pipeline: Baseband I/Q → Range FFT → CFAR thresholding → detections.
This Example¶
- Radar: 24.125 GHz center, 100 MHz BW, single chirp
- Targets: 2 point targets at ~95 m (20 dBsm) and ~30 m (10 dBsm)
- Algorithms: CA-CFAR vs OS-CFAR comparison on a 1D range profile
Radar System Configuration¶
Import Required Modules¶
import numpy as np
import scipy.constants as const
from radarsimpy import Radar, Transmitter, Receiver
import plotly.graph_objs as go
from IPython.display import Image, display
# Set to True for interactive plots; False renders a static JPEG (e.g. for HTML export)
INTERACTIVE = False
def show(fig):
if INTERACTIVE:
fig.show()
else:
display(Image(fig.to_image(format="jpg", scale=2)))
Transmitter Channel Configuration¶
Define transmitter antenna location (monostatic, co-located at origin).
tx_channel = dict(location=(0, 0, 0))
Transmitter Configuration¶
f and t define the frequency sweep. For a linear FMCW chirp: f = [f_start, f_end], t = [t_start, t_end] (or a single value t0 → [0, t0]). Bandwidth = |f[1] − f[0]|. prp ≥ t_end.
| prp
| +-----------+
|
| +---f[1]---> / /
| / /
| / / ...
| / /
| +---f[0]---> / /
|
| +-------+
| t[0] t[1]
| Parameter | Value | Notes |
|---|---|---|
| Frequency sweep | 24.075–24.175 GHz | 100 MHz BW, 1.5 m range resolution |
| Chirp duration | 80 μs | |
| TX power | 30 dBm | |
| PRP | 100 μs | |
| Pulses | 1 | Single chirp (no Doppler processing) |
tx = Transmitter(
f=[24.075e9, 24.175e9],
t=80e-6,
tx_power=30,
prp=100e-6,
pulses=1,
channels=[tx_channel],
)
Receiver Channel Configuration¶
Receiver antenna co-located with transmitter (monostatic).
rx_channel = dict(location=(0, 0, 0))
Receiver Configuration¶
Sampling rate sets maximum unambiguous range: $R_{max} = \frac{f_s c T_c}{2B} = 240$ m.
| Parameter | Value |
|---|---|
Sampling rate fs |
2 MHz |
| Noise figure | 6 dB |
| RF gain | 20 dB |
| Load resistor | 500 Ω |
| Baseband gain | 30 dB |
rx = Receiver(
fs=2e6,
noise_figure=6,
rf_gain=20,
load_resistor=500,
baseband_gain=30,
channels=[rx_channel],
)
Create Radar System¶
Combine transmitter and receiver into the complete radar.
radar = Radar(transmitter=tx, receiver=rx)
Target Configuration¶
| Target | Location (m) | Speed (m/s) | RCS (dBsm) |
|---|---|---|---|
| 1 | (95, 20, 0) | (−50, 0, 0) | 20 |
| 2 | (30, −5, 0) | (−22, 0, 0) | 10 |
target_1 = dict(location=(95, 20, 0), speed=(-50, 0, 0), rcs=20, phase=0)
target_2 = dict(location=(30, -5, 0), speed=(-22, 0, 0), rcs=10, phase=0)
targets = [target_1, target_2]
Simulate Baseband Signals¶
sim_radar computes the complex I/Q beat signal, incorporating propagation delay, Doppler shift, RCS weighting, and thermal noise.
Output shape: [channels, pulses, samples] → [1, 1, 160].
from radarsimpy.simulator import sim_radar
data = sim_radar(radar, targets)
timestamp = data["timestamp"]
baseband = data["baseband"] + data["noise"]
Visualize Baseband I/Q Signals¶
Display time-domain beat signal for the chirp. Two targets produce two overlapping beat frequencies.
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=timestamp[0, 0, :] * 1e6,
y=np.real(baseband[0, 0, :]),
name="I",
)
)
fig.add_trace(
go.Scatter(
x=timestamp[0, 0, :] * 1e6,
y=np.imag(baseband[0, 0, :]),
name="Q",
)
)
fig.update_layout(
title="I/Q Beat Signals for the First Chirp",
yaxis=dict(title="Amplitude (V)"),
xaxis=dict(title="Time (μs)"),
)
show(fig)
Range FFT (Fast-Time Processing)¶
A Chebyshev window (60 dB sidelobe suppression) is applied before the FFT to reduce spectral leakage. Output: range profile [1, 1, 160] — peaks at target ranges.
from scipy import signal
import radarsimpy.processing as proc
range_window = signal.windows.chebwin(radar.sample_prop["samples_per_pulse"], at=60)
range_profile = proc.range_fft(baseband, range_window)
Apply CFAR Detection¶
radarsimpy.processing provides 1D and 2D CFAR detectors. This example applies both 1D algorithms to the range profile.
CA-CFAR (cfar_ca_1d): The threshold is the mean of all 2 × trailing training cells scaled by a factor derived from pfa. Simple and efficient, but the threshold rises when other targets fall inside the training window.
OS-CFAR (cfar_os_1d): Sorts the 2 × trailing training cells and uses the k-th value (rank) as the noise estimate. More robust to target contamination — one or a few strong targets in the training window don't inflate the threshold.
ca_cfar = proc.cfar_ca_1d(
np.abs(range_profile[0, 0, :]) ** 2,
guard=2,
trailing=10,
pfa=1e-4,
detector="squarelaw",
)
os_cfar = proc.cfar_os_1d(
np.abs(range_profile[0, 0, :]) ** 2,
guard=0,
trailing=10,
k=14,
pfa=1e-4,
detector="squarelaw",
)
Compare CFAR Algorithms¶
Range profile with CA-CFAR and OS-CFAR adaptive thresholds. Detection occurs wherever the profile exceeds the threshold.
max_range = (
const.c
* radar.radar_prop["receiver"].bb_prop["fs"]
* radar.radar_prop["transmitter"].waveform_prop["pulse_length"]
/ radar.radar_prop["transmitter"].waveform_prop["bandwidth"]
/ 2
)
range_axis = np.linspace(
0, max_range, radar.sample_prop["samples_per_pulse"], endpoint=False
)
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=range_axis,
y=20 * np.log10(np.abs(range_profile[0, 0, :])),
name="Range profile",
)
)
fig.add_trace(go.Scatter(x=range_axis, y=10 * np.log10(ca_cfar), name="CA-CFAR"))
fig.add_trace(go.Scatter(x=range_axis, y=10 * np.log10(os_cfar), name="OS-CFAR"))
fig.update_layout(
title="Range Profile with CA-CFAR and OS-CFAR Thresholds",
yaxis=dict(title="Amplitude (dB)"),
xaxis=dict(title="Range (m)"),
)
show(fig)
Key observations:
- Both thresholds adapt to the local noise floor and rise near strong peaks.
- CA-CFAR threshold may be elevated in the vicinity of a strong target (target contamination of training cells).
- OS-CFAR threshold is stabler because a few outliers in the training window don't dominate the estimate.
- Guard cells (
guard=2for CA) prevent target energy from leaking into the training estimate.
Summary¶
- CA-CFAR averages all training cells — simple and efficient in homogeneous noise.
- OS-CFAR uses the k-th ordered statistic — robust when other targets contaminate the training window.
guardcells prevent target energy from biasing the noise estimate;pfasets the false-alarm floor.- The same algorithms extend to range-Doppler maps with
cfar_ca_2d/cfar_os_2d.
Things to Try¶
| Experiment | Parameter to change | Observable effect |
|---|---|---|
| Wider training window | Increase trailing |
Smoother, more stable threshold |
| More guard cells | Increase guard |
Threshold unaffected by target sidelobes |
| Tighter false-alarm | Decrease pfa |
Threshold rises; weaker targets missed |
| CA vs OS robustness | Place targets close together | OS threshold stays low; CA inflates |
| 2D detection | Use cfar_ca_2d() on a range-Doppler map |
2D adaptive thresholding |