Accidental antennas

Antenna design is one of those witchcrafts you either know how to do, or you just don’t. I wanted to understand: is it possible to trade years of skill for a bunch of high-end GPUs, and brute-force functional antenna designs?

Accidental antennas

Introduction

Let’s start by addressing the elephant in the room. I know quite literally nothing about antenna design. I have a VNA and know how to measure the specs of an antenna if needed, but that’s about it.

If I need to create an antenna for a PCB, it’s a direct copy/paste from a chip vendor reference design or from the internet, and I hope it works. The issues begin when you need an antenna for a specific frequency range, you’re constrained by your PCB space, and there just isn’t a ready-made design that fits the bill. This is exactly how this project got started.

As with my other projects, the code for this project is available with MIT license. View the code in github

Rocky road

I started where anyone starts these days: asking an LLM what antenna design software is out there. Many open source tools and even more commercial ones. I downloaded a bunch of them and pretty quickly realized how little I actually understand about the topic. I walked away not knowing which is more difficult: using these FEM and FDTD simulation tools, or actually designing the antenna. Also, overwhelmingly, the tools are CPU-based — meaning difficult to get massive parallelism.

I continued scouring the internet and ended up in this black hole where designing your own bespoke antenna seemed every day less likely to happen.

Until I came across a paper from the University of Hannover where a research team had ported an FDTD tool (FDTDX) to JAX for GPUs. Their tool was designed to inverse-design photonic nanostructures, but what caught my eye is that it uses the same Maxwell equations that apply to RF design. This was a light-bulb moment — a glimmer of hope. What if I could somehow turn this tool into something I could use for RF design?

My physics knowledge is about on the same level as my antenna design knowledge, so the only way forward was to go full YOLO with LLMs. Luckily, I’m pretty good at explaining what I want, and years of trying to understand how other people’s things work means I know how to debug.

tmux session

Armed with a tmux session with Claude Code and a couple of OpenAI Codex agents, I put the AI to work: build me software I can use to brute-force antenna designs using fast GPU clusters. I’ll write the AI part as a separate blog post — there’s a lot to go over in that alone, many learnings and workflow tips. Let’s keep this one about antennas.

From optics to copper

The original FDTDX tool is built for optics simulations, so the useful bits for me were the FDTD simulation engine and the gradient solver. Using those as scaffolding, I got the LLMs to build me a physical model for FR-4 PCB material and copper, and integrate that with the rest of the FDTDX codebase.

Getting the code implemented and running was the easy part. Trying to figure out whether the results from the early runs were actually correct… wasn’t.

I quickly realized I needed other simulation software to validate my results. Enter openEMS. I got the LLMs to create a validation layer, mimicking the same material physics in openEMS, which allowed me to use it as a sort of virtual VNA for the FDTDX code. For every FDTDX design, I would fix the feedline, run a virtual SOL calibration using openEMS as my virtual VNA, and then use that as the ground truth for the FDTDX gradient solver.

nvtop

Now, for the solver itself to work, it requires a function that rewards it when it gets closer to the goals and penalizes it when designs don’t meet the targets — basically steering every design decision it makes.

Tuning that reward function didn’t turn out to be terribly simple. It was extremely difficult to let the solver roam free and explore designs while, at the same time, pushing it to fine-tune designs that looked promising. Making things worse, designs that looked absolutely terrible in the beginning would sometimes magically start to work perfectly later on. So figuring out when to kill off designs became one of the big challenges.

For this I came up with a tiered approach: a warmup period with broad exploration and small penalties, followed by gradually increasing rewards and penalties as the epochs progress. The idea is to strike a balance between going wild initially and then tightening toward a functional antenna.

The software

The code is written entirely by a farm of Claude and Codex agents. As such, I’ll let them say a few words about the software stack itself and how it works. I honestly don’t have any clue.

Start of Claude and Codex generated explanation

