REMI Lab
OPEN KNOWLEDGE / TOOLS

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

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.

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-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.

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.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 at or 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),
amplitude_kind="current") # required from 1.9
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. Two routes put loss back.

The documented one is add_thin_conductor(shape, sigma_bulk=5.8e7, surface_impedance_f0=f0), which models the sheet as a resistive sheet of sheet resistance Rs0 = sqrt(pi*f0*mu0/sigma_bulk) — the thick-conductor surface resistance at the band centre f0 . Its scope, from the “Surface-impedance sheets” row of docs/guides/support_matrix.md: it “supports patterned shapes that rasterize to exactly one cell layer and at least one cell on its documented uniform and graded-mesh lanes. It is not a general dispersive or multi-runner conductor model.” One frequency, one cell layer.

The cruder route is a finite sheet-resistance approximation through sigma_bulk, which requires an explicitly justified value below 1e6 S/m — that is the threshold at which the sheet stops being routed to PEC, so anything above it silently reverts to the lossless model.

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.

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.

Continue with Examples for the ordered learning sequence; the cross-validation script above is an accuracy fixture, not a lesson.

Back to top ↑