Skip to content

SAR

SAR image generation and plotting (stripmap and spotlight modes).

Provides the :func:gen entry point that simulates a full synthetic aperture — transmitting pulses along a straight flight path, injecting point-target returns, range-compressing, and azimuth-focusing to produce a SAR image. Spotlight mode is activated by setting scene_center and beamwidth on the :class:~rad_lab.sar_radar.SarRadar instance.

gen(sar_radar, waveform, target_list, seed=0, plot=True, debug=False, window='chebyshev', window_kwargs=None, beam_pattern=None)

Generate a focused SAR image from point-target returns.

Simulates a SAR collection over a straight, level flight path. For each aperture position the function computes range and phase to every target, injects the waveform, then processes the datacube through range compression, azimuth windowing, and azimuth matched-filter focusing.

Stripmap mode (default): leave sar_radar.scene_center as None. Optionally set sar_radar.beamwidth to apply broadside beam-pattern weighting (body-fixed antenna).

Spotlight mode: set both fields on the :class:SarRadar instance. The antenna beam is steered toward scene_center each pulse, and target amplitudes are weighted by a two-way Gaussian beam pattern.

Parameters:

Name Type Description Default
sar_radar SarRadar

SAR system parameters. See :class:rad_lab.sar_radar.SarRadar.

required
waveform WaveformSample

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

required
target_list list[SarTarget]

List of :class:~rad_lab.sar_radar.SarTarget point scatterers.

required
seed int

Random number generator seed for reproducibility.

0
plot bool

If True, plots the focused SAR image.

True
debug bool

If True, plots intermediate processing steps (raw data, range-compressed data).

False
window str

Window function applied along the azimuth dimension before focusing. One of "chebyshev" (default), "blackman-harris", "taylor", or "none".

'chebyshev'
window_kwargs dict | None

Optional dict forwarded to the window function. See :func:._rdm_internals.create_window.

None
beam_pattern Callable[[ndarray], ndarray] | None

Optional callable that maps off-boresight angles [rad] to amplitude weights. Overrides the default Gaussian in spotlight mode. See :func:~rad_lab.uniform_linear_arrays.ula_pattern for a convenient way to build one from a ULA specification.

None

Returns:

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

(cross_range_axis, r_axis, focused_dc, signal_dc):

  • cross_range_axis (np.ndarray): 1-D cross-range axis [m].
  • r_axis (np.ndarray): 1-D slant-range axis [m].
  • focused_dc (np.ndarray): 2-D focused SAR image (signal + noise).
  • signal_dc (np.ndarray): 2-D signal-only focused image.