The optimiser sits on top of FDTDX, a JAX-based GPU electromagnetic solver that treats a 40 mm × 40 mm copper layer as a grid of pixels (the exact count depends on the chosen simulation resolution), each of which can be copper or air. A differentiable loss function lets us compute gradients with respect to every pixel and update them with Adam, effectively brute-forcing antenna topology through gradient descent.

The loss combines a return-loss term derived from S11 with penalty terms for impedance mismatch, low radiation efficiency, poor front-to-back ratio, and residual “grayness” — because gradient-based optimisation wants continuous values, each pixel lives in a gray zone between zero and one during training.

We binarise gradually using a tanh projection filter whose steepness parameter β ramps up across a series of scheduled stages — first a gentle warm-up where penalties are low and the geometry can explore freely, then increasingly aggressive shaping and binarisation phases that push every pixel toward a manufacturable 0-or-1 state, with the impedance, efficiency and binarisation penalties ramping up progressively so they don’t overwhelm the optimiser before it has found a viable resonant structure.

Every few epochs a cheaper CPU evaluation re-simulates the current best design at medium fidelity to catch cases where the GPU solver’s fast approximations are misleading, and the top candidates are handed to OpenEMS — an independent open-source FDTD solver — for final validation.

Because the FDTDX microstrip feed port introduces a systematic measurement bias (much like a real vector network analyser), we built a calibration pipeline: we simulated known reference structures (open, short, matched load) in both solvers, fitted a bilinear Möbius error model with complex A, B, C coefficients at each frequency bin, and now apply that correction to every raw S11 reading — essentially using OpenEMS as a virtual VNA to de-embed the FDTDX port, which closed a gap of up to 12 dB between the two solvers and finally gave the optimiser truthful gradient signals to work with.

End of Claude and Codex generated explanation

This didn’t seem to be an entirely easy task for the LLMs either, and I spent pretty much all evenings over roughly 3 months getting the code to work. Some days I’d see the LLMs doing 8+ hour continuous runs trying to understand how to fix a single issue.

Letting it rip

Once I was fairly confident in the results, I leased a cluster of Nvidia B200 GPUs (192GB VRAM each) and let it run for 24 hours in one go. I ended up with 28 antenna designs with roughly the same specs. Out of those I selected 3 at random for manufacturing and got the PCBs made. I didn’t choose “the best” designs — the whole point of this initial comparison was to validate whether I’m getting the same results in real life as I was getting from simulation.

Here are the specs I was trying to achieve and the constraints:

  • f0 = 5.5 GHz
  • S11 below −10 dB for the 5.15–5.85 GHz band
  • F/B over 6dB (any direction from the middle of the main beam)
  • Impedance matching to 50 ohm, with Zimg as close as possible to 0 at f0
  • PCB: FR-4, 1.6 mm, 1 oz copper, two layers
  • Design area: 40mm x 40mm

At medium resolution this took around ~90GB of VRAM per GPU.

I tested epoch times across different GPUs, but ultimately settled on B200 as it provided the best speed-to-price balance:

  • A100: epoch time ~16 min
  • RTX 6000 Pro: ~12 min
  • B200: ~4 min

A full run (if not dropped early) is 100 epochs, with CPU simulations on projected non-gray copper every 10 epochs, plus openEMS cross-validations every 10 epochs when design parameters are met.

None of these designs actually made it to the final epoch — the gradient solver found what it considered the optimal solution earlier, meaning there was no further improvement after a certain point. The designs were marked as completed and the results from the best epoch were saved.

The moment of truth

The results were surprising. I had to calibrate my VNA multiple times because I just couldn’t believe them. The simulation seems to actually slightly under-predict the performance of the given design.

These are not great antennas by any means — they didn’t even finish the full brute-force pipeline — but this is a validation that the software can indeed produce designs that match the real world.

First manufactured designs

The designs I chose were aptly named (from top to bottom) 7fc, b0c and fd9. Each design has the same directional ground plane on the bottom side. Below is the breakdown of the results.

