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 (Mesh Target)¶
Validates RadarSimPy's mesh-target simulation against theoretical link budget calculations using a 3D STL model of a trihedral corner reflector at 76.5 GHz.
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$ | 13.6 dBsm @ 76.5 GHz |
| 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 — moderate beamwidth (~80° @ −3 dB)
- Elevation: $\cos^{20}(\phi) + 12$ dB — narrower beamwidth (~40° @ −3 dB)
# Define antenna gain in dB
antenna_gain = 12
# Generate azimuth angle pattern from -80° to +80°
az_angle = np.arange(-80, 81, 1)
# Azimuth pattern: cos^4 gives a narrower beamwidth in horizontal plane
az_pattern = 20 * np.log10(np.cos(az_angle / 180 * np.pi) ** 4) + antenna_gain
# Generate elevation angle pattern from -80° to +80°
el_angle = np.arange(-80, 81, 1)
# Elevation pattern: cos^20 gives a much narrower beamwidth in vertical plane
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¶
# Define transmitter channel configuration
tx_channel = dict(
location=(0, 0, 0), # Position at origin (x, y, z) in meters
azimuth_angle=az_angle, # Angles for azimuth pattern
azimuth_pattern=az_pattern, # Gain pattern in azimuth (horizontal)
elevation_angle=el_angle, # Angles for elevation pattern
elevation_pattern=el_pattern, # Gain pattern in elevation (vertical)
)
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 | CPI for SNR integration |
# Create transmitter with FMCW (Frequency Modulated Continuous Wave) configuration
tx = Transmitter(
f=[76.3e9, 76.7e9], # Frequency sweep from 76.3 to 76.7 GHz (400 MHz bandwidth)
t=5.12e-05, # Pulse duration: 51.2 microseconds
tx_power=13, # Transmit power: 13 dBm (approximately 20 mW)
prp=5.5e-05, # Pulse Repetition Period: 55 microseconds
pulses=512, # Total number of pulses in the coherent processing interval
channels=[tx_channel], # Antenna channel configuration
)
Receiver Channel¶
# Define receiver channel configuration (same antenna pattern as transmitter)
rx_channel = dict(
location=(0, 0, 0), # Co-located with transmitter for monostatic radar
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 |
# Create receiver with specified noise and gain characteristics
rx = Receiver(
fs=20e6, # Sampling rate: 20 MHz (determines max unambiguous range)
noise_figure=11, # Noise figure: 11 dB (degrades SNR by this amount)
rf_gain=20, # RF amplifier gain: 20 dB
load_resistor=500, # Load resistor: 500 Ohms
baseband_gain=30, # Baseband amplifier gain: 30 dB
bb_type="real", # Real baseband (as opposed to complex I/Q)
channels=[rx_channel], # Antenna channel configuration
)
Radar System¶
radar = Radar(transmitter=tx, receiver=rx)
Target: Trihedral Corner Reflector¶
A trihedral corner reflector is a standard calibration target with stable, high RCS (13.6 dBsm @ 76.5 GHz). RadarSimPy computes RCS automatically from the 3D STL mesh geometry. Target is placed at 100 m, stationary.
See: Corner Reflector RCS
target_1 = {
"model": "../models/cr.stl",
"unit": "m",
"location": (100, 0, 0),
"speed": (0, 0, 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
# Run the radar simulation
# This simulates the electromagnetic interaction between the radar and targets
data = sim_radar(radar, targets)
# Extract simulation results
timestamp = data["timestamp"] # Time stamps for each sample
baseband = data["baseband"] + data["noise"] # Combined signal + noise
noise = data["noise"] # Noise-only data for SNR analysis
Range-Doppler Processing¶
Two sequential FFTs extract target information:
- Range FFT (fast-time): Beat frequency → target range
- Doppler FFT (slow-time): Phase shift across pulses → radial velocity
For this stationary target, energy concentrates at zero Doppler. The peak amplitude and mean noise floor are extracted per range bin to compute the simulated SNR.
import radarsimpy.processing as proc
# Perform Range-Doppler processing:
# 1. Range FFT: converts time-domain samples to range bins
# 2. Doppler FFT: converts pulse-to-pulse phase changes to velocity
range_doppler = np.fft.fftshift(
proc.range_doppler_fft(baseband), axes=1 # Shift zero-Doppler to center
)
# Process noise-only data for comparison
noise_range_doppler = np.fft.fftshift(proc.range_doppler_fft(noise), axes=1)
# Find peak signal in each range bin (across all Doppler bins)
max_per_range_bin = np.max(np.abs(range_doppler), axis=1)
# Calculate mean noise floor in each range bin
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 per Range Bin",
)
)
fig.add_trace(
go.Scatter(
x=range_axis,
y=20 * np.log10(noise_mean[0, 0:valid_range_bins]),
name="Noise Floor",
)
)
fig.add_annotation(
x=peak_range,
y=peak_val_db,
text=f"Simulated 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: 37.21 dB at 100.1 m
The peak at 100 m is the corner reflector. The gap between the signal peak and the noise floor is the simulated SNR, compared to theory below.
Compare Simulated SNR with Theory¶
Received power and noise power in dBm:
$$P_r = P_t + G_t + G_r + 20\log_{10}\!\left(\frac{\lambda}{(4\pi)^{1.5} R^2}\right) + \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 = 13.6
r = 100
# 1. Received Power (Pr)
# Pr (dBm) = Pt (dBm) + Gt (dB) + Gr (dB) + 20log10(lambda / ((4pi)^1.5 * R^2)) + sigma (dBsm)
path_loss_db = 20 * np.log10(lambda_val / ((4 * np.pi) ** 1.5 * r**2))
pr_dbm = pt_dbm + gt_db + gr_db + path_loss_db + sigma_db
print(f"Received Power (Pr): {pr_dbm:.2f} dBm")
# 2. Noise Power (Pnoise)
fs = 20e6
ns = 1024
np_pulses = 512
b_eff_db = 10 * np.log10(fs / (ns * np_pulses))
nf_db = 11
thermal_noise_dbm_hz = 10 * np.log10(k * T * 1000)
p_noise_dbm = nf_db + b_eff_db + thermal_noise_dbm_hz
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): -110.51 dBm Noise Power (Pnoise): -147.16 dBm Theoretical SNR: 36.65 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, mesh-based RCS computation, and system noise characteristics.
Summary¶
The simulated SNR matched the theoretical prediction, validating:
- Mesh-based RCS computation from the STL corner reflector model
- Correct free-space propagation and system noise modeling
range_doppler_fft()processing gain
Things to Try¶
| Experiment | Parameter | Observable effect |
|---|---|---|
| Verify R⁴ dependency | Change target location |
SNR drops 12 dB per doubling of range |
| Vary TX power | tx_power |
1 dB change → 1 dB SNR change |
| Different RCS | Replace STL model | SNR scales directly with σ (dBsm) |
| Moving target | Set speed ≠ 0 |
Energy shifts to non-zero Doppler bin |
| Change bandwidth | Adjust f sweep |
Affects range resolution ΔR = c/(2B) |