Skip to content

rdm

Range-Doppler map (RDM) generation and plotting.

Provides the :func:gen entry point that simulates a full CPI — adding skin and jammer returns to a datacube, applying matched filtering, Doppler windowing, and the slow-time FFT — and a set of plot helpers for RTMs and RDMs.

gen(radar, waveform, return_list, seed=0, plot=True, debug=False, snr=False, window='chebyshev', window_kwargs=None)

Generate a Range-Doppler Map (RDM) for a single Coherent Processing Interval (CPI).

Simulates received radar data for one or more targets moving at constant range rates and processes it to produce an RDM, accounting for radar system parameters, waveform characteristics, and noise.

Parameters:

Name Type Description Default
radar Radar

Radar system parameters. See :class:rad_lab.pulse_doppler_radar.Radar for required keys and units.

required
waveform WaveformSample

WaveformSample created by a factory function (e.g. :func:rad_lab.waveform.lfm_waveform).

required
return_list list

List of :class:rad_lab.returns.Return objects, each describing one simulated target or jammer.

required
seed int

Random number generator seed for reproducibility. Defaults to 0.

0
plot bool

If True, plots the final RDM. Defaults to True.

True
debug bool

If True, plots intermediate processing steps and prints diagnostic statistics. Defaults to False.

False
snr bool

If True, output amplitudes are normalised to SNR (voltage ratio). If False, output amplitudes are in Volts. Defaults to False.

False
window str

Doppler window function applied before the slow-time FFT. One of "chebyshev" (default), "blackman-harris", "taylor", or "none" (rectangular, no windowing).

'chebyshev'
window_kwargs dict | None

Optional dict forwarded to the underlying scipy.signal.windows function. For example, window_kwargs={"at": 80} sets Chebyshev attenuation to 80 dB; window_kwargs={"nbar": 5, "sll": -35} tunes the Taylor window. See :func:._rdm_internals.create_window.

None

Returns:

Name Type Description
tuple tuple[ndarray, ndarray, ndarray, ndarray]

A four-element tuple (rdot_axis, r_axis, total_dc, signal_dc):

  • rdot_axis (np.ndarray): 1D range-rate (Doppler) axis [m/s].
  • r_axis (np.ndarray): 1D range axis [m].
  • total_dc (np.ndarray): 2D RDM including signal and noise, amplitude in Volts or SNR.
  • signal_dc (np.ndarray): 2D signal-only RDM, amplitude in Volts or SNR.
