Skip to content

Web workbench

The web workbench is the live, no-install face of antennaknobs: a panel of knobs per design, with the radiation pattern, SWR, and impedance re-solving as you drag.

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

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

The server sizes its BLAS/OpenMP thread pools itself (physical-core count). On multi-core boxes you can optionally prefix the command with OMP_WAIT_POLICY=PASSIVE GOMP_SPINCOUNT=0 to park idle solver threads between solves — worth ~15% on live knob-drag latency. These two are read once at process start, so they only take effect as launch environment.

A local instance has no login and none of the hosted instance’s solve-size limits, and it runs any design files you’ve allowed with your user privileges — keep it on the default 127.0.0.1 bind rather than exposing it to a network you don’t fully control.

A hosted simulator is running at app.antennaknobs.dev (a single FastAPI process serving the API, the /ws live-solve channel, and the built React SPA). It’s deployed as a container on Fly.io; the repo’s docs/deploy.md is the runbook.

Each parameter in a design is a knob (the big one is the measurement-frequency VFO dial; the rest are smaller). The dial is normally locked to the design frequency; off-band designs — an antenna cut for one band and worked on another through a tuner (see Cut for one band, worked on another) — open with the lock open and the dial already parked on their operating band. Three ways to change a knob:

  • Drag — press on the knob and move the mouse vertically (up to increase, down to decrease). Horizontal motion is ignored, so a natural hand motion won’t fight you.
  • Keyboard / physical dial — click a knob (or tab to it) to focus it; it shows a highlight ring. While focused, ↑ / → and ↓ / ← step by one increment, Page Up / Down by ten, Home / End jump to the range ends, and Enter opens the value to type a number exactly. A physical USB dial that emits arrow keys drives the focused knob the same way — the knob keeps focus until you click elsewhere, so twisting the dial keeps adjusting it.
  • Right-click menu — right-click a knob for its settings:
    • Turn step — how much one drag-notch / arrow press moves the value.
    • Display range — the min/max the knob sweeps between.
    • Optimize this knob + Optimize range — mark it as a free variable for the optimizer and bound its search (see Optimizing below). The same range bounds Keep the target while I drag: a marked knob is the one the hold moves, and it will not leave this range to do it.

Every turn re-solves and redraws live (when Live is on — see below).

The output stage — views, pins, and layout

Section titled “The output stage — views, pins, and layout”

The right-hand stage shows one primary view at full size with your other views as thumbnails beside it. The roster is:

ViewWhat it draws
Antennathe wires, current heat-map, standing-wave envelope
Azimuth (xy)far-field polar cut in azimuth
Elevation (yz)far-field polar cut in elevation
Smithfeedpoint impedance, plus the sweep locus and any measured overlay
Schematicthe feed network as a chain
S11 (dB) vs freqreturn loss against frequency, the log-magnitude form a VNA shows
VSWR vs freqSWR against frequency

Click a thumbnail to promote it to primary. The last two read the same frequency sweep the Smith chart plots — run a sweep and all three fill in together; the marker on each rides the measurement frequency.

  • Pin the views you watch. The stage carries a pinned set, not the whole roster — Antenna, Azimuth, Elevation, Smith to start, up to six pins. Six is a hard cap: more thumbnails than that and none of them is legible.
  • The rest live in the picker. All views ⌄ +N at the foot of the thumbnail strip opens every view in the roster; the ⊕ / ⊖ beside each row pins or unpins it, and a view added to the roster since you last looked wears a NEW badge. At the cap, pinning is blocked with “Unpin a view first” rather than silently dropping someone else’s pin.
  • Rail or grid. The ▤ / ⊞ toggle on the stage switches between the rail (one primary + thumbnails) and a 2×2 grid of equal cells over your first four pins, for watching several live at once. Each grid cell has a maximize button back to the rail. Grid is desktop-only.
  • On a phone the output pages are the pinned set — swipe between them, with a trailing Info page for the solve readout — and the button opens the same roster as a sheet, so pinning a view gives it a page and unpinning takes it away.
  • Pins are yours, not the design’s: the set (and the layout mode) is stored in the browser and shared by every design session, since which views you care about is a habit, not a property of the antenna. Since v0.43.0 the picker’s ▲/▼ buttons reorder the pinned set — pin order IS rail order, grid-cell order, and phone-page order, so one reorder serves all three — and a second browser window picks up pin/order/layout changes live instead of on reload.
  • Analyses follow the views (v0.43.0): the freq sweep, convergence sweep, and NEC pattern overlay only run while a view that renders them is pinned or open — an enabled sweep with no Smith/S11/VSWR view on screen costs nothing, and pinning one starts it. The norm check is the deliberate exception (its number lives in the always-present solve readout).

The Antenna view draws the wires (with the current heat-map and standing-wave envelope overlays) in one of four projections — Top (xy), Front (xz), Side (yz), and an isometric Iso (the classic corner view: x and y recede left and right, z stays up) — switched by the buttons in the top-right overlay. The ground reference line appears on the two elevation views.

