Skip to main content

Scriptable antenna design with live, knob-driven tuning

Project description

AntennaKNoBs  ·  by KK7KNB

Script your antenna. Explore it in real time by turning knobs.

AntennaKNoBs is a Python package for parametric, programmatic antenna design. You describe an antenna once as a small Python builder — its geometry expressed in terms of named parameters — and then explore the design space two ways:

  • In code, from the command line or a Python script: draw geometry, sweep a parameter, compare radiation patterns, optimize for match or gain, export a NEC deck.
  • In the browser, from a live workbench: drag a knob and watch the 3D wire model, far-field patterns, and Smith chart redraw in real time.

Its built-in engine is momwire, a new in-house set of method-of-moments engines. You can optionally add PyNEC (the battle-tested NEC2 engine) as a second backend and solve the same design both ways to trust the answer.

Test Python package Ruff Coverage


The live web workbench

It's live at app.antennaknobs.dev — open it, pick a design, and drag a knob (no install).

The workbench is the fastest way to feel a design. Pick an antenna, and its parameters appear as a panel of knobs. Drag one and every view updates live over a WebSocket: the solver re-runs and the browser redraws.

What you get:

  • A panel of knobs. Every builder parameter becomes a knob (or dropdown, or checkbox) with sensible min/max/step. Drag and the design re-solves.
  • 3D wire geometry with current visualization, viewable from three orthogonal projections (top / front / side).
  • Azimuth and elevation far-field pattern slices.
  • A Smith chart of input impedance, with optional frequency-sweep and convergence overlays.
  • Three solver slots (A / B / C) you can point at different backends and compare side by side — e.g. momwire triangular vs. B-spline vs. PyNEC on the same antenna, at once.

Live updates stay responsive because rapid knob drags are coalesced into one solve per round-trip, so the solver is never buried under stale requests.

Running it

The workbench is a FastAPI backend plus a React (Vite) frontend.

Installed (no Node needed). A wheel install bundles the pre-built frontend, so one process serves the whole app:

pip install "antennaknobs[web]"
uvicorn antennaknobs.web.server:app      # open http://127.0.0.1:8000

The backend serves the UI at / and the JSON//ws API on the same origin; /docs is the interactive API explorer.

Development (two terminals, hot-reload). When editing the frontend, run the Vite dev server alongside the backend so you get HMR:

# Terminal 1 — backend (from the repo root, in your .venv)
pip install -e ".[web]"
uvicorn antennaknobs.web.server:app --reload   # API on http://127.0.0.1:8000

# Terminal 2 — frontend dev server
cd src/antennaknobs/web/frontend
npm install
npm run dev                              # open http://localhost:5173

The Vite dev server proxies the API and the /ws live-solve channel to the backend on port 8000, so you only ever open http://localhost:5173. (A source checkout has no pre-built bundle, so the backend alone runs API-only until you npm run build — which writes src/antennaknobs/web/static/, the same bundle the wheel ships.)

The [web] extra pulls in uvicorn[standard], which includes the WebSocket support the live-solve channel needs — plain uvicorn fails the /ws handshake.


Two simulation backends

AntennaKNoBs can solve any design with either backend, selected per-run with --engine (CLI) or per-slot (web). Solving the same antenna two ways is the point — agreement between independent engines is your confidence check.

PyNEC momwire
What Python binding to the compiled C++ NEC2 engine In-house method-of-moments engines, pure-Python core with optional C++ accelerators
Basis NEC2 thin-wire (pulse/sinusoidal) Three bases — triangular (tent), sinusoidal, B-spline — plus H-matrix and array-block accelerators built on them
Speed Very fast single-frequency solves Fast; C++ accelerators (pybind11) for assembly/quadrature, pure-Python fallback
Ground Sommerfeld–Norton finite ground (default) PEC image method; free space by default
Install Prebuilt wheel from the python-necpp fork release (OpenBLAS vendored) C++ accelerator built from the momwire submodule
Use it for The established reference; finite-ground patterns Basis-flexible cross-validation; geometries where NEC2 reactance fails to converge

Selecting an engine (CLI):

--engine momwire                 # momwire (default), default triangular basis
--engine momwire:triangular      # piecewise-linear (tent) basis  — the momwire default
--engine momwire:sinusoidal      # NEC2-style three-term basis (cross-validator)
--engine momwire:bspline         # degree-1/2 B-spline Galerkin basis
--engine momwire:hmatrix         # B-spline + hierarchical-matrix (ACA) acceleration
--engine momwire:arrayblock      # element-aware block solver for arrays
--engine pynec                   # NEC2 via PyNEC (needs the optional pynec-accel)

In Python, instantiate an engine directly:

from antennaknobs.engines import PyNECEngine, MomwireEngine
from momwire import BSplineSolver

