Write your first design
The authoring tour shows the same antenna expressed six different ways — but it assumes you can already write a design. This page is the on-ramp before it: we start from the smallest Builder that runs and grow it one change at a time. By the end you’ll have a tunable dipole and will understand every line.
If you haven’t yet, skim The model — the one idea is that an
antenna is a function from knobs to a list of wires, returned by
build_wires().
1. The smallest thing that runs
Section titled “1. The smallest thing that runs”A dipole is just a straight wire fed in the middle. Here it is, hardcoded — one wire, no geometry parameters at all:
from antennaknobs import AntennaBuilderfrom antennaknobs.network import Wire
class Builder(AntennaBuilder): default_params = { "freq": 14.0, # MHz — measurement frequency "design_freq": 14.0, # MHz — the band it's designed for (anchors the mesh) }
def build_wires(self): y = 5.0 # half-length in metres → a 10 m wire, tip to tip return [ Wire((0.0, -y, 0.0), (0.0, y, 0.0), ex=1 + 0j), ]That’s a complete, loadable design. Drop it in your
designs folder as
my_dipole.py, refresh the workbench, and it appears under Your designs.
Reading the one Wire((0,-y,0), (0,y,0), ex=1+0j) against the
build_wires() contract:
(0,-y,0)→(0,y,0)— the two endpoints (metres): a straight wire along the y axis from −5 m to +5 m, lying on the ground plane (z = 0).- no segment count — you didn’t pass one, so the framework meshes the wire
for you, at a density anchored to
design_freq(step 4 explains the rule — you will never pick a count by hand). ex=1 + 0j— this wire is the driven element; the source sits on it. (A structural, undriven wire is justWire(a, b).) Exactly one segment in the whole design carries the feed — here the wire’s own centre segment.
2. Find resonance with the freq knob
Section titled “2. Find resonance with the freq knob”Load it and look at the Smith chart and the impedance readout. With freq at its
default 14.0 MHz the antenna is capacitive — the reactance reads about
X = −55 Ω (slightly too short for this frequency).
Now turn just the freq knob — no code change. As you raise it, watch the reactance climb toward zero:
| freq (MHz) | R (Ω) | X (Ω) |
|---|---|---|
| 14.0 | 65 | −55 |
| 14.4 | 69 | −22 |
| 14.6 | 71 | ≈ 0 |
| 15.0 | 78 | +44 |
Around 14.6 MHz the reactance passes through zero: that’s resonance, and
the resistance settles near 70 Ω — the signature of a half-wave dipole. (A
10 m wire is about a half wavelength at c / 2L ≈ 15 MHz; it resonates a touch
lower because real wire has end effects.)
3. Add height above ground
Section titled “3. Add height above ground”Right now the wire sits at z = 0, on the ground. Real antennas hang in the air,
and height changes the pattern and the impedance. Promote the height to a knob —
add base to the params and use it as the z coordinate:
class Builder(AntennaBuilder): default_params = { "freq": 14.0, # MHz — measurement frequency "design_freq": 14.0, # MHz — mesh anchor "base": 10.0, # metres — height above ground }
def build_wires(self): y = 5.0 z = self.base # every param is read as self.<name> return [ Wire((0.0, -y, z), (0.0, y, z), ex=1 + 0j), ]Every key in default_params is now a knob in the panel, read inside the build
as self.base. Slide it and watch the pattern lift off the ground. Make sure to enable ground modelling.
4. How the mesh gets made
Section titled “4. How the mesh gets made”That None we’ve been carrying is the segment count — and the reason it’s
None deserves an explanation, because it’s where antenna modelling meets
antenna physics.
The solver is a Method of Moments engine: it chops each wire into short segments, puts an unknown current on each, and solves a linear system for them all at once. So the mesh is an accuracy/cost dial:
- too few segments and the current distribution is too coarse to be trustworthy — the impedance and pattern are wrong;
- too many and the solve is needlessly slow;
- what matters is that each segment is short relative to a wavelength, and that segment length stays roughly constant across every wire so no part of the antenna is under- or over-meshed.
You never manage any of that. A None count means the framework meshes the
wire at the design density:
n = max(1, round(N · length / (λ/4))) # λ from design_freqwhere N is the workbench’s segment slider (nominal_nsegs). N is a
physical unit — N=21 means a segment length of λ/84 on every wire of every
design — and because every None wire gets the same segment length, no part
of the antenna can end up meshed out of step with its neighbours. That
uniformity is not a nicety: hand-picked counts that broke it are the single
most common historical bug in our own design catalog (the full story, with the
same design written both ways, is in
Segmentation you never think about).
The mesh is anchored to design_freq — the band the antenna is designed
for — never to the measurement freq, so sweeping the measurement frequency
can never re-mesh the geometry under you.
Our 10 m wire is about two quarter-waves long at 14 MHz, so at the default N=21 it meshes into 39 segments — and the count will track the length once we make it a parameter.
5. Parameterize the length
Section titled “5. Parameterize the length”The last hardcoded number is y. Make it a knob and the antenna becomes
adjustable — slide it longer to drop the resonant frequency, shorter to raise it:
from antennaknobs import AntennaBuilderfrom antennaknobs.network import Wire
class Builder(AntennaBuilder): default_params = { "freq": 14.6, # MHz — measurement frequency "design_freq": 14.6, # MHz — mesh anchor "base": 10.0, # metres — height above ground "half_length": 5.0, # metres — half the wire, tip to centre "ui_params": {"default_view": "xz"}, }
def build_wires(self): y = self.half_length z = self.base return [ Wire((0.0, -y, z), (0.0, y, z), ex=1 + 0j), ]That ui_params block is optional polish — here it just opens the antenna in the
side-on xz view. Now you have two complementary ways to reach resonance:
move freq to meet the antenna, or move half_length to tune the antenna to the
frequency.
6. Make it frequency-based
Section titled “6. Make it frequency-based”half_length in raw metres works, but it freezes the antenna at one size:
pick a different band and you’re back to hand-tuning the length. The fix is to
tie every dimension to the wavelength of the band you’re cutting for — and
you’ve been carrying exactly the right knob since step 1: design_freq, so far
only anchoring the mesh, now sizes the geometry too.
A half-wave dipole is two quarter-wave arms, so the half-length is one
quarter wavelength — times a length_factor near 1.0 that trims a real wire to
true resonance:
from antennaknobs import AntennaBuilderfrom antennaknobs.network import Wire
class Builder(AntennaBuilder): default_params = { "design_freq": 14.6, # MHz — the band this antenna is cut for "freq": 14.6, # MHz — measurement frequency (starts on band) "length_factor": 0.97, # trims the half-wave to resonance (~97 %) "base": 10.0, # metres — height above ground "ui_params": {"default_view": "xz"}, }
def build_wires(self): wavelength = self.design_wavelength # free-space λ at design_freq, in metres quarter = wavelength / 4.0 y = quarter * self.length_factor # half-length ≈ a quarter wavelength z = self.base return [ Wire((0.0, -y, z), (0.0, y, z), ex=1 + 0j), ]Two things changed, and they’re the whole point:
- The geometry now scales with the band. Out of the box this reads
R ≈ 71 Ω, X ≈ −5 Ωat 14.6 MHz — resonant on the band it’s cut for. Changedesign_freqto 21.2 MHz and the half-length rescales from 4.98 m to 3.43 m and it resonates on 15 m too; 28.4 MHz → 2.56 m on 10 m. One knob moves the whole antenna onto a band. length_factoris the fine-tune. The bare half-wave estimate (length_factor = 1.0) lands a little long — real wire resonates around 97 % of it — solength_factoris the small dimensionless trim the optimiser trips, leavingdesign_freqto do the coarse band placement.
Where to go next
Section titled “Where to go next”You’ve gone from one hardcoded wire to a band-tunable dipole. From here:
- Many ways to express geometry — the same shape,
one knob-set, written four ways, including flying a
Droneinstead of computing coordinates by hand. - Design catalog — every built-in is a readable Builder you can copy as a starting point.
- Writing designs with Claude Code — let Claude Code write the next one for you from the seeded contract.