The view auto-fits the whole antenna, and you can navigate from there, map-style:

  • Zoom — scroll wheel (desktop) or pinch (touch), anchored at the cursor or pinch centre, up to 10 000×. Fine construction detail — a cage of wires millimetres apart on an antenna metres across, typical of imported NEC decks — is inspectable without touching any knob.
  • Pan — drag, once zoomed in. At fit zoom a touch drag stays with the page (on a phone that’s the swipe between output screens); after a pinch the canvas owns the gesture until you re-fit.
  • Re-fit — double-click / double-tap, the Fit button in the bottom-right HUD (next to the live zoom readout), or just zoom all the way back out. Switching design also re-fits — the old viewport means nothing for a new geometry.
  • Turning the view keeps your place — switching projection carries the zoomed viewport over: the world point at the centre of the canvas stays centred (its depth along the old camera ray taken from the geometry’s midpoint), at the same on-screen magnification. Zoom into a feed region in Front view and flip to Side or Iso to see the same region from the other angle. Fit is one double-click away whenever the carried view isn’t what you wanted.

The scale bar under the antenna reads λ/4 at fit zoom; once zoomed it switches to a round metric length (1/2/5 × 10ᵏ m — like a map’s) sized to about a quarter of the canvas, so it stays readable and true at any magnification. Zooming magnifies geometry only: wire strokes, labels, the feed dot, and the envelope amplitude keep their size.

A Live toggle sits next to the frequency dial. It looks depressed when on and raised when off:

  • On — every knob turn triggers a solve; the plots track your hand.
  • Paused — knob turns just move the values; nothing solves until you turn Live back on. Useful when you want to set several knobs before paying for a solve, or when a heavy design makes continuous solving sluggish.

The frequency sweep and the far-field cuts sample where the curve actually bends, not just on a fixed grid. Hold the design still for a moment after a sweep settles and the workbench quietly buys extra samples where the drawn polyline still misses the true curve — a sharp SWR notch that a uniform grid would step right over resolves down to sub-pixel, and a pattern lobe’s edge gets angles a uniform circle wouldn’t spend there.

What you’ll notice:

  • Sweeps start faster. With refinement on, the base grid’s job is only to detect features, so it opens at 17 log-spaced points instead of 41 and the S11/VSWR/Smith views fill in sooner; the refinement rounds then sharpen whatever the base grid straddled.
  • The Smith locus is a connected curve — once it has earned it. While refinement is still landing points, the Smith, VSWR and |Γ| views draw unconnected dots: a polyline through a half-refined set shows kinks that are artifacts of sampling, not physics. When the refinement pass settles, the views switch to the connected stroke. (Toggling refinement off mid-run deliberately keeps the dots — the accumulated set is uneven, and the charts keep saying so.)
  • Only what’s on screen refines. Like the sweeps themselves (view-residency gating, above), refinement spends solves only on the projections and cuts whose views are pinned or open.
  • It’s a setting. The gear menu’s adaptive resolution toggle (default on, stored per browser like the theme) turns the whole machinery off, and the base grids revert to their historical sizes (41-point sweeps) and become final. For unusually difficult designs the budgets can be nudged via localStorage overrides (antennaknobs.sweepBaseN, antennaknobs.sweepRefineBudget, antennaknobs.cutRefineBudget, antennaknobs.refineTolerance) — deliberately not knobs in the menu.

Relatedly, the S11 chart no longer clamps at 0 dB: a driven-array port’s active reflection is not bounded by |Γ| ≤ 1 (mutual coupling can push more power into a detuned element than its own generator supplies), so when any drawn value crosses zero the axis top rises to show it, with the 0 dB line kept as the tick a healthy passive port never crosses.

Some decks are solvable but sit somewhere the answer is a strong function of something you may not have thought about. When that happens the solver says so, and the note appears under the design’s description in the catalog panel.

These are advisories, not errors. Nothing was refused and nothing was re-meshed: the solve ran with exactly the settings you gave it and produced exactly the number shown. A deck the solver will not take is refused instead, in the backend picker, before it runs.

Three come from momwire and carry that deck’s own measured figures:

  • a conductor within a few radii of the ground — the impedance is a strong function of the stand-off, and the note quotes the deck’s actual height and h/a. On a sparse surface screen a millimetre of grass is worth tens of ohms;
  • an unresolved crossing node — the mesh where a wire crosses the interface is coarser than its convergence class wants, with the measured bar;
  • razor-2p’s far mesh — that basis is first order in the far mesh, so its default-mesh answer is not converged. This one fires on every razor-2p solve, by design: momwire measured that no solve-free predictor of the error correlates, so there is no honest threshold to gate it behind.

Because the last one is unconditional, it is collapsed to a count you can expand while the deck-specific notes are always shown in full. An advisory that appears every single time trains the eye to skip the whole panel, which would take the deck-specific ones down with it.

One advisory is the app’s own rather than the solver’s:

  • single-valued soil constants, on a deck with buried conductors solved at 14 MHz or above. The soil presets are one (εr, σ) pair each, and real soil disperses with frequency. On the 48-radial buried screen the fixed average soil sits 17.7 % from the frequency-dependent value at 14 MHz and 31.9 % at 28.5 MHz, against 5.9 % at 1.8 MHz — so above 14 MHz treat a buried deck’s impedance as indicative. Elevated antennas are not affected the same way (1–3 % on the same comparison), which is why the note is scoped to buried decks and to the higher bands rather than shown everywhere.

The channel carries whatever the solver raises rather than a fixed list, so an advisory added upstream appears here without a client change — an unfamiliar one is shown in full rather than collapsed.