engine = PyNECEngine(builder)
engine = MomwireEngine(builder, solver=BSplineSolver, solver_kwargs={"degree": 2})

momwire lives in its own repository and is vendored here as a git submodule; its primary TriangularSolver engine converges to NEC accuracy in ~80 segments and is validated against the independent B-spline basis. The H-matrix and array-block engines are newer and aimed at large arrays. PyNEC is an optional second backend — the python-necpp fork, distributed as a self-contained wheel (OpenBLAS vendored, so no SWIG/BLAS/autotools toolchain is required at install time). It is licensed GPL-2.0 and installed separately from its own release; antennaknobs (MIT) neither bundles nor depends on it, and loads it only if present.


Designing antennas in code

An antenna is a subclass of AntennaBuilder that declares named parameters and builds its wires from them. Because the geometry is computed from parameters in ordinary Python, you specify physical coordinates a minimal number of times — the rest follow by reflection and relative position. (Most antenna tools make you type six absolute coordinates per wire.)

For path-shaped geometry (loops, vees, rhombics) you can describe the walk instead of the coordinates, with the Drone 3D-turtle — see the Drone & Transform reference.

Here is the built-in Moxon beam (beams.moxon), abbreviated. Four parameters describe the rectangle; helper functions negate coordinates (rx, ry) and chain nodes into wires (build_path):

from ... import AntennaBuilder
from types import MappingProxyType


class Builder(AntennaBuilder):
    default_params = MappingProxyType(
        {
            "freq": 28.57,
            "base": 7.0,
            "halfdriver": 2.4597430629596713,   # length of one radiating side
            "aspect_ratio": 0.3646010186757216,  # short side / long side
            "tipspacer_factor": 0.07729647745945359,
            "t0_factor": 0.4078045966770739,
        }
    )

    def build_wires(self):
        eps = 0.05
        base = self.base

        long = 2 * self.halfdriver / (1 + 2 * self.aspect_ratio * self.t0_factor)
        short = self.aspect_ratio * long
        tipspacer = short * self.tipspacer_factor
        t0 = short * self.t0_factor

        def build_path(lst, ns, ex):
            return ((a, b, ns, ex) for a, b in zip(lst[:-1], lst[1:]))
        def rx(p): return -p[0], p[1], p[2]   # mirror across x
        def ry(p): return p[0], -p[1], p[2]   # mirror across y

        S = (short / 2, eps, base)
        A = (S[0], long / 2, base)
        B = (A[0] - t0, A[1], base)
        C = (B[0] - tipspacer, B[1], base)
        D = rx(A)
        E, F, G, H, T = ry(D), ry(C), ry(B), ry(A), ry(S)

        n_seg0, n_seg1 = 21, 1
        tups = []
        tups.extend(build_path([S, A, B], n_seg0, None))
        tups.extend(build_path([C, D, E, F], n_seg0, None))
        tups.extend(build_path([G, H, T], n_seg0, None))
        tups.append((T, S, n_seg1, 1 + 0j))   # the driven segment
        return tups

The top-level package re-exports the workhorse functions, so a full design-explore-compare loop is a short script. This optimizes an inverted-V dipole at several heights and overlays the resulting patterns:

import antennaknobs as ant
from antennaknobs.designs.dipoles.invvee import Builder

p = dict(Builder.default_params)
bounds = ((p['length_factor'] * .8, p['length_factor'] * 1.25), (0, 60))

builders = (
    ant.optimize(
        Builder(dict(p, base=base)),
        ['length_factor', 'angle_deg'], z0=50, bounds=bounds,
    )
    for base in [5, 6, 7, 8]
)

ant.compare_patterns(builders)

Command-line usage

Everything is under python -m antennaknobs <subcommand>. Designs are named family.name (with an optional :variant) — run list to see them all.

# Draw a Moxon's wire geometry to a file
python -m antennaknobs draw --builder beams.moxon --fn moxon.png

# Sweep frequency and plot impedance on a Smith chart
python -m antennaknobs sweep --builder beams.moxon --param freq \
    --use_smithchart --npoints 21 --fn moxon_smith.png

# Far-field pattern of a Yagi, solved with momwire
python -m antennaknobs pattern --builder beams.yagi --engine momwire:triangular

# Overlay patterns of three beams
python -m antennaknobs compare_patterns \
    --builders beams.moxon beams.hexbeam beams.yagi --fn beams.png

# Cross-check one design across two backends
python -m antennaknobs compare_patterns \
    --builders beams.moxon beams.moxon --engines pynec momwire:bspline --fn check.png

# Optimize length and arm angle of an inverted-V dipole for a 50 Ω match
python -m antennaknobs optimize --builder dipoles.invvee \
    --params length_factor angle_deg

