import radarsimpy
print("`RadarSimPy` used in this example is version: " + str(radarsimpy.__version__))
`RadarSimPy` used in this example is version: 15.2.0
Pulsed Radar: Range Measurement with Matched Filtering¶
Introduction¶
Pulsed radar transmits short, high-power bursts and measures echo time delay to determine target range — the foundation of air traffic control, weather monitoring, and surveillance systems.
Range from round-trip time delay:
$$R = \frac{c \cdot \tau}{2}, \qquad \Delta R = \frac{c \cdot T_p}{2}, \qquad R_{max} = \frac{c}{2 \cdot PRF}$$
Matched filter maximises SNR by correlating the received signal with a replica of the transmitted pulse:
$$y(t) = \int x(\tau) \cdot h^*(t-\tau)\, d\tau, \qquad G = B \cdot T_p$$
Non-coherent pulse integration of $N$ pulses adds ~$10\log_{10}\sqrt{N}$ dB SNR by summing pulse magnitudes.
This Example¶
Uses RadarSimPy to simulate a 10-pulse X-band pulsed radar with matched filtering:
- Carrier: 10 GHz, PRF ≈ 12.5 kHz, PRP ≈ 80 μs, duty cycle ≈ 0.4%
- Range resolution: 50 m ($T_p \approx 333$ ns, $B = 3$ MHz)
- Max unambiguous range: 12 km, peak power 67 dBm (5 kW), 20 dBi antenna
- Targets: two stationary point targets at 8 km and 14 km range
- Processing: matched filtering + non-coherent integration (10 pulses, ~5 dB gain)
Radar System Configuration¶
import numpy as np
import plotly.graph_objs as go
from IPython.display import Image, display
from radarsimpy import Radar, Transmitter, Receiver
from radarsimpy.simulator import sim_radar
from scipy import constants, signal
INTERACTIVE = False
def show(fig):
if INTERACTIVE:
fig.show()
else:
display(Image(fig.to_image(format="jpg", scale=2)))
Antenna Pattern Definition¶
| Parameter | Value | Notes |
|---|---|---|
| Gain | 20 dBi | High-gain directional |
| Azimuth pattern | cos⁵⁰⁰ | ~2° beamwidth @ −3 dB |
| Elevation pattern | cos⁴⁰⁰ | ~2.5° beamwidth @ −3 dB |
| Angular coverage | ±20° | Pencil beam |
# Define antenna gain
antenna_gain = 20 # dBi - High-gain directional antenna
### Azimuth Pattern (Horizontal Plane) ###
# Define azimuth angle array: -20° to +20° in 1° steps
az_angle = np.arange(-20, 21, 1) # 41 points
# Azimuth pattern: cos^500 taper with 20 dBi gain
# Very narrow beamwidth (~2° @ -3dB) with low sidelobes
az_pattern = 20 * np.log10(np.cos(az_angle / 180 * np.pi) ** 500) + antenna_gain # dB
### Elevation Pattern (Vertical Plane) ###
# Define elevation angle array: -20° to +20° in 1° steps
el_angle = np.arange(-20, 21, 1) # 41 points
# Elevation pattern: cos^400 taper with 20 dBi gain
# Narrow beamwidth (~2.5° @ -3dB)
el_pattern = 20 * np.log10((np.cos(el_angle / 180 * np.pi)) ** 400) + antenna_gain # dB
Visualize Antenna Patterns¶
Display azimuth and elevation radiation patterns showing narrow beamwidth.
fig = go.Figure()
fig.add_trace(go.Scatter(x=az_angle, y=az_pattern, name="Azimuth (horizontal)", line=dict(color="blue", width=2)))
fig.add_trace(go.Scatter(x=el_angle, y=el_pattern, name="Elevation (vertical)", line=dict(color="red", width=2)))
fig.update_layout(
title="Antenna Radiation Patterns: Narrow Pencil Beam (20 dBi)",
yaxis=dict(title="Gain (dB)", range=[-90, 22], gridcolor="lightgray"),
xaxis=dict(title="Angle (degrees)", range=[-90, 90], gridcolor="lightgray"),
height=500,
legend=dict(x=0.02, y=0.98),
)
show(fig)
Waveform Parameter Calculation¶
From $R_{max} = 12000$ m and $\Delta R = 50$ m:
$$B = \frac{c}{2\Delta R} = 3 \text{ MHz}, \quad T_p = \frac{1}{B} \approx 333 \text{ ns}$$
$$PRF = \frac{c}{2R_{max}} \approx 12.5 \text{ kHz}, \quad PRP \approx 80 \text{ μs}, \quad \text{duty cycle} \approx 0.4\%$$
10 pulses for non-coherent integration → ~5 dB gain ($10\log_{10}\sqrt{10}$).
light_speed = constants.c
# Performance Requirements
max_range = 12000 # Maximum unambiguous range: 12,000 m
range_res = 50 # Required range resolution: 50 m
# Derived Waveform Parameters
pulse_bw = light_speed / (2 * range_res) # 3 MHz
pulse_width = 1 / pulse_bw # ~333 ns
prf = light_speed / (2 * max_range) # ~12.5 kHz
prp = 1 / prf # ~80 μs
# Radar Parameters
fs = 6e6 # Sampling rate: 6 MHz
fc = 10e9 # Carrier frequency: 10 GHz
num_pulse = 10 # Pulses for integration
# Sample counts
total_samples = int(prp * num_pulse * fs)
sample_per_pulse = int(total_samples / num_pulse)
# Rectangular pulse envelope
mod_t = np.arange(0, total_samples) / fs
amp = np.zeros_like(mod_t)
amp[mod_t <= pulse_width] = 1
# TX channel with antenna pattern and pulse modulation
tx_channel = dict(
location=(0, 0, 0),
azimuth_angle=az_angle, azimuth_pattern=az_pattern,
elevation_angle=el_angle, elevation_pattern=el_pattern,
amp=amp, mod_t=mod_t,
)
tx = Transmitter(
f=fc,
t=1 / prf,
tx_power=67, # 67 dBm = 5 kW peak
pulses=num_pulse,
channels=[tx_channel],
)
print(f"Pulse Width: {pulse_width*1e9:.1f} ns | PRF: {prf/1e3:.2f} kHz | Duty: {pulse_width/prp*100:.2f}%")
Pulsed Radar Waveform Parameters: Pulse Width: 333.56 ns Pulse Bandwidth: 3.0 MHz PRF: 12.5 kHz PRP: 80.06 μs Duty Cycle: 0.42% Samples per Pulse: 480 Number of Pulses: 10
Visualize Transmit Pulse¶
Display the rectangular pulse envelope showing the short pulse duration.
np.shape(mod_t)
(4803,)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=mod_t[0:100] * 1e9, y=amp[0:100],
name="Transmit Pulse",
line=dict(color="green", width=2),
fill="tozeroy", fillcolor="rgba(0,255,0,0.3)",
))
fig.update_layout(
title=f"Rectangular Transmit Pulse: {pulse_width*1e9:.0f} ns",
yaxis=dict(title="Amplitude (normalized)", gridcolor="lightgray"),
xaxis=dict(title="Time (ns)", gridcolor="lightgray"),
height=400,
)
show(fig)
Receiver Configuration¶
| Parameter | Value |
|---|---|
| Sampling rate | 6 MHz |
| Noise figure | 12 dB |
| RF gain | 20 dB |
| Baseband gain | 30 dB |
| Load resistor | 500 Ω |
Monostatic configuration — same antenna pattern as TX.
# Define receiver channel with same antenna pattern (monostatic)
rx_channel = dict(
location=(0, 0, 0), # Receiver position at origin (same as TX)
azimuth_angle=az_angle, # Same azimuth pattern as TX
azimuth_pattern=az_pattern, # Same azimuth gain
elevation_angle=el_angle, # Same elevation pattern
elevation_pattern=el_pattern, # Same elevation gain
)
Create Radar Receiver¶
Define receiver with sampling and gain parameters.
# Create pulsed radar receiver
rx = Receiver(
fs=fs, # Sampling rate: 6 MHz
noise_figure=12, # Noise figure: 12 dB
rf_gain=20, # RF gain: 20 dB (LNA)
load_resistor=500, # Load resistance: 500 Ω
baseband_gain=30, # Baseband gain: 30 dB
channels=[rx_channel], # Receiver antenna configuration
)
Create Radar System¶
Combine transmitter and receiver to form the complete pulsed radar.
# Create complete pulsed radar system
radar = Radar(transmitter=tx, receiver=rx)
Target Configuration¶
Define two stationary targets at different ranges to test range resolution.
Target Parameters¶
Target Placement Strategy:
Two targets are placed 1000m apart to demonstrate:
- Clear separation: 1000m spacing >> 50m resolution
- Different ranges: 2000m and 3000m (both within 5km max range)
- Stationary targets: Zero velocity (no Doppler)
Target 1:
- Location: (2000, 0, 0) m → 2 km range
- Velocity: (0, 0, 0) m/s → Stationary
- RCS: 10 dBsm → Moderate reflector (small aircraft)
- Phase: 0°
Target 2:
- Location: (3000, 0, 0) m → 3 km range
- Velocity: (0, 0, 0) m/s → Stationary
- RCS: 10 dBsm → Moderate reflector (same as target 1)
- Phase: 0°
Expected Behavior:
With 50m range resolution, the 1000m spacing should clearly separate these targets in the range profile.
# Configure Target 1: 2 km range, stationary
target_1 = dict(
location=(14000, 0, 0), # Position: 2000m range
speed=(0, 0, 0), # Velocity: stationary
rcs=10, # Radar cross section: 10 dBsm (small aircraft)
phase=0, # Initial phase: 0 degrees
)
# Configure Target 2: 3 km range, stationary
target_2 = dict(
location=(8000, 0, 0), # Position: 3000m range (1000m spacing from target 1)
speed=(0, 0, 0), # Velocity: stationary
rcs=10, # Radar cross section: 10 dBsm (same as target 1)
phase=0, # Initial phase: 0 degrees
)
# Combine targets for simulation
targets = [target_1, target_2]
Simulate Baseband Signals¶
Generate pulsed radar baseband signals for 10 pulses with two target returns.
Simulation Process¶
The simulator performs:
- Pulse Transmission: 10 high-power rectangular pulses
- Propagation: Calculate time delays to each target
- Reflection: Apply RCS and antenna patterns
- Reception: Down-convert to baseband I/Q
- Noise: Add thermal noise from receiver
Output Data Structure:
- Dimensions: [channels, pulses, samples]
- Channels: 1 (single TX/RX)
- Pulses: 10
- Samples: ~200 per pulse (6 MHz × 33.3 μs)
Baseband Signal:
Complex I/Q data containing:
- Target echoes at appropriate time delays
- Receiver thermal noise
- Pulse envelope shape
# Import radar simulator
from radarsimpy.simulator import sim_radar
# Simulate pulsed radar with two stationary targets
data = sim_radar(radar, targets)
# Extract simulation results
timestamp = data["timestamp"] # Time array [1, 10, samples]
baseband = data["baseband"] # Complex I/Q + noise [1, 10, samples]
print(f"Simulation complete:")
print(f" Baseband shape: {baseband.shape}")
print(f" Number of pulses: {baseband.shape[1]}")
print(f" Samples per pulse: {baseband.shape[2]}")
Simulation complete: Baseband shape: (1, 10, 480) Number of pulses: 10 Samples per pulse: 480
Visualize Baseband Signals¶
Display time-domain I/Q signals showing pulse returns and noise.
# Create figure for baseband I/Q visualization
fig = go.Figure()
# Plot In-phase (I) component
fig.add_trace(
go.Scatter(
x=timestamp[0, 1, :] * 1e6, # Convert to microseconds
y=np.real(baseband[0, 1, :]), # Real part (I channel)
name="I (In-phase)",
line=dict(color="blue", width=1),
)
)
# Plot Quadrature (Q) component
fig.add_trace(
go.Scatter(
x=timestamp[0, 1, :] * 1e6, # Convert to microseconds
y=np.imag(baseband[0, 1, :]), # Imaginary part (Q channel)
name="Q (Quadrature)",
line=dict(color="red", width=1),
)
)
# Configure plot layout
fig.update_layout(
title="Baseband I/Q Signals: Pulse Returns from Two Targets (First Pulse)",
yaxis=dict(title="Amplitude (V)", gridcolor="lightgray"),
xaxis=dict(title="Time (μs)", gridcolor="lightgray"),
height=500,
legend=dict(x=0.02, y=0.98),
)
# uncomment this to display interactive plot
# fig.show()
# display static image to reduce size on radarsimx.com
img_bytes = fig.to_image(format="jpg", scale=2)
display(Image(img_bytes))
Radar Signal Processing¶
Apply matched filtering and pulse integration to extract range profile.
Matched Filter Processing¶
Matched Filter Theory:
The matched filter is the optimal linear filter for maximizing SNR in the presence of additive white Gaussian noise. It correlates the received signal with a replica of the transmitted pulse:
$$y(t) = \int_{-\infty}^{\infty} x(\tau) \cdot h^*(t-\tau) d\tau$$
Where:
- $x(t)$ = Received signal (baseband with target echoes + noise)
- $h(t)$ = Transmitted pulse replica (time-reversed and conjugated)
- $y(t)$ = Matched filter output (compressed pulse)
Processing Gain:
Matched filtering provides SNR improvement. For a simple rectangular pulse where $B \cdot T_p \approx 1$, there is no pulse compression gain (unlike LFM chirps), but the matched filter is still the optimal linear detector for maximizing SNR in white noise by integrating the signal energy over the pulse duration.
Implementation:
In discrete form, matched filtering is implemented as convolution with the transmitted pulse sequence.
# Import signal processing module
from scipy import signal
### Apply Matched Filter to Each Pulse ###
# Extract matching coefficients (transmitted pulse shape)
# Use only the non-zero portion of the amplitude array
matchingcoeff = amp[amp != 0] # Pulse replica for matched filtering
# Initialize range profile storage [1, 10 pulses, samples]
range_profile = np.zeros_like(baseband, dtype=np.complex128)
# Apply matched filter to each pulse independently
for p_idx in range(0, num_pulse):
# Convolve received signal with pulse replica
# mode='same' keeps output length same as input
range_profile[0, p_idx, :] = signal.convolve(
baseband[0, p_idx, :], # Received baseband for this pulse
matchingcoeff, # Transmitted pulse (matched filter)
mode="same", # Keep same length as input
)
print(f"Matched filtering complete:")
print(f" Matched filter length: {len(matchingcoeff)} samples")
print(f" Matched Filter SNR Improvement: ~{10*np.log10(len(matchingcoeff)):.1f} dB")
Matched filtering complete: Matched filter length: 3 samples Matched Filter SNR Improvement: ~4.8 dB
Non-Coherent Pulse Integration¶
Integrate multiple pulses to improve SNR through non-coherent summation.
Non-Coherent Integration:
Sum the magnitude (envelope) of matched-filtered pulses:
$$y_{int}[n] = \sum_{p=1}^{N} |y_p[n]|$$
Where:
- $N$ = Number of pulses (10 in this example)
- $y_p[n]$ = Matched filter output for pulse $p$
- $|y_p[n]|$ = Magnitude (envelope)
SNR Improvement:
Non-coherent integration provides:
$$G_{int} = \sqrt{N} = \sqrt{10} \approx 3.16 = 5 \text{ dB}$$
This is less than coherent integration (10 dB for 10 pulses) but doesn't require phase coherence.
Why Non-Coherent?
For stationary targets, coherent integration would be optimal. Non-coherent is used here for simplicity and to demonstrate the technique used when phase coherence is unavailable.
# Perform non-coherent integration across 10 pulses
# Sum the magnitude (envelope) of matched-filtered pulses
range_profile_integrated = np.sum(np.abs(range_profile[0, :, :]), axis=0)
print(f"Pulse integration complete:")
print(f" Number of pulses integrated: {num_pulse}")
print(f" Integration gain: ~{10*np.log10(np.sqrt(num_pulse)):.1f} dB")
Pulse integration complete: Number of pulses integrated: 10 Integration gain: ~5.0 dB
Visualize Range Profile¶
Display the final range profile showing detected targets at their ranges.
Interpretation:
- X-axis: Range (m) → Distance from radar
- Y-axis: Amplitude (dB) → Detection strength
- Peaks: Two targets at ~2000m and ~3000m
- Noise Floor: Background noise level
Processing Chain Summary:
- Matched Filter: Optimal SNR improvement
- Pulse Integration: 5 dB integration gain
- Total: Significant SNR improvement
This enables detection of weak targets that would be buried in noise without processing.
# Create range axis from time (convert to distance)
range_axis = mod_t * light_speed / 2 # Range = c*t/2
# Create figure for range profile
fig = go.Figure()
# Plot integrated range profile
fig.add_trace(
go.Scatter(
x=range_axis, # Range axis (m)
y=range_profile_integrated, # Amplitude (dB)
name="Integrated Range Profile",
line=dict(color="purple", width=2),
)
)
# Configure plot layout
fig.update_layout(
title="Range Profile: Two Targets Clearly Resolved (50m Resolution)",
yaxis=dict(title="Amplitude (dB)", gridcolor="lightgray"),
xaxis=dict(title="Range (m)", gridcolor="lightgray", range=[0, 12000]),
height=600,
legend=dict(x=0.02, y=0.98),
)
# uncomment this to display interactive plot
# fig.show()
# display static image to reduce size on radarsimx.com
img_bytes = fig.to_image(format="jpg", scale=2)
display(Image(img_bytes))
Summary¶
This notebook demonstrated a 10-pulse X-band pulsed radar simulation with matched filtering and non-coherent integration using RadarSimPy:
- Calculated waveform parameters from range/resolution requirements ($PRF$, $T_p$, duty cycle)
- Defined a narrow pencil-beam antenna (20 dBi, cos⁵⁰⁰/cos⁴⁰⁰ taper)
- Applied matched filtering to each pulse for optimal SNR
- Performed non-coherent integration across 10 pulses (~5 dB gain)
- Visualised the final range profile with resolved target peaks
Things to Try¶
| Experiment | Change | Expected Effect |
|---|---|---|
| Range resolution | Decrease range_res to 10 m |
Narrower pulse, higher bandwidth |
| Max range | Increase max_range to 50 km |
Lower PRF, more samples per pulse |
| Pulse count | Set num_pulse to 50 |
Higher integration gain (~7 dB) |
| Peak power | Change tx_power to 50 dBm |
Lower SNR, weaker peaks |
| Antenna beamwidth | Reduce cosine exponent (e.g., cos¹⁰⁰) | Wider beam, lower gain |
| Target RCS | Change rcs to 0 dBsm |
Weaker return, test detection threshold |