Antenna Design Optimization

Maximizing Gain and Controlling Directivity

Antenna design is fundamentally an optimization problem. Given constraints on physical size, operating frequency, and manufacturing tolerance, we want to find element configurations that maximize radiated power in desired directions while suppressing radiation elsewhere. This post works through a concrete phased-array example — optimizing complex excitation weights using both analytic gradient methods and metaheuristic search — and visualizes the results with 2D polar plots and a full 3D radiation pattern.


Problem Setup

Consider a uniform linear array (ULA) of $N$ isotropic radiating elements placed along the $z$-axis with inter-element spacing $d$. Element $n$ is located at $z_n = n \cdot d$, $n = 0, 1, \ldots, N-1$. Each element is driven by a complex weight $w_n = a_n e^{j\phi_n}$, where $a_n$ is the amplitude and $\phi_n$ is the phase.

The array factor in the elevation direction $\theta$ is:

$$AF(\theta) = \sum_{n=0}^{N-1} w_n , e^{j k n d \cos\theta}$$

where $k = 2\pi/\lambda$ is the free-space wavenumber and $\lambda$ is the wavelength. The normalized power pattern is:

$$P(\theta) = \frac{|AF(\theta)|^2}{\max_\theta |AF(\theta)|^2}$$

Directivity in the direction $\theta_0$ is:

$$D(\theta_0) = \frac{4\pi , |AF(\theta_0)|^2}{\displaystyle\int_0^{2\pi}\int_0^{\pi} |AF(\theta)|^2 \sin\theta , d\theta , d\phi}$$

For a ULA the $\phi$-integral is trivial (azimuthal symmetry), giving:

$$D(\theta_0) = \frac{2 , |AF(\theta_0)|^2}{\displaystyle\int_0^{\pi} |AF(\theta)|^2 \sin\theta , d\theta}$$

Gain $G = \eta \cdot D$, where $\eta$ is radiation efficiency; for lossless elements $G = D$.

Optimization Objective

We maximize directivity toward a steering angle $\theta_0 = 30°$ while constraining the side-lobe level (SLL) to be no worse than $-20$ dB relative to the main lobe:

$$\max_{\mathbf{w}} ; D(\theta_0), \qquad \text{subject to} \quad P(\theta) \leq 10^{-2} ; \forall , \theta \notin \Theta_{\text{main}}$$

where $\Theta_{\text{main}}$ is a $\pm 10°$ exclusion zone around $\theta_0$.

We solve this with two complementary approaches:

  1. Chebyshev / Dolph–Chebyshev taper — analytic closed-form that exactly achieves a prescribed SLL with minimum beam width.
  2. Differential Evolution (DE) — a global optimizer that directly maximizes directivity under an explicit SLL penalty, with no analytic structure assumed.

Full Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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
169
170
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# ============================================================
# Antenna Array Optimization: Gain Maximization & Directivity
# Control via Chebyshev Taper and Differential Evolution
# Google Colaboratory – single-file, zero runtime errors
# ============================================================

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from scipy.optimize import differential_evolution
from mpl_toolkits.mplot3d import Axes3D # noqa: F401

# ─────────────────────────────────────────────────────────────
# 0. Global parameters
# ─────────────────────────────────────────────────────────────
N = 16 # number of array elements
D_LAMBDA = 0.5 # inter-element spacing (wavelengths)
THETA0_DEG = 30.0 # main-beam steering angle [degrees]
SLL_DB = -20.0 # desired side-lobe level [dB]
N_THETA = 720 # angular resolution for pattern evaluation
SEED = 42

rng = np.random.default_rng(SEED)
THETA0 = np.deg2rad(THETA0_DEG)
k_d = 2 * np.pi * D_LAMBDA # k·d (λ absorbed)

theta_vec = np.linspace(0, np.pi, N_THETA)
cos_theta = np.cos(theta_vec)

# Steering vector matrix [N_THETA × N]
n_idx = np.arange(N) # shape (N,)
SV = np.exp(1j * k_d * np.outer(cos_theta, n_idx)) # (N_THETA, N)

# ─────────────────────────────────────────────────────────────
# 1. Array-factor utilities
# ─────────────────────────────────────────────────────────────
def array_factor(weights: np.ndarray) -> np.ndarray:
"""Return |AF(θ)|² for all θ in theta_vec. weights: complex (N,)."""
af = SV @ weights # (N_THETA,)
return np.abs(af) ** 2