# Export a NEC2 card deck for use in external tools
python -m antennaknobs export --builder beams.hexbeam --out hexbeam.nec

# List the available designs (optionally filter)
python -m antennaknobs list
python -m antennaknobs list dipole

Shared flags: --engine (backend, see above), --ground (free | pec | finite | finite:<eps_r>,<sigma>), --builder/--builders, and --fn (save to file instead of showing on screen).

Below is a typical far-field plot produced by the pattern/compare_patterns commands:

Radiation pattern

Available designs

Roughly 70 built-in designs across nine families — run python -m antennaknobs list for the authoritative list:

Family Examples
dipoles invvee, folded_invvee, ocf_dipole, koch_dipole, dipole_turnstile
beams moxon, hexbeam, yagi, hb9cv
loops quad, delta_loop, diamond_loop, horizontal_loop, bisquare
verticals vertical, jpole, inverted_l, bobtail, four_square, bruce
arrays yagiarray, moxonarray, invveearray, bowtiearray, delta_looparray
multiband fandipole, trap_dipole, hexbeam_5band, twoband_fan_dipole
broadband discone, g5rv, lpda, t2fd
wire sterba, rhombic, vbeam, w8jk, zepp, lazy_h, longwire
specialty hentenna, bowtie, helix, hourglass

User-authored designs (in the user.* namespace) appear here too; filter with list --builtin-only / list --user-only. Drop a Python file in ~/.antennaknobs/designs/ and it shows up in the workbench — see Writing designs with Claude Code for the contract and how to have Claude Code write one from a plain-language description.


Install

From PyPI (prebuilt wheels — no toolchain)

antennaknobs and its C++ engine momwire are published to PyPI with prebuilt wheels, so a plain install needs no compiler:

python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade pip

# antennaknobs + the web workbench; momwire (the engine) comes along as a dep
pip install "antennaknobs[web]"

Optionally, add the NEC2 solver (PyNEC) as an alternative to momwire:

# optional NEC2 solver (Linux / Windows / macOS-arm64 wheels)
pip install "pynec-accel>=1.7.4.post2"

Then launch the workbench with uvicorn antennaknobs.web.server:app (see Running it). On macOS, brew install libomp is required — the momwire and pynec-accel wheels link Homebrew's OpenMP runtime (and share it, so cross-engine use is fully multithreaded; details under macOS).

The sections below build from source instead (a development checkout, or a platform without prebuilt wheels).

Ubuntu (22.04 / 24.04)

PyNEC installs as a prebuilt wheel, so no SWIG/BLAS/autotools toolchain is needed; only the momwire C++ accelerator compiles from source (hence g++).

1. System dependencies

sudo apt-get update
sudo apt-get install \
    python3 python3-pip python3-venv python3-dev \
    g++ build-essential git

2. Clone and create a virtual environment

git clone https://github.com/stevenmburns/antennaknobs
cd antennaknobs
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install setuptools numpy scipy pytest matplotlib icecream scikit-rf

3. Install momwire (the engine)

# momwire: a git submodule; its C++ accelerator builds from source.
pip install pybind11
git submodule update --init momwire
pip install --no-build-isolation -e ./momwire

3b. (Optional) Install PyNEC for cross-validation

PyNEC is an optional second backend — GPL-2.0, installed separately from its own release, and never bundled with or required by antennaknobs. Skip it and momwire is still fully functional; install it only if you want to cross-check against NEC2.

# The fork is published to PyPI as `pynec-accel` (a distinct name from upstream
# PyNEC/pynec, whose builds are broken on current Python; the import name stays
# `import PyNEC`). Its wheels vendor OpenBLAS + libgfortran.
#
# Use >= 1.7.4.post2: earlier builds vendored their own libgomp, which clashes
# with momwire's system libgomp via a static-TLS limit and silently knocks
# momwire's C++ accelerator onto its slow pure-Python path whenever both backends
# load in one process. post2 binds the system libgomp instead (universal on glibc
# Linux — the GCC OpenMP runtime).
pip install "pynec-accel>=1.7.4.post2"

4. Install AntennaKNoBs

pip install -e ".[test]"         # core + test deps (pytest, the web test client)
# or  pip install -e ".[web]"    # just the web workbench, no test extras
# or  pip install -e .           # library only

5. Run the tests

pytest -vv --durations=0 -- tests/

