Skip to content

Uniform Linear Arrays

Uniform linear array (ULA) gain patterns and steering vectors.

Provides functions to compute element steering vectors, apply inter-element time delays, and plot one-way and two-way array gain patterns as a function of angle.

apply_timeshift_due_to_element_position(signal_ar, fs, element_position, tgt_angle)

Applies a time shift to a signal based on element position and angle.

This function models the delay or advance a signal experiences when arriving at an off-center antenna element from a specific target angle. The time shift is applied by resampling the signal using cubic interpolation.

Parameters:

Name Type Description Default
signal_ar ndarray

A complex time-series signal from a single antenna element.

required
fs float

The sampling frequency of the signal in Hertz.

required
element_position float

The element's position relative to the array's phase center in meters.

required
tgt_angle float

The angle of the target in degrees, where 0 is broadside.

required

Returns:

Type Description
ndarray

np.ndarray: The time-shifted complex signal.

Source code in src/rad_lab/uniform_linear_arrays.py
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
def apply_timeshift_due_to_element_position(
    signal_ar: np.ndarray, fs: float, element_position: float, tgt_angle: float
) -> np.ndarray:
    """
    Applies a time shift to a signal based on element position and angle.

    This function models the delay or advance a signal experiences when arriving
    at an off-center antenna element from a specific target angle. The time
    shift is applied by resampling the signal using cubic interpolation.

    Args:
        signal_ar (np.ndarray): A complex time-series signal from a single
            antenna element.
        fs (float): The sampling frequency of the signal in Hertz.
        element_position (float): The element's position relative to the array's
            phase center in meters.
        tgt_angle (float): The angle of the target in degrees, where 0 is
            broadside.

    Returns:
        np.ndarray: The time-shifted complex signal.
    """
    range_diff = element_position * np.sin(np.deg2rad(tgt_angle))
    time_shift = range_diff / c.C
    time_ar = np.arange(len(signal_ar)) / fs
    shifted_time = time_ar + time_shift
    interp_fun = interpolate.interp1d(time_ar, signal_ar, kind="cubic", fill_value="extrapolate")
    shifted_signal = interp_fun(shifted_time)

    return shifted_signal

array_phase_center(position_ar, weight_ar)

Calculates the phase center of an antenna array.

The phase center is the apparent point from which the radiation emanates. It is calculated as the weighted average of the element positions.

Parameters:

Name Type Description Default
position_ar ndarray

1D array of element positions. The unit of the output will match the unit of this input (e.g., meters).

required
weight_ar ndarray

A vector of complex weights for each element. The magnitude of the weights is used in the calculation.

required

Returns:

Name Type Description
float float

The position of the array's phase center.

Source code in src/rad_lab/uniform_linear_arrays.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def array_phase_center(position_ar: np.ndarray, weight_ar: np.ndarray) -> float:
    """
    Calculates the phase center of an antenna array.

    The phase center is the apparent point from which the radiation emanates.
    It is calculated as the weighted average of the element positions.

    Args:
        position_ar (np.ndarray): 1D array of element positions. The unit of
            the output will match the unit of this input (e.g., meters).
        weight_ar (np.ndarray): A vector of complex weights for each element.
            The magnitude of the weights is used in the calculation.

    Returns:
        float: The position of the array's phase center.
    """
    assert len(position_ar) == len(weight_ar)
    # unsure if the weight should be abs value or not
    return np.sum(abs(weight_ar) * position_ar) / np.sum(weight_ar)

linear_antenna_gain(el_pos, weight_vec=None, N_theta=10000, steer_angle=0, plot=False)

Calculates the complex voltage gain pattern for a linear antenna array.

Parameters:

Name Type Description Default
el_pos ndarray

1D array of element positions, normalized by the signal wavelength.

required
weight_vec ndarray

A vector of complex weights for each antenna element. If None, uniform weights (all ones) are used. Defaults to None.

None
N_theta int

The number of angular points to calculate the gain over. Defaults to 10000.

10000
steer_angle float

The angle in degrees at which to steer the main beam. 0 degrees is broadside. Defaults to 0.

0
plot bool

If True, plots the gain pattern in dBi. Defaults to False.

False

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - theta_vec (np.ndarray): The grid of angles in degrees, from -90 to 90. - gain_vec (np.ndarray): The complex voltage gain at each angle in theta_vec.