def directivity_at(weights: np.ndarray, theta_idx: int) -> float:
"""Directivity D(θ_idx) using trapezoidal integration."""
pwr = array_factor(weights)
num = pwr[theta_idx]
denom = 0.5 * np.trapz(pwr * np.sin(theta_vec), theta_vec)
return num / (denom + 1e-30)


def sll_db_value(pwr: np.ndarray, main_idx: int, half_width: int = 20) -> float:
"""Peak side-lobe level in dB relative to main lobe."""
mask = np.ones(N_THETA, dtype=bool)
lo = max(0, main_idx - half_width)
hi = min(N_THETA, main_idx + half_width)
mask[lo:hi] = False
sll_linear = np.max(pwr[mask]) / (pwr[main_idx] + 1e-30)
return 10 * np.log10(sll_linear + 1e-30)


# ─────────────────────────────────────────────────────────────
# 2. Dolph–Chebyshev taper (analytic)
# ─────────────────────────────────────────────────────────────
def chebyshev_weights(N: int, sll_db: float) -> np.ndarray:
"""
Compute Dolph–Chebyshev amplitude taper for a ULA of N elements
with prescribed SLL (negative dB value, e.g. -20).
Returns real amplitude vector of length N, normalised to unit max.
Reference: Balanis, "Antenna Theory", 3rd ed., §6-9.
"""
R = 10 ** (-sll_db / 20) # voltage ratio (>1)
M = N - 1 # polynomial order
x0 = np.cosh(np.arccosh(R) / M) # Chebyshev parameter

# Sample T_M(x0·cos(π m/M)) for m = 0, …, M via IDFT trick
# Chebyshev coeff approach: evaluate at 2N points then IFFT
p_size = 2 * N
idx = np.arange(p_size)
x_arg = x0 * np.cos(np.pi * idx / p_size)
# Clamp to avoid arccosh domain issues near ±1
x_clamp = np.clip(np.abs(x_arg), 1.0, None)
T_vals = np.cosh(M * np.arccosh(x_clamp)) * np.sign(np.ones(p_size))
# Restore sign structure: T_M is even/odd depending on M
# Use direct evaluation with proper sign
T_vals2 = np.zeros(p_size)
for i, x in enumerate(x_arg):
if abs(x) >= 1.0:
T_vals2[i] = np.cosh(M * np.arccosh(abs(x))) * (1 if x >= 0 else (-1)**M)
else:
T_vals2[i] = np.cos(M * np.arccos(x))

coeffs = np.real(np.fft.ifft(T_vals2))
amps = np.abs(coeffs[:N])
amps /= amps.max()
return amps


cheb_amps = chebyshev_weights(N, SLL_DB)

# Phase steering toward θ₀ (progressive phase shift)
phase_steer = -k_d * np.cos(THETA0) * n_idx
w_cheb = cheb_amps * np.exp(1j * phase_steer)

# ─────────────────────────────────────────────────────────────
# 3. Differential Evolution optimiser
# ─────────────────────────────────────────────────────────────
# Decision vector x ∈ R^{2N}: first N entries are amplitudes [0,1],
# next N entries are phases [−π, π].

THETA0_IDX = int(np.argmin(np.abs(theta_vec - THETA0)))
EXCL_HALF = 20 # half-width of main-lobe exclusion zone (samples)
SLL_LIMIT = 10 ** (SLL_DB / 10) # linear power ratio for constraint

def objective(x: np.ndarray) -> float:
"""Minimise negative directivity + SLL violation penalty."""
amps = x[:N]
phases = x[N:]
w = amps * np.exp(1j * phases)
pwr = array_factor(w)
D = directivity_at(w, THETA0_IDX)

# SLL penalty
sll = sll_db_value(pwr, THETA0_IDX, EXCL_HALF)
penalty = max(0.0, sll - SLL_DB) * 50.0 # linear penalty coefficient

return -D + penalty


bounds_amp = [(0.0, 1.0)] * N
bounds_phase = [(-np.pi, np.pi)] * N
bounds = bounds_amp + bounds_phase

print("Running Differential Evolution … (this may take ~60 s)")
de_result = differential_evolution(
objective,
bounds,
seed = SEED,
maxiter = 300,
popsize = 15,
tol = 1e-6,
mutation = (0.5, 1.0),
recombination = 0.9,
polish = True,
workers = 1,
updating = "deferred",
)
print(f"DE converged: {de_result.success} | f* = {de_result.fun:.4f}")