(The [test] extra above is what makes this step work from a clean clone — it pulls in pytest, the [web] server deps, and httpx2 for the web-server tests' TestClient.)

The authoritative, always-tested version of this whole sequence is the CI workflow at .github/workflows/test.yml — it installs both engines and runs the suite on every push. If anything here drifts, that file is the source of truth.

macOS

Tested on Apple Silicon (arm64), macOS 14+. The momwire C++ accelerator compiles from source against Homebrew's OpenMP runtime (libomp); PyNEC installs as a prebuilt wheel. CI only runs Ubuntu, so the Ubuntu sequence above is the source of truth — the steps below are the same with macOS system packages.

1. System dependencies

xcode-select --install              # clang/clang++ + git (skip if already installed)
brew install python git libomp      # libomp = the OpenMP runtime the momwire accelerator links

2. Clone and create a virtual environment. Use a venv — on macOS it is effectively required, not just good hygiene: Homebrew's Python is marked externally managed (PEP 668), so pip install into it fails with an error: externally-managed-environment. A venv sidesteps that and keeps the project's dependencies off your system Python.

git clone https://github.com/stevenmburns/antennaknobs
cd antennaknobs
python3 -m venv .venv
source .venv/bin/activate         # re-run this in each new shell before using the project
pip install --upgrade pip setuptools wheel
pip install numpy scipy pytest matplotlib icecream scikit-rf

3. Install momwire (the engine) — same as Ubuntu:

pip install pybind11
git submodule update --init momwire
pip install --no-build-isolation -e ./momwire

The build finds Homebrew's libomp at /opt/homebrew/opt/libomp, the Apple Silicon default. On an Intel Mac, Homebrew lives under /usr/local, so point the build there with LIBOMP_PREFIX:

LIBOMP_PREFIX=/usr/local/opt/libomp pip install --no-build-isolation -e ./momwire

If the accelerator fails to build for any reason, momwire still installs and runs in its slower pure-Python mode.

3b. (Optional) Install PyNEC for cross-validation

The same optional GPL-2.0 second backend as on Linux. The fork ships prebuilt macOS wheels for Apple Silicon (arm64), macOS 14+, Python 3.10–3.14 only — there are no Intel-Mac wheels, so on an Intel Mac skip PyNEC and use momwire alone.

pip install "pynec-accel>=1.7.4.post2"

With pynec-accel ≥ 1.7.4.post2 and momwire ≥ 0.2.1, neither wheel vendors its own libomp — both link Homebrew's by absolute path, so a process that loads both (any cross-engine run, including the tests) shares a single OpenMP runtime and stays fully multithreaded, with no env vars. That shared runtime is why brew install libomp is required.

Older macOS wheels each bundled a private libomp; two copies in one process abort with OMP: Error #15 (or, with KMP_DUPLICATE_LIB_OK=TRUE, deadlock). If you're pinned to a pre-1.7.4.post2 pynec-accel or pre-0.2.1 momwire, the stopgap is export KMP_DUPLICATE_LIB_OK=TRUE and export OMP_NUM_THREADS=1 before any cross-engine run — at the cost of a single-threaded accelerator.

4. Install AntennaKNoBs and 5. Run the tests — identical to the Ubuntu steps:

pip install -e ".[test]"
pytest -vv --durations=0 -- tests/

(For the web workbench's frontend dev server, also brew install node.)


Acknowledgments

Most of this codebase — the AntennaBuilder framework, the design catalog, the momwire engine bindings, the web workbench, and these docs — was written with Claude Code, Anthropic's agentic coding tool, working from KK7KNB's direction and antenna-engineering judgment.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

antennaknobs-0.12.0.tar.gz (357.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

antennaknobs-0.12.0-py3-none-any.whl (337.2 kB view details)

Uploaded Python 3

File details

Details for the file antennaknobs-0.12.0.tar.gz.

File metadata

  • Download URL: antennaknobs-0.12.0.tar.gz
  • Upload date:
  • Size: 357.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for antennaknobs-0.12.0.tar.gz
Algorithm Hash digest
SHA256 3f05626a305c89710256584a2f314633b68784d83606407121a50c323e2e3272
MD5 1fd6c6f153e98406b078e0530f27bd60
BLAKE2b-256 fcae441d338f611e40bc6fe1781afafbc70f63d5acbb8645f716afb30b6898d7

See more details on using hashes here.

Provenance

The following attestation bundles were made for antennaknobs-0.12.0.tar.gz:

Publisher: publish.yml on stevenmburns/antennaknobs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file antennaknobs-0.12.0-py3-none-any.whl.

File metadata

  • Download URL: antennaknobs-0.12.0-py3-none-any.whl
  • Upload date:
  • Size: 337.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for antennaknobs-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 53159ef76a36361aae051bc82caedef9729f564ccf4991efb5665ab0850232c8
MD5 d9cb4b1043ecb0a276e6d307c075ae9e
BLAKE2b-256 9d64c8a90a66e8cfa075d6b2903a3765864f47509e394d6b4a26dc1db568fec2

See more details on using hashes here.

Provenance

The following attestation bundles were made for antennaknobs-0.12.0-py3-none-any.whl:

Publisher: publish.yml on stevenmburns/antennaknobs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page