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 with Motion Planning¶
This example demonstrates how to simulate a moving radar platform using RadarSimPy's set_motion() API. Platform motion is common in automotive, UAV, and marine radar; the observed Doppler shift depends on the relative velocity between radar and target.
radar.set_motion() API¶
radar.set_motion(
location=(loc_x, loc_y, loc_z), # Time-varying position arrays
speed=(0, 0, 0), # MUST be (0,0,0) when using arrays
rotation=(yaw, pitch, roll), # Optional: time-varying orientation (rad)
rotation_rate=(0, 0, 0), # MUST be (0,0,0) when using rotation arrays
)
Position/rotation are sampled at every ADC sample via radar.time_prop["timestamp"] (shape: [frames×channels, pulses, samples]). Any trajectory expressible as a function of time is supported:
location_x = velocity * timestamps # Linear (constant velocity)
location_x = A * np.sin(2*np.pi*f*timestamps) # Sinusoidal (vibration)
location_x = r * np.cos(ω * timestamps) # Circular (rotating platform)
location_x = 0.5 * a * timestamps**2 # Accelerated
This Example¶
- Radar: 77 GHz FMCW, 100 MHz BW, 256 pulses
- Motion: Linear at −12 m/s along x-axis (moving away from target)
- Target: Stationary corner reflector at x = 50 m
- Expected: Positive Doppler (~12 m/s opening velocity) in Range-Doppler map
Create Radar Model¶
Import Required Modules¶
import numpy as np
import plotly.graph_objs as go
from plotly.subplots import make_subplots
from IPython.display import Image, display
from radarsimpy import Radar, Transmitter, Receiver
# 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¶
FMCW waveform structure:
| prp
| +----------+
| / /
| / /
| f[0] / /
| / --> f[1] /
| +----+
| t[0] t[1]
| Parameter | Value |
|---|---|
| Frequency | 76.95–77.05 GHz (100 MHz BW) |
| Chirp duration | 80 µs |
| TX power | 25 dBm |
| PRP | 100 µs |
| Pulses | 256 |
# Define transmitter channel at the origin
tx_channel = dict(
location=(0, 0, 0), # Initial position (will move according to motion plan)
)
# Create FMCW transmitter
tx = Transmitter(
f=[77e9 - 50e6, 77e9 + 50e6], # 76.95 to 77.05 GHz (100 MHz bandwidth)
t=[0, 80e-6], # 80 microsecond chirp duration
tx_power=25, # 25 dBm transmit power (~316 mW)
prp=100e-6, # 100 microsecond pulse repetition period
pulses=256, # 256 pulses for Doppler processing
channels=[tx_channel],
)
Receiver¶
| Parameter | Value |
|---|---|
| Sampling rate | 2 MHz |
| Noise figure | 8 dB |
| RF gain | 20 dB |
| Baseband gain | 30 dB |
| Load resistor | 500 Ω |
# Define receiver channel co-located with transmitter
rx_channel = dict(
location=(0, 0, 0), # Monostatic configuration (TX and RX at same location)
)
# Create receiver with specified noise and gain parameters
rx = Receiver(
fs=2e6, # 2 MHz sampling rate
noise_figure=8, # 8 dB noise figure
rf_gain=20, # 20 dB RF gain
load_resistor=500, # 500 Ohm load resistor
baseband_gain=30, # 30 dB baseband gain
channels=[rx_channel],
)
Define Platform Motion Trajectory¶
Trajectories are defined over the full timestamp array (shape [frames×channels, pulses, samples]) and then passed to set_motion().
Supported motion types:
| Type | Trajectory definition | Typical use |
|---|---|---|
| Linear | v * t |
Constant-velocity platform |
| Sinusoidal | A * sin(2πft) |
Vibration, oscillating motion |
| Circular | r * cos/sin(ωt) |
Rotating platform, orbit |
| Accelerated | ½at² |
Launch, braking |
| Rotating | Time-varying rotation array |
Antenna scanning |
Key constraints:
speedmust be(0, 0, 0)when using array-basedlocationrotation_ratemust be(0, 0, 0)when using array-basedrotation- Position units: meters; rotation units: radians
# Create radar system from transmitter and receiver
radar = Radar(transmitter=tx, receiver=rx)
# The timestamp array contains the time value for EACH SAMPLE (not just each pulse)
# This allows high-fidelity motion simulation where position is updated at every ADC sample
# You can use this to define ANY arbitrary motion trajectory as a function of time
timestamps = radar.time_prop["timestamp"]
# Note: timestamps.shape = (frames × channels, pulses, samples_per_pulse)
# Motion is evaluated at each of these timestamps for accurate simulation
# ============================================================================
# EXAMPLE MOTION PATTERNS - Uncomment any to try different trajectories
# ============================================================================
# Option 1: Linear motion at constant velocity (ACTIVE)
# Simple constant velocity motion along x-axis
location_x = -12 * timestamps # Moving at -12 m/s
location_y = 0 * timestamps # No y motion
location_z = 0 * timestamps # No z motion
# Option 2: Sinusoidal motion (UNCOMMENT TO TRY)
# Oscillating motion - useful for vibration/jitter analysis
# location_x = 10 * np.sin(2 * np.pi * timestamps) # Oscillate with 1 Hz
# location_y = 0 * timestamps
# location_z = 0 * timestamps
# Option 3: Circular motion (UNCOMMENT TO TRY)
# Radar moving in a circle - simulates rotating platform
# radius = 20
# angular_freq = 2 * np.pi * 0.5 # 0.5 Hz rotation
# location_x = 30 + radius * np.cos(angular_freq * timestamps)
# location_y = radius * np.sin(angular_freq * timestamps)
# location_z = 0 * timestamps
# Option 4: Accelerated motion (UNCOMMENT TO TRY)
# Non-uniform velocity - simulates vehicle acceleration
# acceleration = 5 # m/s^2
# location_x = 0.5 * acceleration * timestamps**2
# location_y = 0 * timestamps
# location_z = 0 * timestamps
# Option 5: Complex trajectory (UNCOMMENT TO TRY)
# Combine multiple components for realistic vehicle motion
# location_x = -12 * timestamps + 2 * np.sin(4 * np.pi * timestamps) # Linear + oscillation
# location_y = 3 * np.sin(2 * np.pi * timestamps) # Side-to-side motion
# location_z = 0 * timestamps
# Option 6: Motion with rotation (UNCOMMENT TO TRY)
# Radar translating and rotating - simulates vehicle turning
# location_x = -12 * timestamps
# location_y = 0 * timestamps
# location_z = 0 * timestamps
# rotation_yaw = 0.5 * timestamps # Yaw: rotating at 0.5 rad/s
# rotation_pitch = 0 * timestamps # Pitch: no pitching
# rotation_roll = 0 * timestamps # Roll: no rolling
# rotation = (rotation_yaw, rotation_pitch, rotation_roll) # Order: (yaw, pitch, roll)
# ============================================================================
# Apply motion plan to radar
# IMPORTANT: When using motion planning (time-varying location/rotation arrays),
# both speed and rotation_rate MUST be set to (0, 0, 0)
# All motion should be encoded in the location and rotation arrays
radar.set_motion(
location=(location_x, location_y, location_z),
speed=(0, 0, 0), # MUST be (0,0,0) when using motion planning arrays
# rotation=(rotation_yaw, rotation_pitch, rotation_roll), # Optional: add rotation (yaw, pitch, roll)
# rotation_rate=(0, 0, 0), # MUST be (0,0,0) when using motion planning arrays
)
# Note: With linear motion at -12 m/s in the negative x direction:
# - Radar starts at x=0 and moves in negative x direction (away from target at x=50m)
# - Target is stationary at x=50m (positive x direction)
# - This creates opening geometry, producing positive Doppler shift
Visualize Motion Geometry¶
Two 3D views:
- Left: Radar motion path only — start (green), end (red)
- Right: Radar path + target (orange diamond) + initial range line (dashed)
The visualization confirms the expected closing/opening geometry before simulation.
motion_x = location_x.flatten()
motion_y = location_y.flatten()
motion_z = location_z.flatten()
fig = make_subplots(
rows=1,
cols=2,
specs=[[{"type": "scene"}, {"type": "scene"}]],
subplot_titles=("Radar Motion Path", "Radar + Target Geometry"),
horizontal_spacing=0,
)
# Left: motion path only
fig.add_trace(
go.Scatter3d(x=motion_x, y=motion_y, z=motion_z,
mode="lines", line=dict(color="blue", width=3), name="Radar Path"),
row=1, col=1,
)
fig.add_trace(
go.Scatter3d(x=[motion_x[0]], y=[motion_y[0]], z=[motion_z[0]],
mode="markers", marker=dict(size=4, color="green"), name="Start"),
row=1, col=1,
)
fig.add_trace(
go.Scatter3d(x=[motion_x[-1]], y=[motion_y[-1]], z=[motion_z[-1]],
mode="markers", marker=dict(size=5, color="red"), name="End"),
row=1, col=1,
)
# Right: radar + target
fig.add_trace(
go.Scatter3d(x=[motion_x[0]], y=[motion_y[0]], z=[motion_z[0]],
mode="markers", marker=dict(size=4, color="green"), name="Start",
showlegend=False),
row=1, col=2,
)
fig.add_trace(
go.Scatter3d(x=[50], y=[0], z=[0],
mode="markers", marker=dict(size=5, color="orange", symbol="diamond"),
name="Target"),
row=1, col=2,
)
fig.add_trace(
go.Scatter3d(x=[motion_x[0], 50], y=[motion_y[0], 0], z=[motion_z[0], 0],
mode="lines", line=dict(color="gray", width=2, dash="dash"),
name="Initial Range", showlegend=False),
row=1, col=2,
)
fig.add_trace(
go.Scatter3d(x=[motion_x[-1]], y=[motion_y[-1]], z=[motion_z[-1]],
mode="markers", marker=dict(size=5, color="red"), name="End",
showlegend=False),
row=1, col=2,
)
camera = dict(eye=dict(x=2, y=2, z=1.5), center=dict(x=0, y=0, z=0), up=dict(x=0, y=0, z=1))
fig.update_layout(
title=dict(text="Radar Motion and Target Geometry", x=0.5, xanchor="center"),
scene=dict(xaxis=dict(title="X (m)"), yaxis=dict(title="Y (m)"),
zaxis=dict(title="Z (m)"), aspectmode="cube", camera=camera),
scene2=dict(xaxis=dict(title="X (m)"), yaxis=dict(title="Y (m)"),
zaxis=dict(title="Z (m)"), aspectmode="cube", camera=camera),
height=500,
legend=dict(x=0.5, y=-0.05, xanchor="center", orientation="h"),
margin=dict(l=0, r=0, b=0, t=80),
)
show(fig)
Try Different Motion Patterns¶
Update the trajectory code below and re-run to explore other motion types. The target remains stationary at x = 50 m.
Define Target¶
A corner reflector placed at x = 50 m — stationary, high RCS, ideal for Doppler validation.
# Define stationary corner reflector target
target_1 = {
"model": "../models/cr.stl", # 3D mesh model of trihedral corner reflector
"unit": "m", # Units for the model
"location": (50, 0, 0), # Fixed position at x=50m, y=0, z=0
"speed": (0, 0, 0), # Stationary target (no motion)
}
targets = [target_1]
# Geometry: Radar starts at origin (0,0,0) and moves in -x direction (away from target)
# Target stays at (50,0,0) in the positive x direction
# Result: Radar moves away from the target, creating positive range rate (opening)
Simulate Baseband Signals¶
Important:
level="sample"is required for motion planning simulations. It triggers per-sample position updates and correctly captures intra-pulse Doppler and phase evolution for arbitrary trajectories.
sim_radar output shape: [channels, pulses, samples_per_pulse] (complex baseband I/Q).
from radarsimpy.simulator import sim_radar
import time
# Run the simulation with timing
tic = time.time()
data = sim_radar(radar, targets, density=0.2, level="sample")
# Combine signal and noise for realistic baseband data
baseband = data["baseband"] + data["noise"]
toc = time.time()
print("Exec time:", toc - tic, "s")
Exec time: 38.62499785423279 s
Range-Doppler Processing¶
Range FFT resolves distance; Doppler FFT across pulses reveals radial velocity:
$$f_d = \frac{2v_r}{\lambda}$$
- Expected target: range ~50 m, radial velocity ~+12 m/s (radar moving away)
- Windowing: Hanning window applied per pulse to reduce range sidelobes; Hanning applied across pulses to reduce Doppler sidelobes
from scipy import signal
import radarsimpy.processing as proc
# Design windowing functions for sidelobe suppression
range_window = signal.windows.chebwin(radar.sample_prop["samples_per_pulse"], at=60)
doppler_window = signal.windows.chebwin(
radar.radar_prop["transmitter"].waveform_prop["pulses"], at=60
)
# Perform 2D FFT to generate Range-Doppler map
# rwin: window applied along range (fast-time) dimension
# dwin: window applied along Doppler (slow-time/pulse) dimension
range_doppler = proc.range_doppler_fft(baseband, rwin=range_window, dwin=doppler_window)
What to Observe¶
- Range peak near 50 m (target location)
- Doppler peak near +12 m/s (opening velocity — radar moving away)
- Doppler ambiguity: unambiguous velocity = $\lambda / (2 \cdot PRP)$; targets beyond this wrap around
- Sidelobe levels: Hanning windowing reduces peak but suppresses sidelobes
max_range = (
3e8
* radar.radar_prop["receiver"].bb_prop["fs"]
* radar.radar_prop["transmitter"].waveform_prop["pulse_length"]
/ radar.radar_prop["transmitter"].waveform_prop["bandwidth"]
/ 2
)
unambiguous_speed = (
3e8 / radar.radar_prop["transmitter"].waveform_prop["prp"][0] / 77e9 / 2
)
range_axis = np.linspace(0, max_range, radar.sample_prop["samples_per_pulse"], endpoint=False)
doppler_axis = np.linspace(
0, unambiguous_speed,
radar.radar_prop["transmitter"].waveform_prop["pulses"], endpoint=False,
)
fig = go.Figure()
fig.add_trace(
go.Surface(
x=range_axis,
y=doppler_axis,
z=20 * np.log10(np.abs(range_doppler[0, :, :])),
colorscale="Rainbow",
)
)
fig.update_layout(
title="Range-Doppler Map (Moving Radar Platform)",
height=600,
scene=dict(
xaxis=dict(title="Range (m)"),
yaxis=dict(title="Velocity (m/s)"),
zaxis=dict(title="Amplitude (dB)"),
),
margin=dict(l=0, r=0, b=60, t=100),
)
show(fig)
Summary¶
set_motion()with array-basedlocation/rotationenables arbitrary platform trajectories;level="sample"is required for accurate per-sample kinematics.- The relative radial velocity between platform and target determines the Doppler frequency; stationary targets appear at non-zero Doppler when the radar is moving.
Motion Types Quick Reference¶
| Type | Code snippet | Result |
|---|---|---|
| Linear | v * timestamps |
Constant Doppler |
| Sinusoidal | A * np.sin(2*np.pi*f*timestamps) |
Periodic Doppler modulation |
| Circular | r * np.cos(omega * timestamps) |
Sinusoidal Doppler vs. azimuth |
| Accelerated | 0.5 * a * timestamps**2 |
Linearly increasing Doppler |
| Rotating antenna | Array-based rotation |
Beam scanning effect |
Things to Try¶
| Experiment | Change to make | Observable effect |
|---|---|---|
| Increase velocity | velocity = -50 |
Larger Doppler shift |
| Sinusoidal motion | A * sin(2πft) trajectory |
Modulated Doppler in RD map |
| Closing target | Positive velocity toward target | Negative Doppler (closing velocity) |
| Higher bandwidth | bandwidth = 500e6 |
Finer range resolution |
| Slower chirp | Reduce PRP, increase pulses | Wider unambiguous velocity range |