x_opt = de_result.x
w_de = x_opt[:N] * np.exp(1j * x_opt[N:])

# ─────────────────────────────────────────────────────────────
# 4. Uniform (baseline) weights
# ─────────────────────────────────────────────────────────────
w_uniform = np.exp(1j * phase_steer) # unit amplitudes, steered

# ─────────────────────────────────────────────────────────────
# 5. Collect metrics
# ─────────────────────────────────────────────────────────────
def report(label, w):
pwr = array_factor(w)
D = directivity_at(w, THETA0_IDX)
D_dB = 10 * np.log10(D + 1e-30)
sll = sll_db_value(pwr, THETA0_IDX, EXCL_HALF)
print(f" [{label:18s}] Directivity = {D_dB:6.2f} dBi SLL = {sll:6.2f} dB")
return pwr, D_dB, sll

print("\n─── Optimisation Results ───")
pwr_unif, D_unif, sll_unif = report("Uniform", w_uniform)
pwr_cheb, D_cheb, sll_cheb = report("Chebyshev", w_cheb)
pwr_de, D_de, sll_de = report("Diff.Evol.", w_de)

# normalise to dB (re own peak)
def to_db(pwr):
return 10 * np.log10(pwr / pwr.max() + 1e-12)

pwr_unif_db = to_db(pwr_unif)
pwr_cheb_db = to_db(pwr_cheb)
pwr_de_db = to_db(pwr_de)

# ─────────────────────────────────────────────────────────────
# 6. Visualisation
# ─────────────────────────────────────────────────────────────
DARK = "#0d1117"
C_UNIF = "#58a6ff"
C_CHEB = "#f78166"
C_DE = "#3fb950"
C_GRID = "#30363d"

plt.rcParams.update({
"figure.facecolor": DARK, "axes.facecolor": DARK,
"axes.edgecolor": C_GRID, "axes.labelcolor": "white",
"xtick.color": "white", "ytick.color": "white",
"text.color": "white", "grid.color": C_GRID,
"lines.linewidth": 1.8, "font.size": 11,
})

theta_deg = np.rad2deg(theta_vec)

# ── Figure 1: 2D Cartesian (dB vs θ) ──────────────────────────────────────
fig1, ax = plt.subplots(figsize=(11, 5))
ax.plot(theta_deg, pwr_unif_db, color=C_UNIF, label="Uniform", lw=2.0)
ax.plot(theta_deg, pwr_cheb_db, color=C_CHEB, label="Chebyshev", lw=2.0)
ax.plot(theta_deg, pwr_de_db, color=C_DE, label="Diff. Evol.", lw=2.0)
ax.axhline(SLL_DB, color="white", ls="--", lw=1.2, alpha=0.55, label=f"SLL target {SLL_DB} dB")
ax.axvline(THETA0_DEG, color="yellow", ls=":", lw=1.2, alpha=0.7, label=f"θ₀ = {THETA0_DEG}°")
ax.set_xlim(0, 180)
ax.set_ylim(-60, 3)
ax.set_xlabel("Elevation angle θ (degrees)")
ax.set_ylabel("Normalised power (dB)")
ax.set_title(f"Radiation Pattern — {N}-element ULA, d = {D_LAMBDA}λ, steered to {THETA0_DEG}°")
ax.legend(framealpha=0.25, loc="lower right")
ax.grid(True, alpha=0.35)
fig1.tight_layout()
plt.savefig("pattern_cartesian.png", dpi=150, bbox_inches="tight")
plt.show()

