Hardware Engineer Recipes#
Practical recipes for hardware engineers designing phased array systems.
Selecting Phase Shifter Resolution#
Problem: Determine the minimum phase shifter bits needed for your sidelobe requirements.
import phased_array as pa
import numpy as np
# Your array configuration
geom = pa.create_rectangular_array(16, 16, dx=0.5, dy=0.5)
k = pa.wavelength_to_k(1.0)
# Target sidelobe level
target_sll_dB = -30
# Design weights with taper
weights = pa.steering_vector(k, geom.x, geom.y, theta0_deg=30, phi0_deg=0)
weights *= pa.taylor_taper_2d(16, 16, sidelobe_dB=target_sll_dB)
# Test different bit depths
print("Bits | RMS Error | Achieved SLL")
print("-" * 35)
for bits in range(3, 8):
weights_q = pa.quantize_phase(weights, n_bits=bits)
_, E_plane, _ = pa.compute_pattern_cuts(geom.x, geom.y, weights_q, k)
# Find peak sidelobe (exclude main beam region)
peak_idx = np.argmax(E_plane)
sidelobes = np.concatenate([E_plane[:peak_idx-10], E_plane[peak_idx+10:]])
achieved_sll = np.max(sidelobes) if len(sidelobes) > 0 else -99
rms = pa.quantization_rms_error(bits)
print(f" {bits} | {rms:5.1f} deg | {achieved_sll:6.1f} dB")
Rule of thumb: For -30 dB sidelobes, use at least 5-bit phase shifters. For -40 dB, use 6-bit.
Element Spacing vs Scan Range#
Problem: Determine maximum element spacing for your scan requirements.
import matplotlib.pyplot as plt
def max_spacing_for_scan(theta_max_deg):
"""Maximum spacing to avoid grating lobes."""
theta_max = np.deg2rad(theta_max_deg)
return 1.0 / (1.0 + np.sin(theta_max))
scan_angles = np.arange(0, 91, 5)
max_spacings = [max_spacing_for_scan(a) for a in scan_angles]
plt.figure(figsize=(8, 5))
plt.plot(scan_angles, max_spacings, 'b-', linewidth=2)
plt.axhline(y=0.5, color='r', linestyle='--', label='λ/2 spacing')
plt.xlabel('Maximum Scan Angle (deg)')
plt.ylabel('Maximum Element Spacing (λ)')
plt.title('Element Spacing vs. Grating Lobe-Free Scan Range')
plt.grid(True)
plt.legend()
plt.xlim(0, 90)
plt.ylim(0.4, 1.0)
Key values:
λ/2 spacing: scan to ±90° (full hemisphere)
0.6λ spacing: scan to ±56°
0.7λ spacing: scan to ±46°
Verifying Grating Lobe Locations#
Problem: Check if grating lobes appear in the visible region.
# Array with larger spacing (potential grating lobes)
dx = 0.7 # Larger than λ/2
geom = pa.create_rectangular_array(16, 16, dx=dx, dy=dx)
k = pa.wavelength_to_k(1.0)
# Scan to 30 degrees
weights = pa.steering_vector(k, geom.x, geom.y, theta0_deg=30, phi0_deg=0)
# Compute UV-space pattern
u, v, pattern_uv = pa.compute_pattern_uv_space(
geom.x, geom.y, weights, k,
u_range=(-1.5, 1.5), v_range=(-1.5, 1.5) # Include invisible region
)
# Plot with visible region marked
pa.plot_pattern_uv_space(u, v, pattern_uv, show_visible_region=True)
# Grating lobe locations (for 1D):
# u_gl = u_0 + n*λ/d where n = ±1, ±2, ...
u0 = np.sin(np.deg2rad(30))
print(f"Main beam: u = {u0:.3f}")
print(f"First grating lobe: u = {u0 + 1/dx:.3f}")
Estimating TTD Requirements#
Problem: Calculate true-time delay values for your array.
# Physical array
frequency = 10e9 # 10 GHz
wavelength = 3e8 / frequency
geom = pa.create_rectangular_array(32, 32, dx=0.5, dy=0.5, wavelength=wavelength)
# Get required delays for 45 degree scan
delays = pa.steering_delays_ttd(
geom.x, geom.y,
theta0_deg=45, phi0_deg=0
)
# Statistics
delay_range = np.max(delays) - np.min(delays)
print(f"Aperture size: {np.max(geom.x) - np.min(geom.x):.3f} m")
print(f"Maximum delay: {np.max(delays)*1e12:.1f} ps")
print(f"Delay range: {delay_range*1e12:.1f} ps")
print(f"Delay bits needed (10 ps LSB): {int(np.ceil(np.log2(delay_range/10e-12)))}")
Thermal Effects on Beam Pointing#
Problem: Estimate beam pointing error from phase shifter temperature drift.
# Phase shifter temperature coefficient (typical: 0.1-0.5 deg/°C)
temp_coeff = 0.2 # deg per °C
# Temperature gradient across array
temp_gradient = 5 # °C edge-to-edge
# Create phase errors (linear gradient)
geom = pa.create_rectangular_array(16, 16, dx=0.5, dy=0.5)
k = pa.wavelength_to_k(1.0)
# Ideal weights
weights = pa.steering_vector(k, geom.x, geom.y, theta0_deg=20, phi0_deg=0)
# Add temperature-induced phase errors
x_norm = (geom.x - geom.x.min()) / (geom.x.max() - geom.x.min())
phase_error = np.deg2rad(temp_coeff * temp_gradient * x_norm)
weights_thermal = weights * np.exp(1j * phase_error)
# Compare beam directions
_, E_ideal, _ = pa.compute_pattern_cuts(geom.x, geom.y, weights, k)
_, E_thermal, _ = pa.compute_pattern_cuts(geom.x, geom.y, weights_thermal, k)
# Find peak locations
theta_deg = np.linspace(-90, 90, 361)
peak_ideal = theta_deg[np.argmax(E_ideal)]
peak_thermal = theta_deg[np.argmax(E_thermal)]
print(f"Beam pointing error: {peak_thermal - peak_ideal:.3f} deg")
Power Amplifier Saturation Effects#
Problem: Model amplitude compression in power amplifiers.
def apply_pa_compression(weights, p1dB_backoff=3.0):
"""
Model PA compression using soft limiter.
Parameters
----------
weights : ndarray
Complex weights
p1dB_backoff : float
Backoff from P1dB in dB
"""
amplitude = np.abs(weights)
phase = np.angle(weights)
# Normalize to peak
amp_norm = amplitude / np.max(amplitude)
# Soft compression (approximate Rapp model)
p = 2 # Smoothness factor
a_sat = 10 ** (-p1dB_backoff / 20)
amp_compressed = amp_norm / (1 + (amp_norm / a_sat) ** (2*p)) ** (1/(2*p))
return amp_compressed * np.max(amplitude) * np.exp(1j * phase)
# Test effect on tapered weights
weights = pa.steering_vector(k, geom.x, geom.y, 0, 0)
weights *= pa.taylor_taper_2d(16, 16, sidelobe_dB=-30)
weights_compressed = apply_pa_compression(weights, p1dB_backoff=2.0)
# Compression reduces taper effectiveness (higher sidelobes)
Calibration Error Budget#
Problem: Understand how calibration errors affect pattern performance.
def add_calibration_errors(weights, amp_error_dB, phase_error_deg, seed=None):
"""Add random amplitude and phase calibration errors."""
if seed is not None:
np.random.seed(seed)
n = len(weights)
# Random errors (Gaussian)
amp_error = 10 ** (amp_error_dB * np.random.randn(n) / 20)
phase_error = np.deg2rad(phase_error_deg * np.random.randn(n))
return weights * amp_error * np.exp(1j * phase_error)
# Monte Carlo analysis
n_trials = 100
sll_results = []
for _ in range(n_trials):
w_err = add_calibration_errors(
weights,
amp_error_dB=0.5, # ±0.5 dB amplitude (1-sigma)
phase_error_deg=3.0 # ±3 deg phase (1-sigma)
)
_, E_plane, _ = pa.compute_pattern_cuts(geom.x, geom.y, w_err, k)
peak_idx = np.argmax(E_plane)
sll = np.max(E_plane[peak_idx+10:])
sll_results.append(sll)
print(f"Mean SLL: {np.mean(sll_results):.1f} dB")
print(f"Worst SLL (95%): {np.percentile(sll_results, 95):.1f} dB")