## Problem specs
lambda_wl <- 1 # wavelength
theta_tar <- 60 # target direction (degrees)
min_sidelobe <- -20 # maximum sidelobe level in dB
max_half_beam <- 50 # starting half beamwidth (must be feasible)
## 2D random antenna locations
set.seed(1)
n <- 36
L <- 5
loc <- matrix(L * runif(n * 2), nrow = n, ncol = 2)Minimize Beamwidth of an Antenna Array
Introduction
Adapted from the CVX example of the same name, by Almir Mutapcic, 2/2/2006 and Judson Wilson, 5/14/2014.
References:
- “Convex optimization examples” lecture notes (EE364) by S. Boyd
- “Antenna array pattern synthesis via convex optimization” by H. Lebret and S. Boyd
This algorithm designs an antenna array such that:
- it has unit sensitivity at some target direction
- it obeys a constraint on a minimum sidelobe level outside the beam
- it minimizes the beamwidth of the pattern.
This is a quasiconvex problem. Define the target direction as
Solving for the minimum beamwidth
Here
Once the optimal beamwidth is found, the solution
The implementation below discretizes the angular quantities.
Problem Data
We use a random 2D positioning of antennas.
Build Optimization Matrices
We construct the matrix
## Theta in degrees from 1 to 360
theta <- matrix(1:360, ncol = 1)
## Build matrix A: y = A * w (complex-valued)
A <- exp(2i * pi / lambda_wl *
(cos(pi * theta / 180) %*% t(loc[, 1]) +
sin(pi * theta / 180) %*% t(loc[, 2])))
## Target constraint
ind_closest <- which.min(abs(theta - theta_tar))
Atar <- A[ind_closest, , drop = FALSE]Solve Using Bisection
Since CVXR works with real variables, we represent complex-valued quantities by separating real and imaginary parts. A complex vector
## Set up real/imaginary representation of Atar
Atar_R <- Re(Atar)
Atar_I <- Im(Atar)
Atar_RI <- rbind(cbind(Atar_R, -Atar_I),
cbind(Atar_I, Atar_R))
## Target vector: [1, 0] (unit gain, no imaginary component)
realones_ri <- c(1.0, 0.0)
## Bisection
halfbeam_bot <- 1
halfbeam_top <- max_half_beam
cat("We are only considering integer values of the half beam-width\n")
cat("(since we are sampling the angle with 1 degree resolution).\n\n")
while (halfbeam_top - halfbeam_bot > 1) {
halfbeam_cur <- ceiling((halfbeam_top + halfbeam_bot) / 2)
## Indices of the stopband (outside the beam)
ind <- which(theta <= (theta_tar - halfbeam_cur) |
theta >= (theta_tar + halfbeam_cur))
As <- A[ind, , drop = FALSE]
## Real/imaginary representation
As_R <- Re(As)
As_I <- Im(As)
As_RI_top <- cbind(As_R, -As_I)
As_RI_bot <- cbind(As_I, As_R)
## CVXR variables and constraints
w_ri <- Variable(2 * n)
constraints <- list(Atar_RI %*% w_ri == realones_ri)
## Add sidelobe constraints row by row
sidelobe_thresh <- 10^(min_sidelobe / 20)
sidelobe_constraints <- lapply(seq_len(nrow(As)), function(i) {
As_ri_row <- rbind(As_RI_top[i, , drop = FALSE],
As_RI_bot[i, , drop = FALSE])
p_norm(As_ri_row %*% w_ri, 2) <= sidelobe_thresh
})
constraints <- c(constraints, sidelobe_constraints)
## Solve feasibility problem
obj <- Minimize(0)
prob <- Problem(obj, constraints)
result <- psolve(prob, solver = "SCS")
## Bisection logic
if (status(prob) == "optimal") {
cat(sprintf("Problem is feasible for half beam-width = %g degrees\n",
halfbeam_cur))
halfbeam_top <- halfbeam_cur
} else {
cat(sprintf("Problem is not feasible for half beam-width = %g degrees\n",
halfbeam_cur))
halfbeam_bot <- halfbeam_cur
}
}
halfbeam <- halfbeam_top
cat(sprintf("\nOptimum half beam-width for given specs is %g degrees\n", halfbeam))We are only considering integer values of the half beam-width
(since we are sampling the angle with 1 degree resolution).
Problem is feasible for half beam-width = 26 degrees
Problem is feasible for half beam-width = 14 degrees
Problem is not feasible for half beam-width = 8 degrees
Problem is feasible for half beam-width = 11 degrees
Problem is feasible for half beam-width = 10 degrees
Problem is not feasible for half beam-width = 9 degrees
Optimum half beam-width for given specs is 10 degrees
Minimum Noise Design
We now compute the minimum-norm weights for the optimal beamwidth.
## Recompute stopband for optimal beamwidth
ind <- which(theta <= (theta_tar - halfbeam) |
theta >= (theta_tar + halfbeam))
As <- A[ind, , drop = FALSE]
As_R <- Re(As)
As_I <- Im(As)
As_RI_top <- cbind(As_R, -As_I)
As_RI_bot <- cbind(As_I, As_R)
w_ri <- Variable(2 * n)
constraints <- list(Atar_RI %*% w_ri == realones_ri)
sidelobe_thresh <- 10^(min_sidelobe / 20)
sidelobe_constraints <- lapply(seq_len(nrow(As)), function(i) {
As_ri_row <- rbind(As_RI_top[i, , drop = FALSE],
As_RI_bot[i, , drop = FALSE])
p_norm(As_ri_row %*% w_ri, 2) <= sidelobe_thresh
})
constraints <- c(constraints, sidelobe_constraints)
## Minimize the weight norm
obj <- Minimize(p_norm(w_ri, 2))
prob <- Problem(obj, constraints)
result_final <- psolve(prob, solver = "SCS")
cat(sprintf("Final objective value: %f\n", result_final))Final objective value: 0.277997
Result Plots
Antenna Locations
ggplot(data.frame(x = loc[, 1], y = loc[, 2])) +
geom_point(aes(x = x, y = y), shape = 1, size = 3, color = "blue") +
labs(title = "Antenna Locations", x = "x", y = "y") +
coord_fixed() +
theme_minimal()
Array Pattern
## Compute full gain pattern
A_R <- Re(A)
A_I <- Im(A)
A_RI <- rbind(cbind(A_R, -A_I), cbind(A_I, A_R))
w_ri_val <- value(w_ri)
y_full <- A_RI %*% w_ri_val
n_theta <- length(theta)
y_complex <- y_full[1:n_theta] + 1i * y_full[(n_theta + 1):(2 * n_theta)]
y_mag_db <- 20 * log10(abs(y_complex))
df_pattern <- data.frame(
theta = as.vector(theta),
gain_db = as.vector(y_mag_db)
)
ggplot(df_pattern, aes(x = theta, y = gain_db)) +
geom_line() +
geom_vline(xintercept = theta_tar, linetype = "dashed", color = "green") +
geom_vline(xintercept = theta_tar + halfbeam, linetype = "dashed", color = "red") +
geom_vline(xintercept = theta_tar - halfbeam, linetype = "dashed", color = "red") +
ylim(-40, 0) +
labs(x = "Look angle (degrees)", y = "Magnitude (dB)",
title = "Array Gain Pattern") +
theme_minimal()
Polar Pattern
zerodB <- 50
dBY <- pmax(y_mag_db + zerodB, 0)
theta_rad <- as.vector(theta) * pi / 180
df_polar <- data.frame(
x = as.vector(dBY) * cos(theta_rad),
y = as.vector(dBY) * sin(theta_rad)
)
## 0 dB circle
circle_0dB <- data.frame(
x = zerodB * cos(theta_rad),
y = zerodB * sin(theta_rad)
)
## Sidelobe circle
m_sl <- min_sidelobe + zerodB
circle_sl <- data.frame(
x = m_sl * cos(theta_rad),
y = m_sl * sin(theta_rad)
)
ggplot() +
geom_path(data = df_polar, aes(x = x, y = y), color = "blue") +
geom_path(data = circle_0dB, aes(x = x, y = y), linetype = "dotted") +
geom_path(data = circle_sl, aes(x = x, y = y), linetype = "dotted") +
coord_fixed(xlim = c(-zerodB, zerodB), ylim = c(-zerodB, zerodB)) +
annotate("text", x = -zerodB, y = 2, label = "0 dB") +
annotate("text", x = -m_sl, y = 2, label = paste0(min_sidelobe, " dB")) +
labs(title = "Polar Radiation Pattern") +
theme_void()
Session Info
R version 4.5.2 (2025-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Tahoe 26.3
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: America/Los_Angeles
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] ggplot2_4.0.2 CVXR_1.8.1
loaded via a namespace (and not attached):
[1] gmp_0.7-5.1 generics_0.1.4 clarabel_0.11.2 slam_0.1-55
[5] lattice_0.22-9 digest_0.6.39 magrittr_2.0.4 evaluate_1.0.5
[9] grid_4.5.2 RColorBrewer_1.1-3 fastmap_1.2.0 jsonlite_2.0.0
[13] Matrix_1.7-4 ECOSolveR_0.6.1 backports_1.5.0 scs_3.2.7
[17] Rmosek_11.1.1 scales_1.4.0 codetools_0.2-20 cli_3.6.5
[21] rlang_1.1.7 Rglpk_0.6-5.1 withr_3.0.2 yaml_2.3.12
[25] otel_0.2.0 tools_4.5.2 osqp_1.0.0 Rcplex_0.3-8
[29] checkmate_2.3.4 dplyr_1.2.0 gurobi_13.0-1 vctrs_0.7.1
[33] R6_2.6.1 lifecycle_1.0.5 htmlwidgets_1.6.4 pkgconfig_2.0.3
[37] cccp_0.3-3 pillar_1.11.1 gtable_0.3.6 glue_1.8.0
[41] Rcpp_1.1.1 xfun_0.56 tibble_3.3.1 tidyselect_1.2.1
[45] knitr_1.51 dichromat_2.0-0.1 highs_1.12.0-3 farver_2.1.2
[49] htmltools_0.5.9 rmarkdown_2.30 labeling_0.4.3 piqp_0.6.2
[53] compiler_4.5.2 S7_0.2.1