Source code in src/rad_lab/rdm.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def gen(
    radar: Radar,
    waveform: WaveformSample,
    return_list: list,
    seed: int = 0,
    plot: bool = True,
    debug: bool = False,
    snr: bool = False,
    window: str = "chebyshev",
    window_kwargs: dict | None = None,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Generate a Range-Doppler Map (RDM) for a single Coherent Processing Interval (CPI).

    Simulates received radar data for one or more targets moving at constant
    range rates and processes it to produce an RDM, accounting for radar system
    parameters, waveform characteristics, and noise.

    Args:
        radar: Radar system parameters. See
            :class:`rad_lab.pulse_doppler_radar.Radar` for required keys and units.
        waveform: WaveformSample created by a factory function
            (e.g. :func:`rad_lab.waveform.lfm_waveform`).
        return_list: List of :class:`rad_lab.returns.Return` objects, each
            describing one simulated target or jammer.
        seed: Random number generator seed for reproducibility. Defaults to 0.
        plot: If True, plots the final RDM. Defaults to True.
        debug: If True, plots intermediate processing steps and prints
            diagnostic statistics. Defaults to False.
        snr: If True, output amplitudes are normalised to SNR (voltage ratio).
            If False, output amplitudes are in Volts. Defaults to False.
        window: Doppler window function applied before the slow-time FFT.
            One of ``"chebyshev"`` (default), ``"blackman-harris"``,
            ``"taylor"``, or ``"none"`` (rectangular, no windowing).
        window_kwargs: Optional dict forwarded to the underlying
            ``scipy.signal.windows`` function. For example,
            ``window_kwargs={"at": 80}`` sets Chebyshev attenuation to
            80 dB; ``window_kwargs={"nbar": 5, "sll": -35}`` tunes the
            Taylor window. See :func:`._rdm_internals.create_window`.

    Returns:
        tuple: A four-element tuple ``(rdot_axis, r_axis, total_dc, signal_dc)``:

            - **rdot_axis** (*np.ndarray*): 1D range-rate (Doppler) axis [m/s].
            - **r_axis** (*np.ndarray*): 1D range axis [m].
            - **total_dc** (*np.ndarray*): 2D RDM including signal and noise,
              amplitude in Volts or SNR.
            - **signal_dc** (*np.ndarray*): 2D signal-only RDM, amplitude in
              Volts or SNR.
    """
    np.random.seed(seed)

    ########## Compute waveform and radar parameters ###############################################
    waveform.set_sample(radar.sample_rate)  # set the recorded sample

    ########## Create range axis for plotting ######################################################
    r_axis = range_axis(radar.sample_rate, number_range_bins(radar.sample_rate, radar.prf))

    ########## Return ##############################################################################
    signal_dc = data_cube(radar.sample_rate, radar.prf, radar.n_pulses)

    if snr:
        ### Direclty plot the RDM in SNR by way of the range equation ###
        # - The SNR is calculated at the initial range and does not change in time
        noise_dc = unity_variance_complex_noise(signal_dc.shape) / np.sqrt(radar.n_pulses)
    else:
        ### Determin scaling factors for max voltage ###
        rxVolt_noise = np.sqrt(
            c.RADAR_LOAD * noise_power(waveform.bw, radar.noise_factor, radar.op_temp)
        )
        noise_dc = np.random.uniform(low=-1, high=1, size=signal_dc.shape) * rxVolt_noise
    add_returns(signal_dc, waveform, return_list, radar, snr=snr)

    total_dc = signal_dc + noise_dc  # adding after return keeps clean signal_dc for plotting

    if debug:
        plot_rtm(r_axis, signal_dc, "Noiseless RTM: unprocessed")

    # list of datacubes to process in the following steps
    rdm_list = [signal_dc, total_dc]

    ########## Apply the match filter ##############################################################
    for dc in rdm_list:
        matchfilter(dc, waveform.pulse_sample, pedantic=False)

    if debug:
        plot_rtm(r_axis, signal_dc, "Noiseless RTM: match filtered")

    ########### Doppler process ####################################################################
    # First create filter window and apply it
    chwin_norm_mat = create_window(
        signal_dc.shape, window=window, window_kwargs=window_kwargs, plot=False
    )
    for dc in rdm_list:
        dc *= chwin_norm_mat

    # Doppler process datacubes
    for dc in rdm_list:
        f_axis, r_axis = doppler_process(dc, radar.sample_rate)

    ########## Plots and checks ####################################################################
    # calc rangeRate axis  #f = -2* fc/c Rdot -> Rdot = -c+f/ (2+fc)
    rdot_axis = -c.C * f_axis / (2 * radar.fcar)

    if debug:
        if snr:
            plot_rdm_snr(rdot_axis, r_axis, signal_dc, "Noiseless RDM", cbar_min=0)
            noise_checks(signal_dc, noise_dc, total_dc)
        else:
            plot_rdm(rdot_axis, r_axis, signal_dc, "Noiseless RDM")
    if plot or debug:
        if snr:
            plot_rdm_snr(
                rdot_axis, r_axis, total_dc, f"Total SNR RDM for {waveform.type}", cbar_min=0
            )
            # if debug:
            check_expected_snr(radar, return_list[0].target, waveform)  # first return item
        else:
            plot_rdm(rdot_axis, r_axis, total_dc, f"Total RDM for {waveform.type}")

    return rdot_axis, r_axis, total_dc, signal_dc

plot_rdm(rdot_axis, r_axis, data, title, cbar_min=-100, volt_to_dbm=True)

Plots a range-Doppler matrix (RDM).

The RDM shows radar data after pulse compression and Doppler processing.

Parameters:

Name Type Description Default
rdot_axis ndarray

1D array of range-rate values in m/s.

required
r_axis ndarray

1D array of range values in meters.

required
data ndarray

2D complex array representing the RDM.

required
title str

The title for the plot.

required
cbar_min float

The minimum value for the color bar. Defaults to -100.

-100
volt_to_dbm bool

If True, converts data from voltage to dBm for plotting. If False, plots power in Watts. Defaults to True.

True

Returns:

Type Description
tuple[Figure, Axes]

The figure and axes objects of the plot.

Source code in src/rad_lab/rdm.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def plot_rdm(
    rdot_axis: np.ndarray,
    r_axis: np.ndarray,
    data: np.ndarray,
    title: str,
    cbar_min: float = -100,
    volt_to_dbm: bool = True,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots a range-Doppler matrix (RDM).

    The RDM shows radar data after pulse compression and Doppler processing.

    Args:
        rdot_axis: 1D array of range-rate values in m/s.
        r_axis: 1D array of range values in meters.
        data: 2D complex array representing the RDM.
        title: The title for the plot.
        cbar_min: The minimum value for the color bar. Defaults to -100.
        volt_to_dbm: If True, converts data from voltage to dBm for plotting.
                       If False, plots power in Watts. Defaults to True.

    Returns:
        The figure and axes objects of the plot.
    """
    magnitude_data = np.abs(data)

    fig, ax = plt.subplots(1, 1)
    fig.suptitle(title)
    ax.set_xlabel("Range Rate [km/s]")
    ax.set_ylabel("Range [km]")

    if volt_to_dbm:
        zero_to_smallest_float(magnitude_data)
        # P_dBm = 10*log10(P_W / 1mW) = 10*log10((V^2/R) / 1e-3)
        plot_data = 20 * np.log10(magnitude_data / np.sqrt(1e-3 * c.RADAR_LOAD))
        cbar_label = "Power [dBm]"
    else:
        # P_W = V^2 / R
        plot_data = magnitude_data**2 / c.RADAR_LOAD
        cbar_label = "Power [W]"

    mesh = ax.pcolormesh(rdot_axis * 1e-3, r_axis * 1e-3, plot_data)
    mesh.set_clim(cbar_min, plot_data.max())
    cbar = fig.colorbar(mesh)
    cbar.set_label(cbar_label)

    fig.tight_layout()
    return fig, ax

plot_rdm_snr(rdot_axis, r_axis, data, title, cbar_min=0, volt_ratio_to_db=True)

Plots a range-Doppler matrix in terms of Signal-to-Noise Ratio (SNR).

Parameters:

Name Type Description Default
rdot_axis ndarray

1D array of range-rate values in m/s.

required
r_axis ndarray

1D array of range values in meters.

required
data ndarray

2D array representing the RDM with amplitudes as a linear SNR voltage ratio (i.e., S_voltage / N_voltage).

required
title str

The title for the plot.

required
cbar_min float

The minimum value for the color bar. Defaults to 0.

0
volt_ratio_to_db bool

If True, converts the SNR voltage ratio to dB. Defaults to True.

True

Returns:

Type Description
tuple[Figure, Axes]

The figure and axes objects of the plot.

Source code in src/rad_lab/rdm.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def plot_rdm_snr(
    rdot_axis: np.ndarray,
    r_axis: np.ndarray,
    data: np.ndarray,
    title: str,
    cbar_min: float = 0,
    volt_ratio_to_db: bool = True,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots a range-Doppler matrix in terms of Signal-to-Noise Ratio (SNR).

    Args:
        rdot_axis: 1D array of range-rate values in m/s.
        r_axis: 1D array of range values in meters.
        data: 2D array representing the RDM with amplitudes as a linear SNR
              voltage ratio (i.e., S_voltage / N_voltage).
        title: The title for the plot.
        cbar_min: The minimum value for the color bar. Defaults to 0.
        volt_ratio_to_db: If True, converts the SNR voltage ratio to dB.
                            Defaults to True.

    Returns:
        The figure and axes objects of the plot.
    """
    snr_voltage_ratio = np.abs(data)
    fig, ax = plt.subplots(1, 1)
    fig.suptitle(title)
    ax.set_xlabel("Range Rate [km/s]")
    ax.set_ylabel("Range [km]")

    if volt_ratio_to_db:
        zero_to_smallest_float(snr_voltage_ratio)
        plot_data = 20 * np.log10(snr_voltage_ratio)
        cbar_label = "SNR [dB]"
    else:
        plot_data = snr_voltage_ratio
        cbar_label = "SNR (Voltage Ratio)"

    mesh = ax.pcolormesh(rdot_axis * 1e-3, r_axis * 1e-3, plot_data)
    mesh.set_clim(cbar_min, plot_data.max())
    cbar = fig.colorbar(mesh)
    cbar.set_label(cbar_label)

    fig.tight_layout()
    return fig, ax

plot_rtm(r_axis, data, title)

Plots the magnitude and phase of a range-time matrix (RTM).

The RTM shows radar data before Doppler processing, with range on one axis and pulse number (slow-time) on the other.

Parameters:

Name Type Description Default
r_axis ndarray

1D array of range values in meters.

required
data ndarray

2D complex array representing the RTM, with shape (num_range_bins, num_pulses).

required
title str

The title for the plot.

required
Source code in src/rad_lab/rdm.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def plot_rtm(r_axis: np.ndarray, data: np.ndarray, title: str) -> None:
    """Plots the magnitude and phase of a range-time matrix (RTM).

    The RTM shows radar data before Doppler processing, with range on one
    axis and pulse number (slow-time) on the other.

    Args:
        r_axis: 1D array of range values in meters.
        data: 2D complex array representing the RTM, with shape
              (num_range_bins, num_pulses).
        title: The title for the plot.
    """
    pulses = range(data.shape[1])
    fig, (ax_mag, ax_phase) = plt.subplots(1, 2, figsize=(12, 5))
    fig.suptitle(title)

    mag_plot = ax_mag.pcolormesh(pulses, r_axis * 1e-3, np.abs(data))
    ax_mag.set_xlabel("Pulse Number")
    ax_mag.set_ylabel("Range [km]")
    ax_mag.set_title("Magnitude")
    fig.colorbar(mag_plot, ax=ax_mag)

    phase_plot = ax_phase.pcolormesh(pulses, r_axis * 1e-3, np.angle(data))
    ax_phase.set_xlabel("Pulse Number")
    ax_phase.set_ylabel("Range [km]")
    ax_phase.set_title("Phase")
    fig.colorbar(phase_plot, ax=ax_phase)

    fig.tight_layout()
    plt.show()