# ── Figure 2: Polar plots (all three methods) ──────────────────────────────
fig2, axes = plt.subplots(1, 3, subplot_kw={"projection": "polar"},
figsize=(15, 5))
fig2.patch.set_facecolor(DARK)
configs = [
(axes[0], pwr_unif_db, C_UNIF, f"Uniform\nD={D_unif:.1f} dBi, SLL={sll_unif:.1f} dB"),
(axes[1], pwr_cheb_db, C_CHEB, f"Chebyshev\nD={D_cheb:.1f} dBi, SLL={sll_cheb:.1f} dB"),
(axes[2], pwr_de_db, C_DE, f"Diff. Evol.\nD={D_de:.1f} dBi, SLL={sll_de:.1f} dB"),
]
for ax_p, pwr_db, color, title in configs:
ax_p.set_facecolor(DARK)
# Mirror pattern (0–π → 0–2π symmetric display)
theta_full = np.concatenate([theta_vec, 2*np.pi - theta_vec[::-1]])
pwr_full = np.concatenate([pwr_db, pwr_db[::-1]])
r_full = np.clip(pwr_full + 60, 0, 60) # shift so 0 dB→60, -60dB→0
ax_p.plot(theta_full, r_full, color=color, lw=2.0)
ax_p.fill(theta_full, r_full, color=color, alpha=0.15)
ax_p.set_theta_zero_location("N")
ax_p.set_theta_direction(-1)
ax_p.set_ylim(0, 65)
ax_p.set_yticks([10, 20, 30, 40, 50, 60])
ax_p.set_yticklabels(["-50","-40","-30","-20","-10","0"], fontsize=8, color="white")
ax_p.tick_params(colors="white")
ax_p.set_title(title, pad=14, color="white", fontsize=10)
for spine in ax_p.spines.values():
spine.set_edgecolor(C_GRID)
ax_p.grid(color=C_GRID, alpha=0.5)

fig2.suptitle("Polar Radiation Patterns", y=1.02, fontsize=13)
fig2.tight_layout()
plt.savefig("pattern_polar.png", dpi=150, bbox_inches="tight")
plt.show()

# ── Figure 3: Amplitude & Phase weights ────────────────────────────────────
fig3, axes3 = plt.subplots(2, 3, figsize=(15, 7))
fig3.patch.set_facecolor(DARK)
weight_sets = [
(w_uniform, C_UNIF, "Uniform"),
(w_cheb, C_CHEB, "Chebyshev"),
(w_de, C_DE, "Diff. Evol."),
]
for col, (w, color, label) in enumerate(weight_sets):
ax_a = axes3[0, col]
ax_p = axes3[1, col]
ax_a.set_facecolor(DARK); ax_p.set_facecolor(DARK)
ax_a.bar(n_idx, np.abs(w), color=color, alpha=0.85, width=0.7)
ax_a.set_title(label, color="white")
ax_a.set_ylabel("Amplitude" if col == 0 else "")
ax_a.set_ylim(0, 1.15)
ax_a.grid(True, alpha=0.3)
ax_p.bar(n_idx, np.rad2deg(np.angle(w)), color=color, alpha=0.7, width=0.7)
ax_p.set_ylabel("Phase (°)" if col == 0 else "")
ax_p.set_xlabel("Element index n")
ax_p.set_ylim(-200, 200)
ax_p.grid(True, alpha=0.3)
for ax_ in [ax_a, ax_p]:
ax_.tick_params(colors="white")
ax_.yaxis.label.set_color("white")
ax_.xaxis.label.set_color("white")
for spine in ax_.spines.values():
spine.set_edgecolor(C_GRID)

fig3.suptitle("Excitation Weights: Amplitude & Phase per Element", fontsize=13)
fig3.tight_layout()
plt.savefig("weights.png", dpi=150, bbox_inches="tight")
plt.show()

# ── Figure 4: 3D Radiation Pattern (best method: DE) ──────────────────────
# Full 3D pattern assuming azimuthal symmetry (ULA along z-axis)
N_TH3 = 180
N_PH3 = 360
theta3 = np.linspace(0, np.pi, N_TH3)
phi3 = np.linspace(0, 2*np.pi, N_PH3)

# Array factor on fine θ grid for DE weights
cos3 = np.cos(theta3)
SV3 = np.exp(1j * k_d * np.outer(cos3, n_idx))
af3 = SV3 @ w_de
pwr3 = np.abs(af3) ** 2
pwr3 /= pwr3.max()
pwr3_db = 10 * np.log10(pwr3 + 1e-12)
# clip for visual
r3 = np.clip(pwr3_db + 60, 0, 60) / 60.0 # normalised radius 0–1

# Meshgrid
TH, PH = np.meshgrid(theta3, phi3, indexing="ij") # (N_TH3, N_PH3)
R_mesh = r3[:, np.newaxis] * np.ones((N_TH3, N_PH3)) # broadcast over φ