Source code in src/rad_lab/sar.py
 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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def gen(
    sar_radar: SarRadar,
    waveform: WaveformSample,
    target_list: list[SarTarget],
    seed: int = 0,
    plot: bool = True,
    debug: bool = False,
    window: str = "chebyshev",
    window_kwargs: dict | None = None,
    beam_pattern: Callable[[np.ndarray], np.ndarray] | None = None,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Generate a focused SAR image from point-target returns.

    Simulates a SAR collection over a straight, level flight path.  For each
    aperture position the function computes range and phase to every target,
    injects the waveform, then processes the datacube through range
    compression, azimuth windowing, and azimuth matched-filter focusing.

    **Stripmap** mode (default): leave ``sar_radar.scene_center`` as
    ``None``.  Optionally set ``sar_radar.beamwidth`` to apply
    broadside beam-pattern weighting (body-fixed antenna).

    **Spotlight** mode: set both fields on the :class:`SarRadar` instance.
    The antenna beam is steered toward ``scene_center`` each pulse, and
    target amplitudes are weighted by a two-way Gaussian beam pattern.

    Args:
        sar_radar: SAR system parameters.
            See :class:`rad_lab.sar_radar.SarRadar`.
        waveform: WaveformSample created by a factory function
            (e.g. :func:`rad_lab.waveform.lfm_waveform`).
        target_list: List of :class:`~rad_lab.sar_radar.SarTarget` point
            scatterers.
        seed: Random number generator seed for reproducibility.
        plot: If True, plots the focused SAR image.
        debug: If True, plots intermediate processing steps (raw data,
            range-compressed data).
        window: Window function applied along the azimuth dimension before
            focusing.  One of ``"chebyshev"`` (default),
            ``"blackman-harris"``, ``"taylor"``, or ``"none"``.
        window_kwargs: Optional dict forwarded to the window function.
            See :func:`._rdm_internals.create_window`.
        beam_pattern: Optional callable that maps off-boresight angles
            [rad] to amplitude weights.  Overrides the default Gaussian
            in spotlight mode.  See
            :func:`~rad_lab.uniform_linear_arrays.ula_pattern` for a
            convenient way to build one from a ULA specification.

    Returns:
        tuple: ``(cross_range_axis, r_axis, focused_dc, signal_dc)``:

            - **cross_range_axis** (*np.ndarray*): 1-D cross-range axis [m].
            - **r_axis** (*np.ndarray*): 1-D slant-range axis [m].
            - **focused_dc** (*np.ndarray*): 2-D focused SAR image (signal + noise).
            - **signal_dc** (*np.ndarray*): 2-D signal-only focused image.
    """
    np.random.seed(seed)

    ########## Waveform setup #####################################################################
    waveform.set_sample(sar_radar.sample_rate)

    ########## Flight path ########################################################################
    platform_positions = flight_path(
        sar_radar.n_pulses, sar_radar.pulse_spacing, sar_radar.platform_altitude
    )

    ########## Create datacube and range axis ######################################################
    n_range_bins = number_range_bins(sar_radar.sample_rate, sar_radar.prf)
    r_axis = range_axis(sar_radar.sample_rate, n_range_bins)

    signal_dc = data_cube(sar_radar.sample_rate, sar_radar.prf, sar_radar.n_pulses)

    ########## Populate with target returns ########################################################
    beam_weights_fn = None
    if sar_radar.scene_center is not None:
        # Spotlight: beam steered toward a fixed scene centre
        beam_weights_fn = partial(
            _beam_weights,
            platform_positions,
            scene_center=sar_radar.scene_center,
            beamwidth=sar_radar.beamwidth,
            pattern=beam_pattern,
        )
    elif sar_radar.beamwidth is not None:
        # Stripmap with beam weighting: body-fixed antenna pointing broadside
        def _stripmap_beam_fn(target_position: list[float]) -> np.ndarray:
            broadside = platform_positions.copy()
            broadside[:, 1] = target_position[1]  # target's cross-track distance
            broadside[:, 2] = 0.0
            return _beam_weights(
                platform_positions,
                target_position,
                scene_center=broadside,
                beamwidth=sar_radar.beamwidth,
                pattern=beam_pattern,
            )

        beam_weights_fn = _stripmap_beam_fn

    add_sar_returns(
        signal_dc, waveform, sar_radar, target_list, platform_positions, beam_weights_fn
    )

    ########## Add noise ##########################################################################
    rx_noise_volt = np.sqrt(
        c.RADAR_LOAD * noise_power(waveform.bw, sar_radar.noise_factor, sar_radar.op_temp)
    )
    noise_dc = np.random.uniform(low=-1, high=1, size=signal_dc.shape) * rx_noise_volt

    total_dc = signal_dc + noise_dc

    if debug:
        _plot_raw(r_axis, signal_dc, "Raw SAR data (noiseless)")

    # Process both signal-only and total datacubes
    dc_list = [signal_dc, total_dc]

    ########## Range compression ##################################################################
    for dc in dc_list:
        matchfilter(dc, waveform.pulse_sample, pedantic=False)

    if debug:
        _plot_raw(r_axis, signal_dc, "Range-compressed (noiseless)")

    ########## Azimuth windowing ##################################################################
    win_mat = create_window(
        signal_dc.shape, window=window, window_kwargs=window_kwargs, plot=False
    )
    for dc in dc_list:
        dc *= win_mat

    ########## Azimuth compression (focusing) #####################################################
    for dc in dc_list:
        cross_range_axis = azimuth_matched_filter(dc, sar_radar, r_axis)

    ########## Plot ###############################################################################
    if plot or debug:
        plot_sar_image(cross_range_axis, r_axis, total_dc, "Focused SAR Image")

    return cross_range_axis, r_axis, total_dc, signal_dc

plot_sar_image(cross_range_axis, r_axis, data, title, cbar_min=-40)

Plots a focused SAR image in dB.

Parameters:

Name Type Description Default
cross_range_axis ndarray

1-D cross-range axis [m].

required
r_axis ndarray

1-D slant-range axis [m] (converted to km for display).

required
data ndarray

2-D complex SAR image.

required
title str

Plot title.

required
cbar_min float

Minimum colorbar value [dB].

-40

Returns:

Type Description
tuple[Figure, Axes]

The figure and axes objects.

Source code in src/rad_lab/sar.py
171
172
173
174
175
176
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
def plot_sar_image(
    cross_range_axis: np.ndarray,
    r_axis: np.ndarray,
    data: np.ndarray,
    title: str,
    cbar_min: float = -40,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots a focused SAR image in dB.

    Args:
        cross_range_axis: 1-D cross-range axis [m].
        r_axis: 1-D slant-range axis [m] (converted to km for display).
        data: 2-D complex SAR image.
        title: Plot title.
        cbar_min: Minimum colorbar value [dB].

    Returns:
        The figure and axes objects.
    """
    magnitude = np.abs(data)
    zero_to_smallest_float(magnitude)
    plot_data = 20 * np.log10(magnitude / magnitude.max())

    fig, ax = plt.subplots(1, 1)
    fig.suptitle(title)
    ax.set_xlabel("Cross-Range [m]")
    ax.set_ylabel("Slant Range [km]")

    mesh = ax.pcolormesh(cross_range_axis, r_axis / 1e3, plot_data)
    mesh.set_clim(cbar_min, 0)
    cbar = fig.colorbar(mesh)
    cbar.set_label("Normalised Magnitude [dB]")

    fig.tight_layout()
    return fig, ax