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.
1. Calculate starting dimensions
Section titled “1. Calculate starting dimensions”The usual transmission-line formulas provide starting dimensions, not a solver validation target:
import numpy as np
C0 = 299_792_458.0f0 = 2.4e9eps_r = 4.4h = 1.5e-3tan_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.5dL = 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_dprint(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.
2. Build a graded z mesh
Section titled “2. Build a graded z mesh”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-3dz_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-3lower_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.095sim = 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.
3. Register the stack, source, and probe
Section titled “3. Register the stack, source, and probe”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.080gy_lo, gy_hi = 0.020, 0.075sim.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 / 2px1 = dom_x / 2 + L / 2py0 = dom_y / 2 - W / 2py1 = dom_y / 2 + W / 2patch = 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_subfeed = (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 > 0assert 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.
5. External comparison
Section titled “5. External comparison”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.