Source code in src/rad_lab/uniform_linear_arrays.py
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
def linear_antenna_gain(
    el_pos: np.ndarray,
    weight_vec: np.ndarray | None = None,
    N_theta: int = 10000,
    steer_angle: float = 0,
    plot: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculates the complex voltage gain pattern for a linear antenna array.

    Args:
        el_pos (np.ndarray): 1D array of element positions, normalized by the
                             signal wavelength.
        weight_vec (np.ndarray, optional): A vector of complex weights for each
            antenna element. If None, uniform weights (all ones) are used.
            Defaults to None.
        N_theta (int, optional): The number of angular points to calculate the
            gain over. Defaults to 10000.
        steer_angle (float, optional): The angle in degrees at which to steer
            the main beam. 0 degrees is broadside. Defaults to 0.
        plot (bool, optional): If True, plots the gain pattern in dBi.
            Defaults to False.

    Returns:
        tuple[np.ndarray, np.ndarray]:
            - theta_vec (np.ndarray): The grid of angles in degrees, from -90 to 90.
            - gain_vec (np.ndarray): The complex voltage gain at each angle in `theta_vec`.
    """
    if weight_vec is None:
        weight_vec = np.ones(len(el_pos)).T

    theta_grid = np.linspace(-np.pi / 2, np.pi / 2, N_theta)

    steer_vec = steering_vector(el_pos, steer_angle)
    weight_vec = weight_vec * steer_vec

    A = np.exp(1j * 2 * np.pi * np.outer(np.sin(theta_grid), el_pos))
    Af = A @ weight_vec

    # The difference between the antenna's gain and the isotropic antenna's gain is dBi
    af_dbi = 20 * np.log10(abs(Af))

    if plot:
        plt.figure()
        plt.plot(np.rad2deg(theta_grid), af_dbi)
        plt.ylim((-30, af_dbi.max() * 1.2))
        plt.xlabel(r"Angle $\theta$ [deg]")
        plt.ylabel("Gain [dBi]")
        plt.grid()

    return np.rad2deg(theta_grid), Af

linear_antenna_gain_N_db(N_el, dx, weight_vec=None, N_theta=10000, steer_angle=0, plot=False)

Calculates gain pattern in dBi for a uniform linear array.

This function defines the array geometry based on the number of elements and their uniform spacing. It then computes and returns the gain in dBi.

Parameters:

Name Type Description Default
N_el int

The number of antenna array elements.

required
dx float

The spacing between elements, normalized by signal wavelength.

required
weight_vec ndarray

A vector of complex weights for each antenna element. If None, uniform weights are used. Defaults to None.

None
N_theta int

The number of angular points to calculate the gain over. Defaults to 10000.

10000
steer_angle float

The angle in degrees at which to steer the main beam. 0 degrees is broadside. Defaults to 0.

0
plot bool

If True, plots the gain pattern in dBi. Defaults to False.

False

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - theta_vec (np.ndarray): The grid of angles in degrees, from -90 to 90. - gain_vec_db (np.ndarray): The voltage gain in dBi at each angle.

Source code in src/rad_lab/uniform_linear_arrays.py
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
def linear_antenna_gain_N_db(
    N_el: int,
    dx: float,
    weight_vec: np.ndarray | None = None,
    N_theta: int = 10000,
    steer_angle: float = 0,
    plot: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculates gain pattern in dBi for a uniform linear array.

    This function defines the array geometry based on the number of elements
    and their uniform spacing. It then computes and returns the gain in dBi.

    Args:
        N_el (int): The number of antenna array elements.
        dx (float): The spacing between elements, normalized by signal wavelength.
        weight_vec (np.ndarray, optional): A vector of complex weights for each
            antenna element. If None, uniform weights are used. Defaults to None.
        N_theta (int, optional): The number of angular points to calculate the
            gain over. Defaults to 10000.
        steer_angle (float, optional): The angle in degrees at which to steer
            the main beam. 0 degrees is broadside. Defaults to 0.
        plot (bool, optional): If True, plots the gain pattern in dBi.
            Defaults to False.

    Returns:
        tuple[np.ndarray, np.ndarray]:
            - theta_vec (np.ndarray): The grid of angles in degrees, from -90 to 90.
            - gain_vec_db (np.ndarray): The voltage gain in dBi at each angle.
    """
    L = (N_el - 1) * dx
    el_pos = np.linspace(-L / 2, L / 2, N_el)  # wavelengths

    thetas, gain = linear_antenna_gain(
        el_pos, weight_vec=weight_vec, N_theta=N_theta, steer_angle=steer_angle, plot=plot
    )
    return thetas, 20 * np.log10(abs(gain))

linear_antenna_gain_meters(el_pos, fc, weight_vec=None, N_theta=10000, steer_angle=0, plot=False)

Calculates gain pattern from element positions in meters and frequency.

This is a convenience wrapper for linear_antenna_gain that converts physical positions and frequency into wavelength-normalized positions.

Parameters:

Name Type Description Default
el_pos ndarray

1D array of element positions in meters.

required
fc float

The signal's center frequency in Hertz.

required
weight_vec ndarray

A vector of complex weights for each antenna element. If None, uniform weights are used. Defaults to None.

None
N_theta int

The number of angular points to calculate the gain over. Defaults to 10000.

10000
steer_angle float

The angle in degrees at which to steer the main beam. 0 degrees is broadside. Defaults to 0.

0
plot bool

If True, plots the gain pattern in dBi. Defaults to False.

False

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - theta_vec (np.ndarray): The grid of angles in degrees, from -90 to 90. - gain_vec (np.ndarray): The complex voltage gain at each angle.

Source code in src/rad_lab/uniform_linear_arrays.py
 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
def linear_antenna_gain_meters(
    el_pos: np.ndarray,
    fc: float,
    weight_vec: np.ndarray | None = None,
    N_theta: int = 10000,
    steer_angle: float = 0,
    plot: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculates gain pattern from element positions in meters and frequency.

    This is a convenience wrapper for `linear_antenna_gain` that converts
    physical positions and frequency into wavelength-normalized positions.

    Args:
        el_pos (np.ndarray): 1D array of element positions in meters.
        fc (float): The signal's center frequency in Hertz.
        weight_vec (np.ndarray, optional): A vector of complex weights for each
            antenna element. If None, uniform weights are used. Defaults to None.
        N_theta (int, optional): The number of angular points to calculate the
            gain over. Defaults to 10000.
        steer_angle (float, optional): The angle in degrees at which to steer
            the main beam. 0 degrees is broadside. Defaults to 0.
        plot (bool, optional): If True, plots the gain pattern in dBi.
            Defaults to False.

    Returns:
        tuple[np.ndarray, np.ndarray]:
            - theta_vec (np.ndarray): The grid of angles in degrees, from -90 to 90.
            - gain_vec (np.ndarray): The complex voltage gain at each angle.
    """
    wavelength = c.C / fc
    el_pos_per_wl = el_pos / wavelength
    return linear_antenna_gain(
        el_pos_per_wl, weight_vec=weight_vec, N_theta=N_theta, steer_angle=steer_angle, plot=plot
    )

steering_vector(el_pos, theta)

Computes the Vandermonde steering vector for a linear array.

This vector represents the phase shifts of a planar wave arriving from a specific angle as measured at each element of the array.

Parameters:

Name Type Description Default
el_pos ndarray

A 1D array of antenna element positions, normalized by the signal wavelength.

required
theta float

The steering angle in degrees, where 0 is broadside.

required

Returns:

Type Description
ndarray

np.ndarray: A complex-valued steering vector of the same size as el_pos.

Source code in src/rad_lab/uniform_linear_arrays.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def steering_vector(el_pos: np.ndarray, theta: float) -> np.ndarray:
    """
    Computes the Vandermonde steering vector for a linear array.

    This vector represents the phase shifts of a planar wave arriving from a specific
    angle as measured at each element of the array.

    Args:
        el_pos (np.ndarray): A 1D array of antenna element positions, normalized
                             by the signal wavelength.
        theta (float): The steering angle in degrees, where 0 is broadside.

    Returns:
        np.ndarray: A complex-valued steering vector of the same size as `el_pos`.
    """
    theta_rad = np.deg2rad(theta)
    return np.exp(-1j * 2 * np.pi * np.sin(theta_rad) * el_pos)

ula_pattern(el_pos, weight_vec=None, two_way=True)

Creates a beam-pattern callable from a ULA for use with SAR spotlight.

Precomputes the array factor via :func:linear_antenna_gain, normalises to unit peak, and returns a function that maps off-boresight angles (in radians) to amplitude weights. The returned callable is directly compatible with the beam_pattern parameter of :func:rad_lab.sar.gen.

Parameters:

Name Type Description Default
el_pos ndarray

Element positions normalised by wavelength.

required
weight_vec ndarray | None

Optional complex element weights (default: uniform).

None
two_way bool

If True (default), square the one-way voltage pattern to model the round-trip amplitude.

True

Returns:

Type Description
Callable[[ndarray], ndarray]

A callable pattern(theta_rad) -> weights where theta_rad is

Callable[[ndarray], ndarray]

an array of off-boresight angles [rad] and weights is a

Callable[[ndarray], ndarray]

same-shape array of amplitude weights in [0, 1].

Source code in src/rad_lab/uniform_linear_arrays.py
221
222
223
224
225
226
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
def ula_pattern(
    el_pos: np.ndarray,
    weight_vec: np.ndarray | None = None,
    two_way: bool = True,
) -> Callable[[np.ndarray], np.ndarray]:
    """Creates a beam-pattern callable from a ULA for use with SAR spotlight.

    Precomputes the array factor via :func:`linear_antenna_gain`, normalises
    to unit peak, and returns a function that maps off-boresight angles
    (in radians) to amplitude weights.  The returned callable is directly
    compatible with the ``beam_pattern`` parameter of :func:`rad_lab.sar.gen`.

    Args:
        el_pos: Element positions normalised by wavelength.
        weight_vec: Optional complex element weights (default: uniform).
        two_way: If True (default), square the one-way voltage pattern to
            model the round-trip amplitude.

    Returns:
        A callable ``pattern(theta_rad) -> weights`` where *theta_rad* is
        an array of off-boresight angles [rad] and *weights* is a
        same-shape array of amplitude weights in [0, 1].
    """
    theta_deg, complex_gain = linear_antenna_gain(el_pos, weight_vec)
    norm_gain = np.abs(complex_gain) / np.abs(complex_gain).max()
    interp_fn = interpolate.interp1d(
        theta_deg, norm_gain, kind="linear", bounds_error=False, fill_value=0.0
    )

    def pattern(theta_rad: np.ndarray) -> np.ndarray:
        weights = interp_fn(np.rad2deg(theta_rad))
        if two_way:
            weights = weights**2
        return weights

    return pattern