import radarsimpy
print("`RadarSimPy` used in this example is version: " + str(radarsimpy.__version__))
`RadarSimPy` used in this example is version: 15.2.0
Phase Noise Simulation¶
Demonstrates RadarSimPy's phase noise modeling for FMCW radar. Phase noise — random carrier phase variations — creates range shoulders around strong targets and reduces system dynamic range.
RadarSimPy accepts phase noise as two arrays passed to Transmitter:
pn_f: Frequency offset from carrier (Hz)pn_power: Single-sideband phase noise power density (dBc/Hz)
This example compares two identical 24 GHz radars — one with and one without phase noise — against two point targets of different RCS (70 dBsm and 40 dBsm) to visualize the dynamic range impact.
import numpy as np
import pandas as pd
import os
import plotly.graph_objs as go
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)))
# Load phase noise data from CSV file
data_path = os.path.join(os.getcwd(), "data", "phase_noise.csv")
df = pd.read_csv(data_path)
phase_noise_freq = df["Frequency"].to_numpy()
phase_noise_power = df["Power"].to_numpy()
Visualize Phase Noise Profile¶
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=phase_noise_freq / 1000000,
y=phase_noise_power,
name="Phase noise",
)
)
fig.update_layout(
title="Phase Noise Profile",
yaxis=dict(tickformat=".2f", title="Power density (dBc/Hz)"),
xaxis=dict(tickformat=".2f", title="Frequency offset (MHz)", type="log"),
)
show(fig)
Radar Configuration¶
Two identical Transmitter objects — tx_pn (with phase noise) and tx (baseline) — share the same waveform parameters. Pass pn_f / pn_power to enable phase noise modeling.
| Parameter | Value |
|---|---|
| Frequency sweep | 24.075–24.175 GHz (100 MHz BW) |
| Chirp duration | 80 μs, PRP 100 μs |
| TX power | 20 dBm |
| Pulses | 128 |
from radarsimpy import Radar, Transmitter, Receiver
tx_channel = dict(
location=(0, 0, 0),
)
tx_pn = Transmitter(
f=[24.125e9 - 50e6, 24.125e9 + 50e6],
t=80e-6,
tx_power=20,
prp=100e-6,
pulses=128,
pn_f=phase_noise_freq,
pn_power=phase_noise_power,
channels=[tx_channel],
)
tx = Transmitter(
f=[24.125e9 - 50e6, 24.125e9 + 50e6],
t=80e-6,
tx_power=20,
prp=100e-6,
pulses=128,
channels=[tx_channel],
)
Receiver¶
| Parameter | Value |
|---|---|
Sampling rate fs |
2 MHz |
| Noise figure | 12 dB |
| RF gain | 20 dB |
| Baseband gain | 30 dB |
| Load resistor | 500 Ω |
rx_channel = dict(
location=(0, 0, 0),
)
rx = Receiver(
fs=2e6,
noise_figure=12,
rf_gain=20,
load_resistor=500,
baseband_gain=30,
channels=[rx_channel],
)
Radar Systems and Targets¶
radar_pn = Radar(transmitter=tx_pn, receiver=rx)
radar = Radar(transmitter=tx, receiver=rx)
Two stationary point targets at different ranges and RCS values to reveal phase noise shoulder effects and dynamic range degradation.
target_1 = dict(location=(150, 20, 0), speed=(0, 0, 0), rcs=70, phase=0)
target_2 = dict(location=(80, -5, 0), speed=(0, 0, 0), rcs=40, phase=0)
targets = [target_1, target_2]
Simulation and Processing¶
Simulate both radars, then apply a Chebyshev-windowed range FFT (60 dB sidelobe suppression) to extract range profiles.
from radarsimpy.simulator import sim_radar
raw_data_pn = sim_radar(radar_pn, targets)
data_matrix_pn = raw_data_pn["baseband"] + raw_data_pn["noise"]
raw_data = sim_radar(radar, targets)
data_matrix = raw_data["baseband"] + raw_data["noise"]
Range Profile Comparison¶
from scipy import signal, constants
import radarsimpy.processing as proc
range_window = signal.windows.chebwin(radar.sample_prop["samples_per_pulse"], at=60)
range_profile_pn = proc.range_fft(data_matrix_pn, range_window)
range_profile = proc.range_fft(data_matrix, range_window)
Averaged range profile (mean across 128 chirps) with and without phase noise.
max_range = (
constants.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.mean(np.abs(range_profile_pn[0, :, :]), axis=0)),
mode="lines+markers",
name="With phase noise",
)
)
fig.add_trace(
go.Scatter(
x=range_axis,
y=20 * np.log10(np.mean(np.abs(range_profile[0, :, :]), axis=0)),
name="Without phase noise",
)
)
fig.update_layout(
title="Range Profile: Phase Noise Comparison",
yaxis=dict(tickformat=".2f", title="Amplitude (dB)"),
xaxis=dict(tickformat=".2f", title="Range (m)"),
)
show(fig)
Summary¶
Phase noise raises the spectral floor around each target peak, creating range shoulders that can mask weaker nearby targets. The stronger the target RCS, the wider and higher the shoulder.
Things to Try¶
| Experiment | Change | Observable effect |
|---|---|---|
| Better oscillator | Scale pn_power down |
Narrower, shallower shoulders |
| Closer weak target | Move target 2 closer to target 1 | Target 2 disappears under shoulder |
| More chirps | Increase pulses |
Shoulders average down with more coherent integration |
| Different frequency | Change f sweep |
Shoulder pattern shifts with carrier |
| Moving targets | Set speed ≠ 0 |
Phase noise also spreads Doppler bins |