import radarsimpy
print("`RadarSimPy` used in this example is version: " + str(radarsimpy.__version__))
`RadarSimPy` used in this example is version: 15.2.0
FMCW Radar Link Budget Analysis (Point Target)¶
Validates RadarSimPy's point-target simulation against theoretical link budget calculations. Compared to the mesh-target notebook, point targets offer fast, analytical RCS specification — ideal for system design and range-budget sweeps.
Radar Range Equation: $$P_r = \frac{P_t G_t G_r \lambda^2 \sigma}{(4\pi)^3 R^4 L_s}, \quad \text{SNR} = \frac{P_r}{P_\text{noise}}, \quad P_\text{noise} = k T B F$$
System Parameters¶
| Component | Parameter | Value |
|---|---|---|
| Transmitter | Power $P_t$ | 13 dBm |
| Gain $G_t$ | 12 dB | |
| Pulses $N_p$ | 512 | |
| Receiver | Sampling rate $f_s$ | 20 MHz ($N_s$ = 1024 samples) |
| Gain $G_r$ | 12 dB | |
| Noise figure $F$ | 11 dB | |
| Target | RCS $\sigma$ | 10 dBsm (≈10 m², car-sized) |
| Range $R$ | 100 m |
Radar System Configuration¶
import numpy as np
import scipy.constants as sci_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)))
Antenna Configuration¶
Antenna gain: 12 dB. Idealized cosine patterns approximate a real radar antenna:
- Azimuth: $\cos^4(\theta) + 12$ dB
- Elevation: $\cos^{20}(\phi) + 12$ dB (narrower beam)
antenna_gain = 12
az_angle = np.arange(-80, 81, 1)
az_pattern = 20 * np.log10(np.cos(az_angle / 180 * np.pi) ** 4) + antenna_gain
el_angle = np.arange(-80, 81, 1)
el_pattern = 20 * np.log10((np.cos(el_angle / 180 * np.pi)) ** 20) + antenna_gain
Visualize Antenna Patterns¶
fig = go.Figure()
fig.add_trace(go.Scatter(x=az_angle, y=az_pattern, name="Azimuth"))
fig.add_trace(go.Scatter(x=el_angle, y=el_pattern, name="Elevation"))
fig.update_layout(
title="Antenna Pattern",
yaxis=dict(title="Amplitude (dB)", range=[-20, 20]),
xaxis=dict(title="Angle (deg)"),
)
show(fig)
Transmitter Channel¶
tx_channel = dict(
location=(0, 0, 0),
azimuth_angle=az_angle,
azimuth_pattern=az_pattern,
elevation_angle=el_angle,
elevation_pattern=el_pattern,
)
Radar Transmitter¶
| Parameter | Value | Notes |
|---|---|---|
| Frequency sweep | 76.3–76.7 GHz | 400 MHz BW, ΔR = 0.375 m |
| Chirp duration | 51.2 μs | |
| TX power | 13 dBm | |
| PRP | 55 μs | |
| Pulses | 512 |
tx = Transmitter(
f=[76.3e9, 76.7e9],
t=5.12e-05,
tx_power=13,
prp=5.5e-05,
pulses=512,
channels=[tx_channel],
)
Receiver Channel¶
rx_channel = dict(
location=(0, 0, 0),
azimuth_angle=az_angle,
azimuth_pattern=az_pattern,
elevation_angle=el_angle,
elevation_pattern=el_pattern,
)
Radar Receiver¶
| Parameter | Value |
|---|---|
Sampling rate fs |
20 MHz |
| Noise figure | 11 dB |
| RF gain | 20 dB |
| Baseband gain | 30 dB |
| Load resistor | 500 Ω |
| Baseband type | Real |
rx = Receiver(
fs=20e6,
noise_figure=11,
rf_gain=20,
load_resistor=500,
baseband_gain=30,
bb_type="real",
channels=[rx_channel],
)
Radar System¶
radar = Radar(transmitter=tx, receiver=rx)
Target: Point Target¶
A point target at 100 m, stationary, RCS = 10 dBsm (≈10 m², car-sized). Unlike mesh targets, point targets specify RCS directly — no STL model or ray tracing required.
target_1 = dict(location=(100, 0, 0), speed=(0, 0, 0), rcs=10, phase=0)
targets = [target_1]
Simulate Baseband Signals¶
sim_radar returns baseband and noise as separate arrays with shape [channels, pulses, ADC samples]. Combine them to get the noisy signal.
from radarsimpy.simulator import sim_radar
data = sim_radar(radar, targets)
timestamp = data["timestamp"]
baseband = data["baseband"] + data["noise"]
noise = data["noise"]
Range-Doppler Processing¶
Two sequential FFTs extract target information:
- Range FFT (fast-time): Beat frequency → range
- Doppler FFT (slow-time): Phase change across pulses → velocity
For this stationary target, energy concentrates at zero Doppler. Peak amplitude and mean noise floor are extracted per range bin to compute the simulated SNR.
import radarsimpy.processing as proc
# Perform 2D FFT: Range FFT followed by Doppler FFT
# fftshift moves zero-Doppler to center of array
range_doppler = np.fft.fftshift(proc.range_doppler_fft(baseband), axes=1)
# Process noise-only data to measure noise floor
noise_range_doppler = np.fft.fftshift(proc.range_doppler_fft(noise), axes=1)
# Extract peak signal per range bin (maximum across all Doppler bins)
# This gives us the strongest return at each range
max_per_range_bin = np.max(np.abs(range_doppler), axis=1)
# Calculate average noise level per range bin
# This represents the noise floor we must exceed for detection
noise_mean = np.mean(np.abs(noise_range_doppler), axis=1)
valid_range_bins = int(radar.sample_prop["samples_per_pulse"] / 2)
max_range = (
sci_const.c
* radar.radar_prop["receiver"].bb_prop["fs"]
* radar.radar_prop["transmitter"].waveform_prop["pulse_length"]
/ radar.radar_prop["transmitter"].waveform_prop["bandwidth"]
/ 4
)
range_axis = np.linspace(0, max_range, valid_range_bins, endpoint=False)
# Calculate Simulated SNR
peak_idx = np.argmax(max_per_range_bin[0, 0:valid_range_bins])
peak_val_db = 20 * np.log10(max_per_range_bin[0, peak_idx])
noise_val_db = 20 * np.log10(noise_mean[0, peak_idx])
simulated_snr = peak_val_db - noise_val_db
peak_range = range_axis[peak_idx]
print(f"Simulated SNR: {simulated_snr:.2f} dB at {peak_range:.1f} m")
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=range_axis,
y=20 * np.log10(max_per_range_bin[0, 0:valid_range_bins]),
name="Peak Signal per Range Bin",
line=dict(color="blue", width=2),
)
)
fig.add_trace(
go.Scatter(
x=range_axis,
y=20 * np.log10(noise_mean[0, 0:valid_range_bins]),
name="Noise Floor",
line=dict(color="red", width=2, dash="dash"),
)
)
fig.add_annotation(
x=peak_range,
y=peak_val_db,
text=f"SNR: {simulated_snr:.2f} dB",
showarrow=True,
arrowhead=1,
yshift=10,
)
fig.update_layout(
title="Range Profile: Signal Peak vs Noise Floor",
yaxis=dict(title="Amplitude (dB)"),
xaxis=dict(title="Range (m)"),
)
show(fig)
Simulated SNR: 33.49 dB at 100.1 m
The sharp peak at 100 m is the target return. The gap to the noise floor is the simulated SNR, compared to theory below. Peak width reflects range resolution ΔR = c/(2B) = 0.375 m.
Compare Simulated SNR with Theory¶
Received power and noise power in dBm:
$$P_r = P_t + G_t + G_r + 20\log_{10}(\lambda) - 30\log_{10}(4\pi) - 40\log_{10}(R) + \sigma$$
$$B_\text{eff} = \frac{f_s}{N_s N_p}, \quad P_\text{noise} = F + 10\log_{10}(B_\text{eff}) + 10\log_{10}(kT) + 30$$
# Constants
c = sci_const.c
k = sci_const.k
T = 290
# System Parameters
pt_dbm = 13
gt_db = 12
gr_db = 12
f = 76.5e9
lambda_val = c / f
sigma_db = 10 # Point target RCS
r = 100
# 1. Received Power (Pr)
# Pr (dBm) = Pt + Gt + Gr + 20log10(lambda) - 30log10(4pi) - 40log10(R) + sigma
pr_dbm = (
pt_dbm
+ gt_db
+ gr_db
+ 20 * np.log10(lambda_val)
- 30 * np.log10(4 * np.pi)
- 40 * np.log10(r)
+ sigma_db
)
print(f"Received Power (Pr): {pr_dbm:.2f} dBm")
# 2. Noise Power (Pnoise)
fs = 20e6
ns = 1024
np_pulses = 512
b_eff_hz = fs / (ns * np_pulses)
b_eff_db = 10 * np.log10(b_eff_hz)
nf_db = 11
thermal_noise_dbm_hz = 10 * np.log10(k * T * 1000)
p_noise_dbm = thermal_noise_dbm_hz + b_eff_db + nf_db
print(f"Noise Power (Pnoise): {p_noise_dbm:.2f} dBm")
# 3. SNR
snr = pr_dbm - p_noise_dbm
print(f"Theoretical SNR: {snr:.2f} dB")
Received Power (Pr): -114.11 dBm Noise Power (Pnoise): -147.16 dBm Theoretical SNR: 33.05 dB
Validation Result¶
The theoretical SNR calculated above matches the simulated result in the plot!
This excellent agreement validates that RadarSimPy's sim_radar() accurately models electromagnetic propagation, point target RCS computation, and system noise characteristics.
Summary¶
The simulated SNR matched the theoretical prediction, validating point-target RCS, free-space propagation, and noise modeling in RadarSimPy.
Things to Try¶
| Experiment | Parameter | Observable effect |
|---|---|---|
| Verify R⁴ dependency | Change target location |
SNR drops 12 dB per doubling of range |
| Vary RCS | Change rcs |
1 dBsm change → 1 dB SNR change |
| Multiple targets | Add dicts to targets list |
Additional peaks in range profile |
| Moving target | Set speed ≠ 0 |
Energy shifts to non-zero Doppler bin |
| Mesh vs point | Use STL model | Compare accuracy vs computation time |
Thanks for this informative section. I didnt quite follow why you add +30 to Pnoise. Could you kindly clarify
Convert dBW to dBm.