Skip to content

Your First Patch Antenna

This tutorial builds a finite-ground-plane patch model and runs a short setup check. It does not assign a resonant frequency, return loss, or Q to the model without a mesh/time-window study and an independent reference.

The usual transmission-line formulas provide starting dimensions, not a solver validation target:

import numpy as np
C0 = 299_792_458.0
f0 = 2.4e9
eps_r = 4.4
h = 1.5e-3
tan_d = 0.02
W = C0 / (2 * f0) * np.sqrt(2 / (eps_r + 1))
eps_eff = (eps_r + 1) / 2 + (eps_r - 1) / 2 * (1 + 12 * h / W) ** -0.5
dL = 0.412 * h * (
(eps_eff + 0.3) * (W / h + 0.264)
/ ((eps_eff - 0.258) * (W / h + 0.8))
)
L = C0 / (2 * f0 * np.sqrt(eps_eff)) - 2 * dL
# Constant-conductivity approximation to tan(delta) at f0 only.
sigma_fr4 = 2 * np.pi * f0 * 8.854_187_8128e-12 * eps_r * tan_d
print(f"L={L * 1e3:.2f} mm, W={W * 1e3:.2f} mm")

FR4 permittivity and loss vary by material system, frequency, copper roughness, and manufacturing process. Replace these nominal values with the data needed by the intended comparison.

The model keeps the structure outside eight CPML cells, uses six cells through the substrate, and grades between 1 mm air cells and 0.25 mm substrate cells with adjacent ratios below 1.3.

from rfx import Box, GaussianPulse, Simulation
dx = 1.0e-3
dz_sub = h / 6
# 1.0 mm -> 0.25 mm transition; max adjacent ratio is below 1.3.
transition = np.array([1.000, 0.780, 0.610, 0.477, 0.373, 0.291, 0.250]) * 1e-3
lower_air = np.concatenate([np.full(10, dx), transition[1:]])
upper_air = np.concatenate([transition[-2::-1], np.full(24, dx)])
dz_profile = np.concatenate([lower_air, np.full(6, dz_sub), upper_air])
z_sub_lo = float(lower_air.sum())
z_sub_hi = z_sub_lo + h
dom_x, dom_y = 0.100, 0.095
sim = Simulation(
freq_max=4e9,
domain=(dom_x, dom_y, 0.0), # z extent is the sum of dz_profile
dx=dx,
dz_profile=dz_profile,
boundary="cpml",
cpml_layers=8,
)

Do not specify an independent nonzero z-domain length with a dz_profile; its sum defines the z extent. Preserve 1 mm first and last cells so the z-boundary cell size matches dx.

The ground and patch are subcell sheets. A zero-thickness Box passed to sim.add() would rasterize no volume, so the sheets use add_thin_conductor() instead.

sim.add_material("fr4_design", eps_r=eps_r, sigma=sigma_fr4)
gx_lo, gx_hi = 0.020, 0.080
gy_lo, gy_hi = 0.020, 0.075
sim.add(
Box((gx_lo, gy_lo, z_sub_lo), (gx_hi, gy_hi, z_sub_hi)),
material="fr4_design",
)
# Copper values above 1e6 S/m are routed to the PEC sheet mask.
ground = Box((gx_lo, gy_lo, z_sub_lo), (gx_hi, gy_hi, z_sub_lo))
sim.add_thin_conductor(ground, sigma_bulk=5.8e7, thickness=35e-6)
px0 = dom_x / 2 - L / 2
px1 = dom_x / 2 + L / 2
py0 = dom_y / 2 - W / 2
py1 = dom_y / 2 + W / 2
patch = Box((px0, py0, z_sub_hi), (px1, py1, z_sub_hi))
sim.add_thin_conductor(patch, sigma_bulk=5.8e7, thickness=35e-6)
src_z = z_sub_lo + 2.5 * dz_sub
feed = (px0 + 8e-3, dom_y / 2, src_z)
probe = (dom_x / 2 + 5e-3, dom_y / 2 + 5e-3, src_z)
sim.add_source(feed, "ez", waveform=GaussianPulse(f0=f0, bandwidth=0.8))
sim.add_probe(probe, "ez")
sim.preflight(strict=True)

The thin-conductor calls warn that the copper sheet is routed to PEC. That is the represented model: conductor skin depth, surface roughness, and finite copper loss are not included. A finite sheet-resistance approximation requires an explicitly justified sigma_bulk < 1e6 S/m.

4. Check execution before interpreting RF results

Section titled “4. Check execution before interpreting RF results”

A short run verifies execution and finite probe data only:

import numpy as np
short_run = sim.run(num_periods=2)
assert short_run.time_series.size > 0
assert np.isfinite(np.asarray(short_run.time_series)).all()

It is too short to establish resonance or Q. For a resonance study, use a much longer fixed window, discard the driven portion, and handle the case where find_resonances(...) returns no modes. Repeat with finer x/y and z meshes, larger air margins, and longer windows. Report the movement of the selected mode, not just the finest result.

The short run may emit the ring-down-truncated advisory. That is expected for this short window and is another reason not to interpret its spectrum.

The source above is a soft field source, not a 50-ohm feed. It cannot establish return loss. An impedance-referenced study must use the matching port family and calculator from Sources and Ports and remain within its support limits.

From a source checkout, validation/crossval/05_patch_antenna.py runs the repository’s rfx/OpenEMS patch comparison. OpenEMS and CSXCAD are separate requirements. If either is unavailable, the script reports a skipped comparison and exits with status 2, not a pass. The script’s evidence applies only to its committed geometry, mesh, frequency band, metric, and tolerances. Do not transfer its frequency or Q to the model above or to a modified feed without a new comparison.

See Cross-Validation and Accuracy for evidence levels and S-Parameters and Ports for calculator-specific limits.

For the ordered learning sequence, continue with Examples. A source checkout lists the exact tutorial order in examples/README.md; the cross-validation script above remains an accuracy fixture rather than a lesson.