Next to Live is an Optimize toggle (same depressed-when-on look), with a gear menu beside it for the objective. The optimizer continuously tunes the knobs you’ve marked to hit a target:

  1. Pick an objective in the gear menu — SWR, Resonance or Match Z₀. They differ in more than the target: see Which objective.
  2. Mark the knobs to vary — right-click each knob you’ll let the optimizer move, check Optimize this knob, and set its Optimize range (the search bounds). A marked knob is visually flagged. To flip the flag from the keyboard, focus a knob (click or tab to it) and press o — the same toggle as the menu checkbox, without leaving the home row.
  3. Turn on Optimize. While Live is also on, the optimizer runs reactively: any time you change a fixed knob, it re-tunes the marked knobs (a short debounce, then a few dozen solves) and writes the best values back, so the antenna stays on target as you explore.

The three are not flavours of the same thing. Two of them are roots — an exact condition with an exact answer — and one is a minimisation, and that difference decides how many knobs each needs and what the readout shows.

  • SWR — minimize SWR against the design’s reference impedance (Z₀, 50 Ω by default). A best compromise rather than an exact target, searched by Nelder–Mead. It is the only one of the three that works with any number of marked knobs and on a multi-feed design, which is why it stays the default choice for anything three-knob or many-port.
  • Resonance — drive the feed-point reactance to zero (X = 0) exactly. With exactly one marked knob this is a scalar root and is solved as one, by a bracketed secant: typically three to five solves where the general search takes ten to sixteen.
  • Match Z₀ — drive both R = R₀ and X = 0 exactly. With exactly two marked knobs this is a two-component root, solved by Newton on (R − R₀, X) with a finite-difference Jacobian, started from a short survey of the knob box. Typically eleven to thirteen solves against Nelder–Mead’s thirty to fifty.

The gear menu says which is which in a line each. The knob counts are what the root methods need, not a restriction: mark two knobs with Resonance selected, or a number other than two with Match Z₀, and the run falls back to Nelder–Mead on the same objective rather than refusing. A multi-feed design does the same, because a minimax over several ports has no single root.

The readout shows the residual for the root objectives — |X| for Resonance, |Z − Z₀| for Match Z₀ — rather than an eval count and an SWR. A root method’s residual falls monotonically, so it is worth watching; a simplex’s best-so-far is not, and showing one where the other is expected reads as a fault.

A run shows its work. Progress streams back per evaluation: the VFO panel’s readout ticks through each candidate’s eval count, impedance, and SWR as the search moves, and the Smith chart follows the run live — its dot walks the Γ-plane with every trial. On a multi-feed design it draws one hollow ring per feed, so you watch the array come into balance rather than one element of it; the feed the objective is currently chasing (the worst one) is drawn bright and the others recede. Every other view keeps describing the design you started from (the knobs aren’t touched until the run finishes), so those views dim for the duration — the same stale-fade a mid-solve view wears — and light up when the result lands. The schematic stays bright: it’s drawn from the current knob values and stays right throughout.

The solve readout keeps moving too. Its solve field shows the last full solve inside the run, rtt gives way to a per eval figure — the gap between updates, which is what you are actually waiting through, rather than the interactive round trip the optimizer’s own request never makes — and a solves row counts the work the run has paid for, so forty solves stop looking like one.

Under the hood, SWR is a derivative-free Nelder–Mead search, and Nelder–Mead is also the finisher whenever a root method cannot apply (see Which objective above); Resonance and Match Z₀ with the right knob counts are solved as roots instead. Every evaluation is a full MoM solve, bounded by your Optimize ranges, and it always runs on the fast momwire engine — never PyNEC, which is too slow for an interactive loop. It’s a tuning aid, not a global optimizer: give it sensible ranges and a couple of free knobs, not a dozen.

Evaluations and solves are different numbers. Nelder–Mead keeps only its current simplex, so it re-probes points it has already paid for; those are answered from within the run instead of re-solved. A run reports both, and on a one-knob run the gap is about a fifth — 24 evaluations for 19 solves on a 12-radial buried screen. The match is exact, never approximate: a point that differs in the last bit is a different point and gets its own solve, so the run is the run it would have been, only cheaper.

Seed from a survey is an off-by-default switch in the same gear menu, under Search. With it on, the optimizer first samples the whole knob box, fits a surface to the impedance it measured, and hands the most promising point to the search as a starting position — to Nelder–Mead for SWR, and to the Newton root for Match Z₀, which starts from the surface’s predicted crossing rather than its best sample. The search is still the finisher — the survey proposes a place to start, it never decides the answer, and every value reported is a point that was actually solved.

It is off by default because it is only worth its cost some of the time. From a design’s shipped tuning, where the answer is already near the start, the survey spends evaluations to arrive at about the same place. From knobs you have dragged well away from a good answer it is the difference between converging and not: on the Moxon, started from a corner of its ranges, plain Nelder–Mead sticks at SWR 1.60 at every budget — it walks into a local basin and parks a knob on its bound — while the seeded run reaches 1.0006. Turn it on when you are exploring rather than refining.

What it costs is visible: the survey’s points are spread across the whole box, so their SWR jumps around and none of them need be an improvement. The readout says seeding 3/6 through that phase rather than showing an eval count and a wandering SWR, because a working search should not look like a failing one. The survey’s evaluations come out of the same budget, so switching it on never makes a run more expensive. One free knob skips it entirely — a one-knob search converges quickly enough that surveying would cost more than it saves.