Real world measurement results

7fc b0c fd9
f0 5.5Ghz 5.5Ghz 5.5Ghz
S11 min in band (dB) -17.3 @ 5.15Ghz -22.5 @ f0 -16.0 @ 5.82Ghz
S11 @ f0 (dB) -8.8 -22.5 -4.5
Zreal + Zimg (ohm) @ f0 33.1+j24 46.0+j0.9 20.4+j35.9
SWR min in band 1.30 @ 5.15Ghz 1.16 @ f0 1.38 @ 5.82Ghz
SWR @ f0 2.08 1.16 3.81

7fc 7fc091802059 (7fc)

b0c b0cce7951178 (b0c)

fd9 fd933fa66a4f (fd9)

Simulation measurement results

f0 = 5.5Ghz 7fc b0c fd9
S11 min in band (dB) -15.5 @ 5.15Ghz -17.8 @ 5.8Ghz -15.2 @ 5.82Ghz
S11 @ f0 (dB) -7.5 -11.4 -5.8
Zreal + Zimg (ohm) @ f0 94.2+j13 86.51+j3.24 96+j14.6

At some point I’ll put these antennas into a proper EMC chamber to test whether the beam patterns, gain, and emission properties match too. The plan is also to manufacture a few more batches of various designs to collect more real-world data and hopefully use that to improve the calibration and push simulation results closer to reality.

There are other things I learned from measuring the manufactured designs too. For example, the simulation needs more sampling points for the S11 calculations — this alone should push the matching closer. Currently the simulation only uses 72 points, whereas the VNA measurements used 201.

Thoughts..

I feel like what I’ve stumbled upon is just the beginning. There are so many things left to explore: the initial geometries where the gradient solver starts, the reward function, gray copper projections, trying out different PCB and copper thicknesses. There’s a lot I haven’t even properly played with yet.

When you’re brute-forcing designs using a gradient function with properly set rewards and penalties, you’re sort of doing reinforcement learning in a way. And as Richard Sutton has pointed out, one of the biggest limitations we have is our own understanding. A human wouldn’t ever design an antenna like some of the ones the brute-force gradient algorithm comes up with, because it’s extremely difficult to understand why they work. I think this is exactly why we’re constrained to yagis, dipoles, patches, and the like — they’re easy to understand and simulate, but that also limits what we can discover.

With this tool, as long as you set the targets, constraints, and basic parameters — and they’re not wildly off (like physically impossible) — the gradient-based brute forcing will find some solution that matches as well as possible.

This experiment has been eye-opening for many reasons. Not only does it highlight the somewhat scary capabilities LLMs are starting to have, but it also raises a more interesting question: what can’t you replace with massively parallel, ridiculously powerful GPUs?

I’m not trying to say antenna designers are out of a job now. But what I would like to understand is: if we get designs out of tools like this that work, but can’t really be comprehended by humans… do we need to understand why they work? Especially if the design was done to meet specific requirements, and it meets them.

Where to go from here

I’ve been running these simulations for hundreds and hundreds of epochs, and at some point I started to think: while getting the final designs is nice, why wouldn’t I also collect all of the data from every epoch?

So as a final addition I bolted on a logging feature that takes the geometries and simulation results from each epoch and stores them in H5 files. Hopefully those can, at some point, be used to train a CNN that massively speeds up the initial design process — and then use the FDTDX code only for fine-tuning via the gradient solver.

Out of the data I’ve already collected, I’ve done some initial analysis. Based on that it looks like there are clear patterns, and those should be possible to generalize given enough data — my estimates land somewhere north of 20,000 epochs. In addition, I estimate that 2,000–5,000 random single epochs should also be collected, as the data from the actual runs has a fair amount of correlation between consecutive epochs and might otherwise pollute the dataset.

Resources

The code for the tool is available with MIT license on my github View the code in github