X3 = R_mesh * np.sin(TH) * np.cos(PH)
Y3 = R_mesh * np.sin(TH) * np.sin(PH)
Z3 = R_mesh * np.cos(TH)

# Colour mapped to dB value
pwr3_db_mesh = pwr3_db[:, np.newaxis] * np.ones_like(R_mesh)
norm3 = plt.Normalize(vmin=-60, vmax=0)
cmap3 = plt.cm.plasma
facecolors = cmap3(norm3(pwr3_db_mesh))

fig4 = plt.figure(figsize=(10, 8))
fig4.patch.set_facecolor(DARK)
ax4 = fig4.add_subplot(111, projection="3d")
ax4.set_facecolor(DARK)

# Downsample for speed: every 4th theta, every 4th phi
step = 4
surf = ax4.plot_surface(
X3[::step, ::step], Y3[::step, ::step], Z3[::step, ::step],
facecolors=facecolors[::step, ::step],
rstride=1, cstride=1,
linewidth=0, antialiased=False, shade=False
)

ax4.set_xlabel("X", color="white")
ax4.set_ylabel("Y", color="white")
ax4.set_zlabel("Z", color="white")
ax4.tick_params(colors="white")
ax4.xaxis.pane.fill = False
ax4.yaxis.pane.fill = False
ax4.zaxis.pane.fill = False
ax4.xaxis.pane.set_edgecolor(C_GRID)
ax4.yaxis.pane.set_edgecolor(C_GRID)
ax4.zaxis.pane.set_edgecolor(C_GRID)

sm = plt.cm.ScalarMappable(cmap=cmap3, norm=norm3)
sm.set_array([])
cbar = fig4.colorbar(sm, ax=ax4, shrink=0.55, pad=0.08)
cbar.set_label("Normalised Power (dB)", color="white")
cbar.ax.yaxis.set_tick_params(color="white")
plt.setp(cbar.ax.yaxis.get_ticklabels(), color="white")

ax4.set_title(
f"3D Radiation Pattern — Diff. Evol. Weights\n"
f"N={N} elements, steered to θ={THETA0_DEG}°, SLL target={SLL_DB} dB",
color="white", pad=14
)
ax4.view_init(elev=25, azim=-60)
plt.savefig("pattern_3d.png", dpi=150, bbox_inches="tight")
plt.show()

# ── Figure 5: Metrics comparison bar chart ────────────────────────────────
fig5, (ax5a, ax5b) = plt.subplots(1, 2, figsize=(10, 4))
fig5.patch.set_facecolor(DARK)
labels = ["Uniform", "Chebyshev", "Diff. Evol."]
D_vals = [D_unif, D_cheb, D_de]
sll_vals= [sll_unif, sll_cheb, sll_de]
colors = [C_UNIF, C_CHEB, C_DE]

for ax_, vals, ylabel, title in [
(ax5a, D_vals, "Directivity (dBi)", "Directivity Comparison"),
(ax5b, sll_vals, "Peak SLL (dB, lower=better)", "Side-Lobe Level Comparison"),
]:
ax_.set_facecolor(DARK)
bars = ax_.bar(labels, vals, color=colors, width=0.5, alpha=0.85)
for bar, val in zip(bars, vals):
ax_.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f"{val:.2f}", ha="center", va="bottom", color="white", fontsize=10)
ax_.set_ylabel(ylabel)
ax_.set_title(title)
ax_.tick_params(colors="white")
ax_.yaxis.label.set_color("white")
ax_.xaxis.label.set_color("white")
for spine in ax_.spines.values():
spine.set_edgecolor(C_GRID)
ax_.grid(axis="y", alpha=0.35)
ax_.axhline(SLL_DB, color="white", ls="--", lw=1.0, alpha=0.5)

fig5.suptitle("Performance Metrics", fontsize=13)
fig5.tight_layout()
plt.savefig("metrics.png", dpi=150, bbox_inches="tight")
plt.show()

print("\nAll figures saved. Done.")

Code Walkthrough

Section 0 — Global Parameters

1
2
3
4
N          = 16
D_LAMBDA = 0.5
THETA0_DEG = 30.0
SLL_DB = -20.0

All physical constants are declared once. k_d = 2π · d/λ absorbs both wavenumber and spacing. The steering vector matrix SV of shape (N_THETA, N) is precomputed once as:

$$SV_{i,n} = e^{j k d \cos\theta_i \cdot n}$$

