# Your First Patch Antenna

Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723

Page: https://remilab.ai/rfx/examples/first-patch/
Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/examples/first-patch.mdx

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:

```python
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:

```python
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](https://remilab.ai/rfx/model/domain-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](https://remilab.ai/rfx/start/concepts/)):

```python
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

```python
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](https://remilab.ai/rfx/evidence/mesh-convergence/).
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](https://remilab.ai/rfx/model/ports/).
5. **Compare with a reference.** Use another solver or a measurement.
   [Benchmarks](https://remilab.ai/rfx/guide/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](https://remilab.ai/rfx/examples/) lists the
full learning path.