Multi-feed designs score their worst feed. On a design with several independently driven ports (a bowtie array, a pair of phased verticals), the objective is evaluated per feed and the optimizer minimizes the worst one — so “SWR 1.4” after a run means no element sits worse than 1.4, and one bad feed can’t hide behind several good ones. The SWR shown while optimizing is that worst feed’s, and it is the bright ring on the Smith chart — which feed that is can change from one evaluation to the next as the search rebalances them. A design that feeds its elements from a single source through a network is different: there the match that matters is the network’s input, so the optimizer scores the driven plane — the same impedance the readout shows. The CLI’s optimize aggregates feeds the same way (it differs only in scoring |Z − Z₀| distance rather than SWR).

The optimizer tunes for you and hands back an answer. This is the other half: you drag a knob and the marked knobs follow, holding the target while you explore.

The switch is in the same gear menu, under While dragging. Turn it on and drag any knob you have not marked — the marked ones move under your hand to keep the objective satisfied, and the chart and readout stay on target throughout. It costs one solve per drag tick, the same solve the app already owed you to redraw at the new knob position: the hold predicts where the marked knobs go and the tick’s own solve usually confirms it.

It needs the objective’s exact knob count — one marked knob for Resonance, two for Match Z₀ — because holding an exact target is a root problem. Where the optimizer would fall back to a general search, the hold cannot: it refuses, and the menu entry says why, with the count it found. SWR is refused for the same reason: it is a best compromise, with no target to hold.

Marked knobs still belong to the optimizer. Grab one by hand and the hold switches off, exactly as it switches an optimizer run off — the knob is yours again, and the dragged knob is never promoted into the search.

When the target runs out. The hold stops and says which of the two things happened, because they have different fixes. If a marked knob has reached the end of its Optimize range — a limit you set — it names the knob: “Resonance not held: length_factor is at the end of its optimize range — widen it to keep going”, naming both knobs when Match Z₀ has run both out. If instead there is no longer a target to hold at all, which happens where a resonance simply stops existing as the geometry changes, it says “Resonance not held: none within reach — drag back to recover it”. Match Z₀ says Match in place of Resonance.

Either way the marked knobs stay at their last good values rather than drifting, and the hold re-acquires the moment the target is reachable again — whether you carry on in the same direction or drag back the way you came.

What restarts it. Changing a marked knob’s Optimize range, or the measurement frequency, restarts the hold from a fresh root find rather than continuing: the surface it was tracking is no longer the surface it measured. Switching the mode off and on again does the same, which is also the way out of a hold you would rather not drag out of.

Loading a design pauses Optimize. Switching antenna or picking a variant turns Optimize off — its objective and marks belong to the design you left — and briefly says so. Switching antenna keeps that design’s marks (they’re remembered per design, so coming back restores them); loading a variant instead clears its marks, because their ranges were scaled to the values the variant just replaced. Re-mark the knobs and turn Optimize back on to resume.

A solver selector offers a few preset slots so you can flip between engines without re-entering options — e.g. a fast dense basis, an accelerated array engine, and the PyNEC reference. The available engines are the momwire bases (Sinusoidal, Sin-Galerkin, B-spline), the accelerators (H-matrix (ACA), Array-block), the optional PyNEC backend — see The solver & accuracy for what each is good at — and, on a machine with a licensed binary, NEC-5 (setup and terms): the slot appears exactly when the server resolves NEC5_EXE, which is why the hosted simulator never shows it while your own local instance can. The list is served by the backend you’re pointed at, so a server without an optional engine simply doesn’t offer it, rather than offering a slot that fails on the first solve. NEC-5 solves are one external run per request — right for A/B snapshot checks against momwire in the next slot, heavier than the in-process engines for live dragging.

Under the tab strip in that gear menu, each engine states what it is made of in one line — basis, testing, kernel, quadrature, solve strategy, feed model — for instance B-spline · degree 2 · Galerkin · reduced kernel · converged quadrature · dense · point gap. It exists because three of the tabs differ in one word: B-spline, H-matrix (ACA) and Array-block are the same physics with a different solve strategy, and Sin-Galerkin is the Sinusoidal basis with Galerkin testing in place of point matching. Every control below the line rewrites its one segment in place, a segment the preset pins is marked (pinned), and PyNEC and NEC-5 say External engine, not described compositionally rather than inventing one. The vocabulary is momwire’s own, served with the roster — the browser carries no engine names.

The controls themselves are drawn from the served option catalogue rather than a hand-written panel per engine, so a knob appears exactly when the engine behind the tab takes it, with the server’s own bounds and captions. When the loaded design cannot be solved with a tab’s current settings — a buried deck under the extended kernel, say — the tab says so inline with momwire’s own refusal sentence and the issue it cites, and there is no Solve anyway: momwire would raise, so an override would buy an error dialog rather than a result.

On the buried designs, that gate now covers the external engines too, and each answer is measured rather than assumed. PyNEC is greyed out: NEC-2’s Sommerfeld-Norton ground is formulated for sources above the interface and has no below-interface case, and nec2++ does not refuse such a deck — it solves it as though the wire were still in air and returns a plausible number. Measured, moving a buried dipole from 5 cm above the interface to 5 cm below moves PyNEC’s impedance by 12 % where the physics moves it by a factor of ten. That is a worse failure than a refusal, so the tab refuses on PyNEC’s behalf. NEC-5 serves buried decks and is not gated; a conductor that stops exactly on the plane is refused, because the ground flag a card deck needs differs between a buried wire and one bonded to ground.

The solver’s gear menu also exposes segments / wire (N) — how finely each wire is discretized. More segments = more accurate (up to convergence) but a larger, slower solve. See Segments & convergence for what N means and how to find “enough.”