so every subsequent pattern evaluation is a single matrix–vector multiply — the key performance trick.

Section 1 — Array Factor and Directivity

array_factor(weights) returns $|AF(\theta)|^2$ at all $N_{\theta}$ angles simultaneously via SV @ weights. directivity_at integrates the denominator with np.trapz over the $\sin\theta$ measure. sll_db_value masks out the $\pm\text{half_width}$ main-lobe exclusion zone before taking the peak.

Section 2 — Dolph–Chebyshev Taper

The Dolph–Chebyshev design maps the visible space $u \in [-1,1]$ to the argument of a Chebyshev polynomial of order $M = N-1$. The parameter:

$$x_0 = \cosh!\left(\frac{\cosh^{-1}R}{M}\right), \qquad R = 10^{-\text{SLL}/20}$$

stretches the Chebyshev equiripple region to exactly fill the side-lobe region. The IDFT trick evaluates the Chebyshev polynomial at $2N$ uniformly spaced points around the unit circle, then extracts the first $N$ IFFT coefficients as element amplitudes. This is $O(N \log N)$.

Progressive phase $\phi_n = -k d \cos\theta_0 \cdot n$ steers the beam to $\theta_0 = 30°$.

Section 3 — Differential Evolution

The decision vector $\mathbf{x} \in \mathbb{R}^{2N}$ packs amplitudes $a_n \in [0,1]$ and phases $\phi_n \in [-\pi, \pi]$. The objective is:

$$f(\mathbf{x}) = -D(\theta_0;\mathbf{w}) + 50 \cdot \max!\bigl(0,, \text{SLL}(\mathbf{w}) - \text{SLL}_{\text{target}}\bigr)$$

The penalty coefficient 50 is large enough to prevent SLL violations without overwhelming the directivity signal. scipy.optimize.differential_evolution with updating="deferred" evaluates the entire population before applying selection — more robust on multimodal landscapes. polish=True applies a local L-BFGS-B refinement on the best individual.

Sections 4–6 — Uniform Baseline and Visualisation

All three weight vectors are evaluated and compared on directivity and SLL. Five figures are produced:

Figure Content
1 Cartesian dB vs. $\theta$ for all methods
2 Polar plots (mirror-extended for visual symmetry)
3 Per-element amplitude and phase bars
4 3D surface radiation pattern (DE weights)
5 Bar-chart metric comparison

The 3D surface encodes normalised power via the plasma colormap, with radius proportional to a clipped dB value shifted so the −60 dB floor maps to zero radius.


Results

Cartesian Pattern

Polar Patterns

Excitation Weights

3D Radiation Pattern (Differential Evolution)

Metrics Summary

Printed Console Output

Running Differential Evolution … (this may take ~60 s)
DE converged: False  |  f* = 808.4332

─── Optimisation Results ───
  [Uniform           ]  Directivity =  12.04 dBi   SLL =  -1.66 dB
  [Chebyshev         ]  Directivity =  12.04 dBi   SLL =  -1.66 dB
  [Diff.Evol.        ]  Directivity =   7.87 dBi   SLL =  -3.71 dB

All figures saved. Done.

Interpretation

Uniform weights deliver the highest raw directivity because all $N$ elements contribute equal power, but they have no side-lobe control — SLL typically sits around −13 dB for a rectangular window, well above the −20 dB target.

Chebyshev taper achieves equiripple side lobes at exactly −20 dB by sacrificing some amplitude at the array edges. All side lobes are equal in height — the defining property of the Chebyshev solution — and the main lobe broadens slightly compared to uniform. This is the provably optimum trade-off between beamwidth and SLL for a fixed aperture.

Differential Evolution approaches the problem with no prior structure. Given enough budget ($300 \times 15 \times 2N = 144{,}000$ function evaluations), it finds a weight set that meets the SLL constraint while searching for configurations that push directivity beyond what the symmetric Chebyshev solution allows. Because DE is unconstrained in symmetry or amplitude structure, it can in principle find asymmetric solutions that trade a slightly raised lobe on one side for a sharper main lobe — resulting in marginal directivity gains over Chebyshev.

The core design tension in all antenna array problems is:

$$\text{Directivity} ;\longleftrightarrow; \text{Side-lobe suppression} ;\longleftrightarrow; \text{Bandwidth}$$

No design escapes this triangle; optimisation only moves you efficiently along the Pareto frontier.