REMI Lab
OPEN KNOWLEDGE / TOOLS

Your First Patch Antenna

이 페이지의 내용

This tutorial builds a real device: a 2.4 GHz rectangular patch on a finite FR4 board. It teaches four things you will need for your own models:

  • sizing a starting design from closed-form formulas;
  • a graded z mesh that resolves a thin substrate without refining the whole domain;
  • declaring copper as sheets;
  • reading preflight output, and knowing what a short run can and cannot tell you.

It ends with a quick execution check. Reporting a resonance, a return loss or a Q needs more than that, and the last section lists what.

1. Starting dimensions

The usual transmission-line model gives a first guess for the patch length L and width W:

import numpy as np
C0 = 299_792_458.0
f0 = 2.4e9 # design frequency
eps_r = 4.4 # nominal FR4
h = 1.5e-3 # substrate thickness
tan_d = 0.02 # nominal FR4 loss tangent
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
# A constant conductivity that matches 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")

These formulas give a starting point, not a reference to validate the solver against. Real FR4 permittivity and loss depend on the laminate and the frequency, so use your material’s data.

2. A graded z mesh

The substrate is 1.5 mm thick. Resolving it with 6 cells needs 0.25 mm cells, but the air around the antenna is fine at 1 mm. A dz_profile lists the z cell sizes from bottom to top, so you can use fine cells only where you need them:

from rfx import Box, GaussianPulse, Simulation
from rfx.mesh_edges import edge_aware_profiles
dx = 1.0e-3 # nominal x and y cell size
dz_sub = h / 6 # 6 cells through the substrate
# Grade 1.0 mm -> 0.25 mm with each neighbour ratio 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()) # substrate bottom
z_sub_hi = z_sub_lo + h # substrate top
dom_x, dom_y = 0.100, 0.095
gx_lo, gx_hi = 0.020, 0.080
gy_lo, gy_hi = 0.020, 0.075
px0, px1 = dom_x / 2 - L / 2, dom_x / 2 + L / 2
py0, py1 = dom_y / 2 - W / 2, dom_y / 2 + W / 2
ground = Box((gx_lo, gy_lo, z_sub_lo), (gx_hi, gy_hi, z_sub_lo))
substrate = Box((gx_lo, gy_lo, z_sub_lo), (gx_hi, gy_hi, z_sub_hi))
patch = Box((px0, py0, z_sub_hi), (px1, py1, z_sub_hi))
feed_x, feed_y = px0 + 8e-3, dom_y / 2
probe_x, probe_y = dom_x / 2 + 5e-3, dom_y / 2 + 5e-3
profiles = edge_aware_profiles(
(dom_x, dom_y, float(dz_profile.sum())), dx,
sheets=[ground, patch], solids=[substrate],
faces={"x": [feed_x, probe_x], "y": [feed_y, probe_y]},
axes="xy",
)
sim = Simulation(
freq_max=4e9,
domain=(dom_x, dom_y, 0.0), # z size comes from sum(dz_profile)
dx=dx,
dz_profile=dz_profile,
**profiles,
boundary="cpml",
cpml_layers=8,
)

Three rules for graded meshes:

  • Keep neighbouring cell sizes within a ratio of about 1.3.
  • Start and end the profile with cells equal to dx, so the absorber sees the same cell size on every face.
  • Put material interfaces exactly on cell boundaries. Here the substrate starts at sum(lower_air) for that reason.

Non-Uniform Mesh covers graded meshes in depth.

3. Board, copper, source and probe

The ground plane and the patch are copper foil, which rfx models as sheets of zero thickness on one grid plane (see Core Concepts):

sim.add_material("fr4_design", eps_r=eps_r, sigma=sigma_fr4)
sim.add(substrate, material="fr4_design")
sim.add_thin_conductor(ground)
sim.add_thin_conductor(patch)
# A soft source under the patch, 8 mm in from the edge, and an offset probe.
src_z = z_sub_lo + 2.5 * dz_sub
sim.add_source((feed_x, feed_y, src_z), "ez",
waveform=GaussianPulse(f0=f0, bandwidth=0.8),
amplitude_kind="current")
sim.add_probe((probe_x, probe_y, src_z), "ez")
report = sim.preflight()
print(report.format())

Reading what rfx tells you

Each add_thin_conductor call warns that the sheet is a lossless PEC sheet, because copper’s conductivity is above the 1e6 S/m PEC threshold. The patch therefore has no conductor loss. If conductor loss matters for your Q or efficiency, pass surface_impedance_f0=f0. The sheet is then modelled as a resistive sheet with copper’s surface resistance at f0, on the same footprint, instead of as PEC.

The in-plane edge_aware_profiles mesh registers the ground and patch to their solved edges and the feed and probe to grid nodes. This keeps the drawn patch dimensions without rounding them to a uniform lattice. Read sim.fidelity_report() and the preflight findings to check that realization; registration does not replace a mesh-convergence study.

This edge-aware in-plane grading raises four nu_grading_reaches_absorber advisories because the interior cells adjacent to the x/y absorber faces are not uniform. The grading changes the discrete medium normal to the boundary where the absorber starts, leaving no interior runway to separate the transition’s reflection from the absorber’s, so this example is outside the validated multi-band accuracy envelope.

Treat preflight warnings as part of your result. Use preflight(strict=True) only for models you expect to have no findings at all, because strict mode turns every finding into an error.

4. A short execution check

short_run = sim.run(num_periods=2)
ts = np.asarray(short_run.time_series)
assert ts.size > 0 and np.isfinite(ts).all()
print(f"{ts.shape[0]} steps, settling {short_run.settling_db:.1f} dB")

This shows that the model runs and produces finite data. Two periods is far too short for the patch to ring down, so rfx warns that the ring-down is truncated. Nothing from this run is a result.

5. From a model to a result

To report a resonance, Q or return loss for this patch:

  1. Run long enough. Use until_decay= or a long fixed record, and check result.settling_db. A patch has a moderately high Q, so it needs many periods.
  2. Extract the resonance with result.find_resonances(freq_range=(2e9, 3e9)). Pick the mode by its field pattern or far field, not by which peak is biggest. Handle the case where no mode is found.
  3. Refine the mesh in x/y and z, and enlarge the air margin. Report how much the frequency moves, not just the finest answer. See Convergence Study.
  4. For return loss, use a port. The soft source here has no impedance, so it cannot give S11. Use a lumped or microstrip port and its calculator from Sources and Ports.
  5. Compare with a reference. Use another solver or a measurement. Benchmarks shows how rfx’s own patch case compares with openEMS.

examples/tutorials/patch_antenna_demo.py carries out this workflow end to end, including far-field mode identification. Examples lists the full learning path.

Back to top ↑