128 files · 3.7 MB at 05eecdc Download ZIP
.dockerignore 72 B
.gitignore 170 B
CLAUDE.md 11.0 KB
Dockerfile 3.2 KB
fp-lib-table 152 B
pyproject.toml 497 B
QFP58-feed-RHCP-drc.rpt 553 B
QFP58-feed-RHCP.kicad_pcb 108 KB
QFP58-feed-RHCP.kicad_pro 9.6 KB
README.md 15.5 KB
run.sh 4.2 KB
uv.lock 189 KB
docs/
design_report.json 5.3 KB
preview_LHCP.png 195 KB
preview_RHCP.png 195 KB
docs/svg/
QFP58-feed-LHCP-B_Cu.svg 134 KB
QFP58-feed-LHCP-Edge_Cuts.svg 11.5 KB
QFP58-feed-LHCP-F_Cu.svg 61.9 KB
QFP58-feed-RHCP-B_Cu.svg 131 KB
QFP58-feed-RHCP-Edge_Cuts.svg 11.6 KB
QFP58-feed-RHCP-F_Cu.svg 61.9 KB
QFP58-patch-B_Cu.svg 37.2 KB
QFP58-patch-Edge_Cuts.svg 11.9 KB
QFP58-patch-F_Cu.svg 16.3 KB
gerbers/
QFP58-feed-LHCP-fab.zip 31.5 KB
QFP58-feed-RHCP-fab.zip 31.6 KB
QFP58-patch-d23.06-fab.zip 12.0 KB
QFP58-patch-d23.36-fab.zip 12.0 KB
QFP58-patch-fab.zip 11.7 KB
gerbers/QFP58-feed-LHCP/
QFP58-feed-LHCP-B_Cu.gbl 4.2 KB
QFP58-feed-LHCP-B_Mask.gbs 1.6 KB
QFP58-feed-LHCP-B_Silkscreen.gbo 41.5 KB
QFP58-feed-LHCP-Edge_Cuts.gm1 17.0 KB
QFP58-feed-LHCP-F_Cu.gtl 47.5 KB
QFP58-feed-LHCP-F_Mask.gts 1.2 KB
QFP58-feed-LHCP-F_Silkscreen.gto 1.2 KB
QFP58-feed-LHCP-job.gbrjob 2.4 KB
QFP58-feed-LHCP-NPTH.drl 383 B
QFP58-feed-LHCP-PTH.drl 530 B
gerbers/QFP58-feed-RHCP/
QFP58-feed-RHCP-B_Cu.gbl 4.2 KB
QFP58-feed-RHCP-B_Mask.gbs 1.6 KB
QFP58-feed-RHCP-B_Silkscreen.gbo 40.6 KB
QFP58-feed-RHCP-Edge_Cuts.gm1 17.0 KB
QFP58-feed-RHCP-F_Cu.gtl 47.5 KB
QFP58-feed-RHCP-F_Mask.gts 1.2 KB
QFP58-feed-RHCP-F_Silkscreen.gto 1.2 KB
QFP58-feed-RHCP-job.gbrjob 2.4 KB
QFP58-feed-RHCP-NPTH.drl 383 B
QFP58-feed-RHCP-PTH.drl 536 B
gerbers/QFP58-patch/
QFP58-patch-B_Cu.gbl 21.0 KB
QFP58-patch-B_Mask.gbs 653 B
QFP58-patch-B_Silkscreen.gbo 663 B
QFP58-patch-Edge_Cuts.gm1 640 B
QFP58-patch-F_Cu.gtl 937 B
QFP58-patch-F_Mask.gts 810 B
QFP58-patch-F_Silkscreen.gto 9.4 KB
QFP58-patch-job.gbrjob 2.4 KB
QFP58-patch-NPTH.drl 264 B
QFP58-patch-PTH.drl 375 B
gerbers/QFP58-patch-d23.06/
QFP58-patch-d23.06-B_Cu.gbl 21.0 KB
QFP58-patch-d23.06-B_Mask.gbs 660 B
QFP58-patch-d23.06-B_Silkscreen.gbo 670 B
QFP58-patch-d23.06-Edge_Cuts.gm1 647 B
QFP58-patch-d23.06-F_Cu.gtl 944 B
QFP58-patch-d23.06-F_Mask.gts 817 B
QFP58-patch-d23.06-F_Silkscreen.gto 10.5 KB
QFP58-patch-d23.06-job.gbrjob 2.4 KB
gerbers/QFP58-patch-d23.36/
QFP58-patch-d23.36-B_Cu.gbl 21.1 KB
QFP58-patch-d23.36-B_Mask.gbs 660 B
QFP58-patch-d23.36-B_Silkscreen.gbo 670 B
QFP58-patch-d23.36-Edge_Cuts.gm1 647 B
QFP58-patch-d23.36-F_Cu.gtl 944 B
QFP58-patch-d23.36-F_Mask.gts 817 B
QFP58-patch-d23.36-F_Silkscreen.gto 10.1 KB
QFP58-patch-d23.36-job.gbrjob 2.4 KB
kicad/QFP58-feed-LHCP/
fp-lib-table 149 B
QFP58-feed-LHCP-drc.rpt 553 B
QFP58-feed-LHCP.kicad_pcb 108 KB
QFP58-feed-LHCP.kicad_pro 9.6 KB
kicad/QFP58-patch/
fp-lib-table 149 B
QFP58-patch-drc.rpt 576 B
QFP58-patch.kicad_pcb 36.3 KB
QFP58-patch.kicad_pro 9.6 KB
kicad/QFP58-patch-d23.06/
fp-lib-table 149 B
QFP58-patch-d23.06-drc.rpt 528 B
QFP58-patch-d23.06.kicad_pcb 36.3 KB
QFP58-patch-d23.06.kicad_pro 9.5 KB
kicad/QFP58-patch-d23.36/
fp-lib-table 149 B
QFP58-patch-d23.36-drc.rpt 528 B
QFP58-patch-d23.36.kicad_pcb 36.4 KB
QFP58-patch-d23.36.kicad_pro 9.5 KB
kicad/QFP58.pretty/
CouplingDisk_3.6mm.kicad_mod 1.2 KB
FeedPin_1.1mm.kicad_mod 1.0 KB
MountingHole_2.2mm_M2.kicad_mod 1.0 KB
R_0603_Wilkinson.kicad_mod 1.2 KB
SMA_EdgeLaunch_1.0mm_generic.kicad_mod 1.7 KB
sim/full/
full_cuts_5.65GHz.png 86.6 KB
full_cuts_5.80GHz.png 87.0 KB
full_cuts_5.95GHz.png 88.6 KB
full_farfield_5.65GHz.npz 147 KB
full_farfield_5.80GHz.npz 147 KB
full_farfield_5.95GHz.npz 147 KB
full_pattern_5.65GHz.png 79.2 KB
full_pattern_5.80GHz.png 80.5 KB
full_pattern_5.95GHz.png 83.6 KB
full_pattern3d_5.65GHz.png 154 KB
full_pattern3d_5.80GHz.png 156 KB
full_pattern3d_5.95GHz.png 155 KB
full_sparams.csv 3.2 KB
full_sparams.png 30.3 KB
summary.json 1.5 KB
sim/network/
network_sparams.csv 12.9 KB
network_sparams.png 45.0 KB
summary.json 286 B
sim/patch/
patch_ideal_feed_pattern_5.65GHz.png 69.2 KB
patch_ideal_feed_pattern_5.80GHz.png 69.5 KB
patch_ideal_feed_pattern_5.95GHz.png 70.5 KB
patch_sparams.png 48.0 KB
summary.json 1.4 KB
src/
generate.py 1.9 KB
kicad_build.py 13.9 KB
qfp_geom.py 11.6 KB
qfp_layers.py 6.9 KB
qfp_sim.py 27.4 KB
# CLAUDE.md — QFP-58 antenna project
Context for anyone (human or model) picking this repo up cold.
## What this is
A parametric generator for **QFP-58**: a 5.8 GHz circularly polarised patch antenna for FPV/UAV links.
Two FR4 boards separated by a 4 mm air gap; four probe feeds at 90° spacing, driven 0/−90/−180/−270°
(sequential rotation) by a two-stage Wilkinson divider on the back of the lower board.
Everything — KiCad boards, Gerbers, SVGs, openEMS models — is generated from one geometry description.
**Never hand-edit the outputs.** Change `P` in `qfp_geom.py` and regenerate.
Concept credit: the Triple Feed Patch by Maarten Baert & Robin Theunis (CC BY-SA 4.0). Three feeds at 120°,
same two-board construction. No TFP files were used; all geometry here is computed from scratch.
Their measured reference: 9.3 dBi, 1.21 dB axial ratio, 74 % efficiency, ~800 MHz bandwidth.
## Layout
```
src/qfp_geom.py parameters + network geometry as primitives (segments, arcs, resistor/pad/via poses)
src/qfp_layers.py shapely layer polygons, SVG export, matplotlib preview
src/kicad_build.py native KiCad 7 boards via the pcbnew Python API (tracks/arcs/zones/footprints)
src/generate.py one-shot: report JSON, SVGs, previews, KiCad boards, Gerbers + drills, fab zips
src/qfp_sim.py openEMS models: patch / network / cascade / full
QFP58-feed-RHCP.kicad_pro/.kicad_pcb + fp-lib-table the one board in the root (showcase for the PCB-hosting repo,
which can only handle a single PCB in the root)
kicad/<board>/ every other board, one project per folder; kicad/QFP58.pretty shared by all (relative lib tables)
gerbers/ docs/{svg,preview_*.png,design_report.json} sim/ archive/ README.md
```
`qfp_geom.build(hand)` returns `(feed, info)`. `feed['prims']` drives all three consumers, so a geometry change
propagates to boards, previews and simulations at once. LHCP = the RHCP geometry mirrored about the x axis.
## Conventions
- Units mm, angles radians internally, degrees in reports.
- Origin = antenna axis. **Top view** (looking at the patch from the radiating side), x right, y up.
Bottom-layer features are given in top-view coordinates, as Gerber and KiCad do.
- Board placement lives in `kicad_build.board_path()` (`SHOWCASE` board in the root, the rest in `kicad/<name>/`);
keep the root to one PCB.
- KiCad mapping is `(x, y) → (X0 + x, Y0 − y)` with the board centred at (150, 100) on the page.
- z stack in the simulations: −1 = feed-board bottom copper (network), 0 = ground plane, 4 = patch copper,
5 = coupling disks.
- Pin/port index k sits at angle k·90° and carries phase −k·90°.
## Current design (rev C)
| Parameter | Value | Note |
|---|---|---|
| `patch_d` | 22.76 mm | patch disc, bottom of the upper board; variants 23.06 / 23.36 mm |
| `gap_air` | 4.00 mm | copper to copper, ±0.1 mm target |
| `rp` | 4.00 mm | feed pin radius, 1.0 mm Cu wire |
| `z_match` / `match_len` | 100 Ω / 2.0 mm | thin series-inductive section at each pin |
| `z_out` | 50 Ω | divider outputs after the matching section |
| `z_br` / `z_in` | 60 / 50 Ω | branches, SMA input |
| `z_w1` = `z_w2` | 77.46 Ω | both divider arm sets, √(2·Z_in·Z_L) |
| `r1` / `branch_clear` | 9.0 / 2.0 mm | resistor radius, branch-to-ring clearance |
| `phase_corr2` / `phase_corr1` | 1.074 / 0.972 | openEMS-calibrated differential arc lengths |
| `patch_antipad` / `patch_pin_ring` | 2.2 / 1.4 mm | 0.4 mm gap; 0.2 mm was a solder-bridge risk |
| R1 / R2 / R3 | 120 / 100 / 100 Ω | 0603, stage 1 / stage 2 |
Line widths on 1.0 mm FR4 (εr 4.4): 50 Ω → 1.883, 60 Ω → 1.357, 77.5 Ω → 0.794, 100 Ω → 0.403 mm.
## Design decisions worth keeping
- **Four feeds, not three.** Sequential rotation with 90° steps excites only modes n ≡ 1 (mod 4), cancelling
TM01, TM21 and the counter-rotating TM11. A 3-feed version leaves TM21 in play.
- **Wilkinson, not a branch-line coupler.** The isolation resistors absorb what the patch reflects, so S11 at the
SMA stays under −10 dB across the whole 5.0–6.6 GHz sweep even though each pin is only matched to ~−15 dB.
- **The cavity model was wrong about impedance.** It predicted 100 Ω per pin; FDTD shows 40–50 Ω, because the
patch and the pin/disk circuit form a coupled pair with two resonances and a valley in between. Kretzschmar's
effective-radius formula also overestimates fringing at h/a = 0.35. **Trust the simulation, not the formula.**
- **The 2 mm of 100 Ω line at each pin is a matching element**, not routing. It turns ≈41 − j30 Ω into ≈48 Ω.
- **Divider outputs leave the resistor pads at 45°** so the two ports don't merge, and lead length is larger for
wide (50 Ω) outputs so they clear the ring.
## Workflows
```bash
./run.sh # = --build: boards (RHCP root, rest kicad/), DRC, Gerbers, docs/
./run.sh --sim # full model, coarse mesh (~4 min): quick whole-antenna check
./run.sh --fullsim # full model, medium mesh (~10 min): the reference result
./run.sh --build --fullsim --cpus 5 --detach # combine; background; limit cores (see ./run.sh --help)
./run.sh python src/qfp_layers.py RHCP # docs/preview_RHCP.png only
./run.sh python src/qfp_sim.py patch --res fine # 1.7 M cells, patch impedance + ideal-feed pattern
./run.sh python src/qfp_sim.py network --res medium # 3.6 M cells, amplitudes/phases/loss at the pins
./run.sh python src/qfp_sim.py cascade # combines the two, no FDTD
./run.sh python src/qfp_sim.py full --res medium # 7.6 M cells, the reference result
./run.sh python src/qfp_sim.py patch --res coarse --no-ff --set patch_d=25.0 rp=4.2 --tag _sweep # fast tuning
./run.sh bash # shell in the build container
```
Tuning loop: sweep on `--res coarse --no-ff` (~90 s each), confirm the winner on `--res fine`, then run `--fullsim`.
`--sim`/`--fullsim` tag results with a timestamp (sim_out/full_<hand>_<res>_<tag>/, log in sim_out/<tag>.log);
for direct `qfp_sim.py` calls always pass `--tag`, or runs overwrite each other. `--sim` (coarse) is good for
gain and S11 but its axial ratio is ~0.4 dB off; quote `--fullsim` numbers.
Environment: everything runs in the Docker image built from `Dockerfile`; the host only needs Docker. The image is
`kicad/kicad:10.0.6` (pcbnew + kicad-cli) plus openEMS v0.0.36 compiled from source (Debian only has 0.0.35) and
a venv at `/opt/venv` with the deps locked in `pyproject.toml` / `uv.lock`. Change a dependency with `uv add`/`uv lock`
(or edit the versions at the top of the Dockerfile); `run.sh` rebuilds the image when those files change.
Boards are saved in KiCad 10 format, which older KiCad won't open. `kicad_build.py` still runs on KiCad 7–9.
The KiCad GND zone is `outline.buffer(-edge_pullback)`, the same plane the SVGs and openEMS use; drawing it on the
raw outline let the fill touch the edge (KiCad 10 DRC). All boards: 0 DRC violations.
## Pitfalls hit already
- **Conducting-sheet model is unstable** on these meshes — energy grows after ~3 ns. Copper is PEC; add
conductor loss analytically (~10 % of the FR4 dielectric loss) if it matters.
- **Mesh lines that nearly coincide** produced 0.0014 mm cells and collapsed the timestep. `make_mesh` now
enforces a minimum spacing; check the "min cell" figure it prints (keep ≥0.05 mm).
- **Isolation resistors must be in the model.** Without them the divider split is wrong. They're lumped
elements, which have to be axis-aligned, so each rotated 0603 becomes a 0.25 mm strip across the pad gap.
- **KiCad DRC cannot catch a gap across a resistor**, since both sides are the same net. An early revision left
0.07 mm. `qfp_geom` now asserts `min_res_gap`.
- **Phase corrections wrap.** A measured +176° is −184°, i.e. too much lag, not too little. Getting this
backwards made rev C worse on the first attempt.
- **The isolated-copper DRC warning on the patch board is intentional** — the patch floats by design, and the
project file sets that check to ignore.
## Verified performance (rev C, full model)
| | 5.65 GHz | 5.80 GHz | 5.95 GHz |
|---|---|---|---|
| RHCP gain, boresight | 8.74 dBi | 9.37 dBi | 9.55 dBi |
| Axial ratio, boresight | 2.11 dB | 0.94 dB | 0.70 dB |
| Axial ratio, mean over HPBW | 2.33 dB | 1.32 dB | 1.32 dB |
| HPBW, narrowest–widest azimuth | 47–54° | 48–52° | 48–50° |
| Radiated / incident | 0.66 | 0.76 | 0.81 |
S11 ≤ −21.4 dB over 5.65–5.95 GHz, < −20 dB from 5.62 to 6.32 GHz. Pin phases 0 / −90.3 / −179.3 / −271.6°. Network loss 1.32 dB.
Accuracy: ±0.5 dB on gain and efficiency; copper loss and the SMA body are not modelled.
Rerun 2026-09-24 in the build container (openEMS v0.0.36, 7.59 Mcells, 588 s); it reproduces the earlier run within
0.03 dB. `report_pattern` also writes `*_pattern3d_*.png` and `*_cuts_*.png` (laid out like the TFP page's 3D
pattern and vertical cross-sections) plus the full-sphere far field as `*_farfield_*.npz`, so plots can be
redrawn without re-simulating. README has the TFP comparison.
## Open items
- Pin amplitudes are −6.89 / −7.54 / −6.87 / −8.28 dB. The quarter-wave offset between each divider's two
outputs makes them see the pin mismatch with opposite phase, so the split goes uneven. This sets the ~1 dB
axial-ratio floor; fixing it needs a different topology, not a parameter tweak.
- The response centres slightly high. `P['patch_variants']` = (23.06, 23.36) builds spare patch boards
(`QFP58-patch-d23.06/-d23.36`, diameter on the silkscreen) to order alongside the 22.76 mm design value; they also
cover FR4 spread between fabs. Coarse-mesh sweep (README table): each +0.3 mm moves
the S11 dip 5.90 → 5.85 → 5.81 GHz; 23.06 balances the band edges (AR over beam 1.62/1.04/1.64 dB vs
1.90/1.14/1.40 for 22.76). Not confirmed on the medium mesh.
JLC builds on Nan Ya NP-155F (εr 4.2-4.4, tan δ 0.014-0.016 @ 1 GHz). At εr 4.2 / 0.016 the response moves up one
0.3 mm step: 23.36 becomes the balanced patch (AR over beam 1.62/0.98/1.65 dB), gain +0.3 dB. For a two-board
order: 23.06 + 23.36. Simulate other laminates with `--set sim_er=… sim_tand=…`: those change only the simulated
material. `sub_er`/`sub_tand` are the design values the line widths are synthesized from; changing them redraws
the lines. Decide from the measured S11.
- The SMA footprint is generic (signal 2.8 × 1.883 mm, ground 3.2 × 1.6 mm at ±2.9 mm). **Check it against the
connector's datasheet before ordering.**
- No hardware built yet. Nothing here is measured.
## House rules
- Both boards must be 1.0 mm FR4; the line widths depend on it.
- Use a removable 4.00 mm gauge when soldering the pins, and leave no dielectric between the plates.
- When reporting simulation results, state the mesh and which model produced them; `patch` and `cascade` numbers
are not interchangeable with `full`.
- Don't claim improvement over the TFP. On simulation this design is level with their measurements, with a
wider input match.