Beside wire radius, every momwire slot’s gear menu carries an extended kernel (EK) check — NEC’s extended thin-wire kernel, the EK card. It changes how the solve treats the wire’s radius on-axis: instead of collapsing the current to a filament, it integrates NEC’s O(a²) expansion over the tube. That only matters when a wire is fat relative to its own segments — the Δ/a ratio. Above Δ/a ≈ 10 it moves the impedance a fraction of a percent; below Δ/a ≈ 3 it moves it several percent, in NEC’s direction. It costs about 1.0–1.3× the ordinary solve.

Turn it on when you’re modelling thick elements (tubing, cages, a fat-wire imported deck) or cross-checking a NEC model whose deck carries an EK card — the flag is per slot, so the natural use is A against B: the same basis and mesh, one slot with the kernel and one without, and the readouts side by side. A slot running it is labelled +EK on its chip (e.g. B-spline d=2 +EK), so the pair stays tellable apart.

Every momwire basis serves it — the Galerkin family joined with momwire 0.27.0 (momwire#246/#287/#299: every ground model, bent and stepped geometry included). Some combinations are refused, and the check greys out and says which, in momwire’s words: the reasons are served with the roster from momwire’s own table of couplings, never retyped here.

  • K≥3 junction singular enrichment (the validation-only knob on the B-spline family) cannot run alongside it: the enrichment degrees of freedom bypass the very kernels the extended kernel corrects (momwire#249). The two grey each other out, so you can always back out of either.
  • A buried wire cannot be solved under it (momwire#553): on a deck with a conductor below the interface the tab says so and withholds the solve.

PyNEC has no such check — the toggle drives momwire’s kernel. Changing a slot’s solver resets the check along with that solver’s other options, so an armed kernel never rides silently onto a basis you just switched to.

Sin-Galerkin and the B-spline family (B-spline, H-matrix, Array-block) carry a feed model control — how the source gap itself is modelled:

  • NEC-compatible (the default) — NEC’s segment-wide gap. The readout reproduces NEC/EZNEC behaviour, including the familiar reactance drift as the mesh refines. Use it when you’re cross-checking against NEC results.
  • Converged — a zero-width (point) gap instead: the impedance converges to the B-spline answer, and the port admittance is exactly reciprocal.

On near-open, high-Q feeds the choice is worth a lot, and the gear menu says so: those designs (wire.lazy_h, wire.vbeam and their class) show a hint recommending Converged, which removes two to three orders of magnitude of the apparent disagreement between solver bases (momwire#213). What it does not do is reduce the mesh such a design needs — budget fine segments either way; see the near-open feed. A Sin-Galerkin slot running the non-default setting carries a suffix on its chip, so two such slots stay tellable apart at a glance — and which value is the deviation flipped with momwire#654. The point gap is the solver’s default now, so a plain Sin-Galerkin chip is the converged one and the chip reading Sin-Galerkin (NEC gap) is the slot asking for NEC’s source. The B-spline family defaults to the point gap too; its chip carries the degree instead, and the composition line is where its feed model reads.

The plain Sinusoidal solver does not carry the control: it cannot express a zero-width gap under point matching (momwire#212), which is why its composition line says segment gap where Sin-Galerkin’s says point gap — the one difference that follows from the other.

Everything a workbench tab computes — the live solve, the frequency sweep, the convergence ladder, the norm check, the NEC pattern — runs through a single per-session solve lane on the server, one computation at a time, with the live solve always first in line. You’ll notice it as steadiness on heavy designs: background sweeps never compete with the solve that’s drawing the heatmap, and a knob turn preempts stale background work at the solver’s next internal checkpoint (milliseconds to one sweep point, not the rest of the batch). Abandoning the tab mid-sweep stops the computation the same way.

When the selected solver is a poor match for the design (a dense engine on a benchmark-class mesh), the workbench warns and withholds the solve; the server holds background batches to the same answer, so a sweep of minutes-per-point solves only runs once you’ve clicked Solve anyway.

Real antennas hang over real ground, so the workbench starts there: the ground plane checkbox is on by default, with free space one click away. The over-ground picture — takeoff angle, the ground-lobed elevation pattern, the shifted feed-point impedance — is usually the one your design decisions actually depend on.

The selector describes what the ground is, independent of solver:

  • finite — real earth, the default. Its two constants are yours to set: εr (relative permittivity, 1–81) and σ (conductivity, S/m, on a log scale from 1e-4 to 5), with named presets — very poor, poor, average, very good, fresh water, salt water — that fill both at once. The active preset is read off the two numbers, so dragging a knob off a preset simply deselects it; the default 10 / 0.002 is shown as custom soil. The soil travels with every solve — sweeps, patterns, cuts, near fields and the exported NEC deck’s GN 2 card — and a changed soil is a new sweep, not a cache hit. Each preset is a single (εr, σ) pair, while real soil disperses with frequency: on a buried deck above about 14 MHz that is worth tens of percent in impedance, and the solve says so in an advisory (antennaknobs#1188 tracks making the presets frequency-dependent). An antenna clear of the ground is barely affected;
  • PEC — a perfect reflector, mainly for apples-to-apples engine comparisons; or
  • terrain — a faceted height profile around the site (levee/cliff presets), for antennas where the ground is not flat. See Faceted terrain below.

Every solver then offers the same method sub-choice — full Sommerfeld/Norton (most accurate, the reference below ~0.1λ heights) vs. the reflection-coefficient approximation (the default: much faster per solve, and fine above ~0.1λ; Sommerfeld’s first solve of a session builds an interpolation grid, but since momwire 0.15.0 that grid is reused across a band’s frequencies — a sweep pays a few fills on its first pass, then every repeat sweep and knob-drag tick runs warm in tens of milliseconds). Since momwire 0.8.0 the choice is uniform: every momwire solver honours both models (Sommerfeld validated against an independent NEC-2 implementation down to 0.02λ — within ~2.4 Ω on the B-spline bases, ~0.1 Ω on the sinusoidal basis, and the accelerators solve it on their fast paths), and PyNEC honours both natively — with one caveat: PyNEC’s (nec2++‘s) Sommerfeld solve is known-unreliable for conductors within 0.1λ of the ground that don’t touch it (low radials, half-squares, slopers — the engine warns; use a momwire engine as the reference in that configuration). The far-field pattern uses the real εr/σ on every basis. Whatever runs, the solve readout’s ground row reports the model that was actually used, and over a finite ground the norm check readout becomes a radiated percentage — the share of your input power that actually leaves as sky wave. The rest is absorbed power, not error.

One class of designs picks its own method: the buried-wire designs (verticals.buried_radial_vertical, verticals.elevated_buried_counterpoise, specialty.buried_dipole) declare that they require the Sommerfeld model — a conductor below the surface only exists under a Sommerfeld half-space, and the reflection-coefficient approximation refuses it by name. Loading one auto-selects finite ground with the Sommerfeld method and notes it in the ground panel; you can still flip the radio back, but the solver’s refusal is the answer you’ll get.

The terrain ground type models a site whose ground is not an infinite flat plane: a piecewise-linear height profile per azimuth sector, each facet carrying its own medium. Three presets cover the common cases, with every number a live knob:

  • levee — a raised crest with two sloped sides: crest width, slope, drop to water on the water bearing side, drop to land opposite. Crest and slopes are earth; the water medium starts at the water-side toe.
  • cliff — flat earth out to the cliff edge, then a sheer drop to water; arc < 360° restricts the cliff to a sector facing the bearing.
  • hillside — a flat bench on a slope: the ground rises at the uphill slope on one side and falls at the downhill slope on the other (facing the downhill bearing), all earth. There is no bottom to reference and none is needed — the slope itself is the reflector, so the effective height grows continuously as the elevation drops. One honest limit: below the uphill slope angle the real sky is shadowed by the hill, which a specular model cannot express.

Media are fixed in this version (water εr=80 σ=0.005, land and crest εr=13 σ=0.005 — shown read-only in the panel); arbitrary facet profiles and custom media are available from Python via antennaknobs.terrain.

How it solves: the impedance/current solution runs over a flat Sommerfeld ground with the crest medium (near-field ground interaction is crest-local; soil and water cannot be wire-gridded), and the far field reflects each ray off the facet its specular point lands on — tilted incidence, that facet’s Fresnel coefficients, and the facet’s height folded into the reflected-path phase, which is what lets a modest mast act electrically tall toward a drop-off. A single flat facet reproduces the plain finite ground exactly, and the solve readout’s ground row reports terrain (crest Somm.). It is a specular model: lobe positions and direction-dependent asymmetry are its business; diffraction behind a crest is not. The worked example — Antennas on a levee — shows what it changes on a real site and where each flat model fails.

Engine notes: momwire applies the facet far field natively. PyNEC runs a hybrid — NEC-2 has no facet model, so NEC solves the currents over the crest-medium Sommerfeld ground (exactly what the terrain recipe feeds the current solve anyway) and the server applies the facet reflection to those currents; the two engines agree to engine tolerance. The NEC rp overlay switch (PyNEC only) is greyed out over terrain, because NEC’s own rp_card pattern is flat-ground-only and would silently disagree with the facet traces. Download .nec exports the crest medium as a flat GN card — a NEC deck cannot carry the facets.

A wire that ends at exactly z = 0 connects to the ground plane — the return path a design like wire.terminated_longwire (fed and terminated against ground through its vertical legs) depends on. NEC-2 makes that connection physical over PEC ground only (finite-ground contact is a NEC-4 feature), so solve ground-connected designs with PEC selected; elevated designs are unaffected.

Designs can add their own rows to the solve readout (v0.43.0): a builder that computes physical diagnostics — the catenary inverted vee reports its rigging tension in N and lbf, sag, and derived rope cut length — sends them as self-describing rows the readout renders generically, so a new design idea (including a user design in ~/.antennaknobs/designs/) gets its numbers on screen with no frontend change.

When the design you’re iterating is a user design — editor in one window, workbench in the other — a reload button next to the design picker re-reads the file and re-solves in place. Your tuned knob values always survive the reload; a parameter the edited file just grew appears with the file’s default. One click instead of a page reload per edit cycle.

Designs with a lossy feed network — a real coax or ladder-line run, a matching network with a finite-Q coil, a lossy balun, a terminating resistor — get a power budget table in the solve readout: one row per network branch with the fraction of the source’s input power it dissipates, plus an antenna (accepted) row for what actually reaches the wires. The rows come straight from the MNA network solve (each branch current is an explicit unknown, so the watts are read off the solution, not modelled separately — see Station modelling for the network vocabulary). Rows from a station box are grouped under a header naming its instance and indented beneath it — one indent step per nesting level — and a design may retitle rows for display via ui_params["budget_labels"]. The same accounting drives the reported radiation efficiency: every dissipative branch counts, including resistive coupling and matching elements, not just explicit Loads. Gain is normalised by input power, so network loss already shows up in dBi; the budget tells you where it went. Lossless networks hide the branch rows.

Below the budget sits the honest bottom line: a radiated (incl. ground) row — the fraction of input power that leaves as far-field radiation after the ground has taken its share, the third of the three efficiency ledgers. It comes from the dwell-triggered norm check pattern integral, so it greys to the moment any knob moves and fills in once you settle — never costing the live drag path anything. Expect a shock the first time: a “95% efficient” portable vertical over average ground radiates ~30%; a 7 m-high inverted vee on 20 m about 70%. Over PEC ground or free space (nothing to absorb) it collapses back onto the structural efficiency.

Designs that declare a real wire material (a wire_type knob over the WIRES catalog, e.g. dipoles.pota_invvee) additionally get a wire loss (I²R) row: the skin-effect power burned in the antenna conductor itself, read from the solve’s current distribution. It counts toward the reported efficiency the same way the network rows do, and the Info pane shows the matching wire length and wire weight rows.

The Schematic view draws the design’s feed network — feedline, tuner, balun, and the port the source sits on — as a circuit chain, the same drawing the CLI writes with schematic. The server renders it to themed SVG on every knob change; no solve is involved, so component labels (lengths, C and L values, turns ratios) always match the knobs on screen.

  • The power budget is folded into the picture. Each box carries the fraction of input power it burns, drawn where the loss happens, so the budget rows and the circuit are one artifact rather than two. Elements that dissipate nothing — an ideal TL, a bypass() — appear here and nowhere else.
  • Balanced sections are drawn as two conductors. Past a floating balun’s secondary or along a balanced line the return rides the partner wire, so there is a second rail, the isolation barrier crosses the balun, and no ground symbol appears beyond it.
  • What isn’t a chain isn’t faked. A trap in a dipole leg or a curtain’s risers draw beneath the antenna labelled with the nodes they bridge; a parallel second antenna is noted rather than drawn in series; a 16-source array summarizes its feeds instead of drawing sixteen identical branches.
  • Roughly two-thirds of the catalog is a bare antenna with no feed circuit at all. Those say so — “No feed circuit — this design is the bare antenna” — instead of showing an empty box.

A local install needs the optional extra for this view: pip install 'antennaknobs[schematic]'.

Every number in the readout and every impedance chart is referenced to one port. For a bare antenna that’s the feedpoint; for a station design whose chain runs from the rig through a tuner and feedline, it’s the rig end — what a sweep taken in the shack sees.

When a design’s chain has more than one named port, the readout grows a plane selector, and the picked plane is marked on the schematic with the disconnected part of the chain dimmed. Picking a plane re-solves the design as a VNA clipped on at that port would see it: the chain upstream of the pick is unscrewed, not merely re-driven — leaving a length of open-ended coax dangling in parallel with the antenna would be neither what you asked for nor anything you could measure. Attachments that hang off the structure (traps, stubs wired into the antenna) are upstream of nothing, so they always stay.

The frequency sweep, convergence ladder, and chart titles all follow the pick, so a measurement overlay is compared at the plane you actually calibrated at. The CLI has the same control on fit via --plane.

To check that your chosen N is converged — i.e. adding more segments no longer moves the impedance — run a convergence sweep. It re-solves the current antenna across a range of N values and plots the resulting feed-point impedance, so you can see where the curve flattens out. (Like the freq sweep, it runs only while the Smith view that draws it is on screen — v0.43.0’s view-residency gating.) Basics: Segments & convergence; the full method (ladders, cross-basis validation with a second solver slot, and what a non-settling curve is telling you): How many segments?.

Measured overlay — your VNA on the Smith chart

Section titled “Measured overlay — your VNA on the Smith chart”

Under the Smith chart, measured .s1p… loads a one-port Touchstone file — a NanoVNA export, or any VNA’s — and draws the antenna you actually measured as a dashed violet locus against the modeled one. It is the “did my model match reality?” view, and the same overlay the CLI draws with --measured.

  • Capture is a local CLI step. python -m antennaknobs capture --out bench.s1p --start 27 --stop 30 sweeps an attached NanoVNA into a file you then load here. The server never opens a serial port — its ports are not yours when the backend runs elsewhere.
  • Your file stays on your machine. The browser reads it and posts the text to be parsed; nothing is stored server-side. That also means it works unchanged when the backend runs somewhere else while the VNA is plugged in here.
  • Reference impedance is handled. The measurement travels as impedance and is converted at the chart’s own reference, so a 75 Ω calibration lands correctly on a 50 Ω chart.
  • Bands are clipped to the frequency sweep so both loci cover the same frequencies; the label under the chart reports the span actually drawn, and says (clipped) when the measurement reaches past the swept band. A measurement with no overlap at all says so rather than silently drawing nothing — turn the freq sweep onto that band, or move the measurement frequency there.
  • Calibrate at the plane you’re comparing. The chart shows the feedpoint, so a measurement taken at the feedpoint is the like-for-like one; a shack-end sweep includes feedline the model may not have.

Expect some irreducible disagreement — common-mode current on a real feedline perturbs a measurement in ways a differential model doesn’t reproduce. A structured gap (the same offset across the band) usually points at something physical: line length, ground, a connector.

The sidebar is a notebook: the tabs across its top (D1, D2, …) are independent design sessions, each with its own geometry, knob values, design and measurement frequency, ground setting, solver slot, and results. Click + to open a new session — it starts fresh and solves on its own — switch by clicking a tab, and close one with the (the last remaining tab can’t be closed).

  • Sessions are fully independent: changing a knob, the solver, or the ground model in one leaves every other session exactly as you left it.
  • Hover a tab for its summary — design, solver, segment count, and ground model — e.g. dipoles.invvee · B-spline d=2 N=15 · reflection-coef ground.
  • Switching to a session re-solves it, which is near-instant because the server caches recent solves (see How a knob turn works).
  • The light/dark theme and pinned patterns are shared across all sessions; everything else is per-session.

Open the same design in two tabs to compare tunings, or load two different antennas — then pair it with pattern pinning to overlay one session’s radiation pattern on another’s.

The far-field views are calibrated azimuth and elevation polar cuts — the numbers-first presentation you read gain, takeoff angle, and beamwidth straight off. (The solver computes the full sphere on every basis; the cuts are how the workbench chooses to show it.)

On the azimuth and elevation pattern views a 📌 Pin pattern button (top-left of the plot) freezes the current radiation pattern as a dimmed, dashed ghost overlaid on the live one. Pin it, then change knobs — or switch to a completely different design, or another session tab — and the live lobe redraws over the pinned ghost so you can see the effect directly.

  • Pins are shared across every design session: pin in one tab and the ghost (and its table row) is there in all the others, so you can overlay one antenna’s pattern on another’s — a Yagi’s beam against a dipole’s figure-8, say — not just two tunings of the same design. A pin is a frozen snapshot: it survives switching designs and even closing the tab that made it.
  • Each pinned trace recomputes for whichever cut (azimuth or elevation) and cut-angle you’re viewing, so it always shares the live plot’s geometry.
  • A compare table appears alongside with a row per pattern — peak gain (dBi), takeoff angle, front-to-back, and −3 dB azimuth beamwidth — so the overlaid shapes come with the numbers that matter.
  • Show or hide a pin without losing it: click a pinned row’s colored swatch-and-name in the compare table. The ghost disappears from the plot and the row dims, but its metrics stay readable for the side-by-side numbers; click the name again to bring the ghost back in the same color. Handy when several pins crowd the plot and you want to declutter one at a time.
  • Removing pins: the on a row deletes that pin (everywhere — pins are shared); clear above the table removes them all. The button minimizes the table to a compact n pinned chip — ghosts stay on the plot — and clicking the chip reopens it.

On the azimuth and elevation pattern views a norm check checkbox (top-right of the plot) draws a second, dotted radiation curve over the solid live one — a built-in “should I trust this pattern?” gauge.

The two curves are the same pattern normalised two different ways. The solid line scales it by the input power the feed delivers (the circuit side — what the impedance solve says went in). The dotted line renormalises by the pattern’s own integrated radiated power (the field side — what the far-field integral says came back out). For a lossless PEC antenna those two must be equal, so:

  • The curves overlap ⇒ the solve conserves power: the mesh is fine enough that the currents and the radiated field agree.
  • A visible gap ⇒ discretisation error — too few segments (or too stiff a basis) for this geometry. Add segments and the gap closes.

Beside the checkbox a Δ readout gives that gap as one number in decibels — 0 dB is perfect power balance. A few tenths of a dB is typical and harmless; a large value means the pattern and its gain figures should not be trusted until you refine the mesh. This is exactly NEC’s classic “average gain” sanity check, which most tools make you compute by hand. It’s cheap (a closed-form integral for free space and PEC ground, a small reference-grid quadrature over finite ground, either way evaluated once the knob settles), so it’s on by default — uncheck it to hide the overlay and the readout.

Over a finite ground the gap is not supposed to be zero — it’s physics, so the readout switches to its honest form: radiated NN%, the fraction of input power that actually leaves as far-field radiation. The pattern integral only counts power that leaves upward — what the lossy ground absorbs never comes back — so the shortfall from 100% is structural loss plus real ground absorption, exactly like NEC’s average-gain value over real ground (hover the readout for the raw Δ dB). The same number fills the power budget’s radiated (incl. ground) row, so it follows you to every view. It’s still a mesh check: what should be small is how much the reading moves as you add segments, and switching the ground to PEC (or off) should send it back toward ~100% (Δ 0 dB).

The gear menu (⚙, top of the sidebar) has Copy params (Python), which copies the current knob values to the clipboard as a paste-ready default_params = {...} block (a <variant>_params block when you’re on a named variant). Drop it straight into a design file to bake in whatever you dialed in — no more transcribing values off the screen by hand.

The same gear menu also has Download .nec deck, which exports the design as a NEC-2 card deck for xnec2c / 4nec2 / EZNEC. The reverse — bringing a .nec deck someone published into the workbench — is Loading NEC decks.

On phones, the gear menu also has a full screen check (under display): it hides the system status and navigation bars so the whole screen is workbench — uncheck it or use the back gesture to exit. The control appears only in the mobile layout (desktop already has F11), and only on browsers with full-screen support (so not iPhone Safari).

A knob change sends one message over the /ws WebSocket; the server re-solves in a worker thread and sends the result back. Perceived latency is dominated by the solve time (free-space dipole-class solves are tens of milliseconds), not the network — so a regional server feels responsive for live tuning. Repeated solves of the same request hit a server-side cache, so flicking a knob back to a prior value is instant.