Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]
## [1.2.0] - 2026-05-30

### Added

- Transmission-line feeders (and other non-radiating structures) now render as dashed lines in the 3D viewport, in both the Simulator and the Wire Editor — so antennas whose feeders are modelled as transmission lines (G5RV, log-periodic) no longer show a feedpoint floating disconnected from the antenna

### Changed

- Antenna templates can now declare lumped loads (`generateLoads`) and multiple/phased excitations (`generateExcitation` may return an array), enabling antennas that need tuning capacitors or phased feeders. The Simulator now runs through the unified advanced engine path; existing single-excitation templates are unaffected (verified identical results)

### Fixed

- Moxon Rectangle template produced grossly oversized elements (~1 wavelength wide instead of ~0.37λ), causing SWR >99 across the band. Replaced the dimension formulas with L.B. Cebik's (W4RNL) MoxGen regression equations and corrected the full-width vs. half-width handling (#63)
- End-Fed Half-Wave template stretched the radiating wire when the far-end height was changed (horizontal span was fixed at the half-wave length), making the conductor longer than λ/2 and shifting resonance below the band. The wire is now held at a fixed half-wave length and the far end tilts as a sloper, restoring resonance near the design frequency (SWR at design drops from ~3.9 to ~1.5 for the default 40m design)
- Fan Dipole template was only usable on its lowest band — 20m and 10m showed very high SWR. Three issues: (1) every element shared a single center node with the source on the longest element, so only that dipole was driven differentially while the others hung off the feed as quasi-parasitic stubs; (2) applying the fan spread stretched each element beyond its resonant length; (3) the end-effect shortening placed the coupled elements above their bands. Now all left/right halves connect to two feed terminals bridged by the driven segment (every dipole is fed across its center), each arm stays a fixed length while the spread only tilts it, and the element length compensates for fan coupling. Verified with nec2c: 20m SWR ~14→2.3 and 10m ~27→1.9 at band center for the default design
- Small Magnetic Loop template never resonated — it had no tuning capacitor and was fed directly (a directly-fed small loop is <1Ω, so SWR pegged near infinity). It now models a closed main loop with a series tuning capacitor (computed from the loop inductance) plus a fed Faraday coupling loop, with two controls: Coupling Loop Size (sets the feed resistance) and Capacitor Tuning (peaks resonance on frequency). Verified with nec2c: SWR ~500 → ~1.4 at resonance for the default design. Also corrected the feedpoint marker to use NEC coordinates
- G5RV template modelled the 450Ω open-wire matching section as a single radiating wire, giving the wrong impedance (~99:1 SWR by default). It now models a single dipole wire fed at its center segment through a 450Ω transmission line (with the line's velocity factor applied to the electrical length) to a coax stub. Verified with nec2c: ~1.9:1 on 20m (the G5RV's design band) with realistic per-band behaviour elsewhere
- Log-Periodic Dipole Array template only fed the front element, leaving the rest as floating parasitics — it was not a working LPDA. It now models the proper transposed phase-line feeder: a Carrel-designed feeder characteristic impedance, crossed (transposed) transmission lines between element centers, a shorted rear termination stub, and an element range extended past both band edges. Verified with nec2c: ~11 dBi forward gain and SWR mostly under 2 across 14–30 MHz. Also relaxed the backend transmission-line impedance constraint to allow a negative characteristic impedance, which is NEC's convention for a crossed/transposed line
- Wire Editor: transmission-line and lumped-load segment references now scale with the wire when a design-frequency change re-segments it (previously only excitations were scaled), keeping a loaded G5RV/LPDA feeder valid for simulation and fixing the feeder dashed line rendering at the wrong angle. The viewport also defensively clamps a stale segment reference onto the wire

## [1.1.1] - 2026-04-30

Expand Down Expand Up @@ -310,6 +322,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

This was the initial public release -- a complete rewrite of the original prototype into a production-quality application with React 19, TypeScript, FastAPI, and Docker.

[1.2.0]: https://github.com/EA1FUO/AntennaSim/compare/v1.1.1...v1.2.0
[1.0.0]: https://github.com/EA1FUO/AntennaSim/compare/v0.8.0...v1.0.0
[0.8.0]: https://github.com/EA1FUO/AntennaSim/compare/v0.7.7...v0.8.0
[0.7.7]: https://github.com/EA1FUO/AntennaSim/compare/v0.7.6...v0.7.7
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</p>

<p align="center">
<img src="https://img.shields.io/badge/version-1.1.1-blue?style=flat-square" alt="Version">
<img src="https://img.shields.io/badge/version-1.2.0-blue?style=flat-square" alt="Version">
<img src="https://img.shields.io/badge/license-GPL--3.0-green?style=flat-square" alt="License">
<img src="https://img.shields.io/badge/NEC2-engine-orange?style=flat-square" alt="NEC2">
<img src="https://img.shields.io/badge/docker-ready-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker">
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.1
1.2.0
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "antsim-backend"
version = "1.1.1"
version = "1.2.0"
description = "AntennaSim — Web Antenna Simulator Backend"
requires-python = ">=3.12"
dependencies = [
Expand Down
11 changes: 10 additions & 1 deletion backend/src/models/antenna.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,19 @@ class TransmissionLine(BaseModel):
segment1: int = Field(ge=1, le=200, description="Segment on first wire")
wire_tag2: int = Field(ge=1, le=9999, description="Second wire tag")
segment2: int = Field(ge=1, le=200, description="Segment on second wire")
impedance: float = Field(ge=1.0, le=1000.0, description="Characteristic impedance Z0 (Ohms)")
impedance: float = Field(
ge=-1000.0, le=1000.0,
description="Characteristic impedance Z0 (Ohms). A negative value selects a "
"crossed (transposed) line, e.g. a log-periodic phase line (NEC2 convention).")
length: float = Field(ge=0.0, le=1000.0, default=0.0,
description="Physical length (m). 0 = calculate from wire geometry")
shunt_admittance_real1: float = Field(default=0.0, description="Shunt admittance at end 1, real part")
shunt_admittance_imag1: float = Field(default=0.0, description="Shunt admittance at end 1, imag part")
shunt_admittance_real2: float = Field(default=0.0, description="Shunt admittance at end 2, real part")
shunt_admittance_imag2: float = Field(default=0.0, description="Shunt admittance at end 2, imag part")

@model_validator(mode="after")
def check_impedance_magnitude(self) -> "TransmissionLine":
if abs(self.impedance) < 1.0:
raise ValueError("Transmission line |impedance| must be at least 1 ohm")
return self
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "antsim-frontend",
"private": true,
"version": "1.1.1",
"version": "1.2.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/three/EditorScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { NearFieldPlane } from "./NearFieldPlane";
import { CurrentFlowParticles } from "./CurrentFlowParticles";
import { RadiationSlice } from "./RadiationSlice";
import { SceneRaycaster } from "./SceneRaycaster";
import { NonRadiatingLines } from "./NonRadiatingLines";
import { resolveTransmissionLines } from "./transmissionLineViz";
import type { ViewToggles } from "./types";
import type { PatternData, SegmentCurrent, NearFieldResult } from "../../api/nec";
import { useUIStore } from "../../stores/uiStore";
Expand Down Expand Up @@ -138,6 +140,7 @@ function EditorSceneContent({

const wires = useEditorStore((s) => s.wires);
const excitations = useEditorStore((s) => s.excitations);
const transmissionLines = useEditorStore((s) => s.transmissionLines);
const selectedTags = useEditorStore((s) => s.selectedTags);
const mode = useEditorStore((s) => s.mode);
const snapSize = useEditorStore((s) => s.snapSize);
Expand Down Expand Up @@ -518,6 +521,12 @@ function EditorSceneContent({
[excitations]
);

// Transmission-line feeders drawn as dashed (non-radiating) lines.
const feederSegments = useMemo(
() => resolveTransmissionLines(transmissionLines, wireDataList),
[transmissionLines, wireDataList]
);

// Antenna centroid for pattern
const antennaCentroid = useMemo((): [number, number, number] => {
if (wires.length === 0) return [0, 0, 0];
Expand Down Expand Up @@ -588,6 +597,11 @@ function EditorSceneContent({
/>
))}

{/* Transmission-line feeders drawn as dashed (non-radiating) lines */}
{viewToggles.wires && feederSegments.length > 0 && (
<NonRadiatingLines segments={feederSegments} />
)}

{/* Ghost wire preview (add mode) */}
{ghostWire && (
<GhostWire start={ghostWire.start} end={ghostWire.end} />
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/components/three/NonRadiatingLines.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Renders non-radiating structures (transmission-line feeders, support stays,
* etc.) as dashed lines in the 3D viewport. These elements carry no antenna
* current, so they are drawn distinctly from the solid radiating wires.
*/

import { Line } from "@react-three/drei";
import type { NonRadiatingSegment } from "./transmissionLineViz";

/** NEC2 (Z=up) → Three.js (Y=up): [necX, necZ, -necY]. */
function toThree([x, y, z]: [number, number, number]): [number, number, number] {
return [x, z, -y];
}

interface NonRadiatingLinesProps {
segments: NonRadiatingSegment[];
color?: string;
}

export function NonRadiatingLines({ segments, color = "#7dd3fc" }: NonRadiatingLinesProps) {
return (
<>
{segments.map((seg, i) => (
<Line
key={i}
points={[toThree(seg.start), toThree(seg.end)]}
color={color}
lineWidth={1.5}
dashed
dashSize={0.4}
gapSize={0.25}
transparent
opacity={0.85}
/>
))}
</>
);
}
10 changes: 10 additions & 0 deletions frontend/src/components/three/SceneRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { CompassRose } from "./CompassRose";
import { AxesHelper } from "./AxesHelper";
import { AntennaModel, JunctionSpheres } from "./AntennaModel";
import { FeedpointMarker } from "./FeedpointMarker";
import { NonRadiatingLines } from "./NonRadiatingLines";
import type { NonRadiatingSegment } from "./transmissionLineViz";
import { CameraControls } from "./CameraControls";
import { PostProcessing } from "./PostProcessing";
import { RadiationPattern3D } from "./RadiationPattern3D";
Expand All @@ -24,6 +26,8 @@ interface SceneRootProps {
wires: WireData[];
feedpoints: FeedpointData[];
viewToggles: ViewToggles;
/** Non-radiating structures (e.g. transmission-line feeders) drawn dashed */
nonRadiatingLines?: NonRadiatingSegment[];
/** Radiation pattern data to render as 3D mesh */
patternData?: PatternData | null;
/** V2: Current distribution data */
Expand All @@ -36,6 +40,7 @@ export function SceneRoot({
wires,
feedpoints,
viewToggles,
nonRadiatingLines,
patternData,
currents,
nearField,
Expand Down Expand Up @@ -124,6 +129,11 @@ export function SceneRoot({
<FeedpointMarker key={i} position={fp.position} />
))}

{/* Non-radiating structures (transmission-line feeders) drawn dashed */}
{viewToggles.wires && nonRadiatingLines && nonRadiatingLines.length > 0 && (
<NonRadiatingLines segments={nonRadiatingLines} />
)}

{/* 3D Radiation Pattern — surface mode */}
{viewToggles.pattern && !viewToggles.volumetric && patternData && (
<RadiationPattern3D
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/components/three/transmissionLineViz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Resolves transmission-line port references into drawable 3D segments for the
* viewport. Kept separate from the rendering component so the component file
* only exports a component (React Fast Refresh requirement).
*/

import type { WireGeometry } from "../../templates/types";
import type { TransmissionLine } from "../../api/nec";

/** A straight segment in NEC coordinates (X=east, Y=north, Z=up). */
export interface NonRadiatingSegment {
start: [number, number, number];
end: [number, number, number];
}

/** Center of a given 1-based segment along a wire, in NEC coordinates. */
function segmentCenter(wire: WireGeometry, segment: number): [number, number, number] {
// Clamp to the wire's segment range so a stale reference (e.g. after the wire
// was re-segmented) lands on the wire instead of extrapolating past its end.
const seg = Math.max(1, Math.min(segment, wire.segments));
const t = (seg - 0.5) / wire.segments;
return [
wire.x1 + t * (wire.x2 - wire.x1),
wire.y1 + t * (wire.y2 - wire.y1),
wire.z1 + t * (wire.z2 - wire.z1),
];
}

/**
* Resolve transmission-line port references (wire tag + segment) into segments
* connecting the two endpoints, so the feeder can be drawn in the viewport.
*/
export function resolveTransmissionLines(
transmissionLines: TransmissionLine[],
wires: WireGeometry[]
): NonRadiatingSegment[] {
const wireByTag = new Map<number, WireGeometry>();
for (const w of wires) {
if (!wireByTag.has(w.tag)) wireByTag.set(w.tag, w);
}

const segments: NonRadiatingSegment[] = [];
for (const tl of transmissionLines) {
const w1 = wireByTag.get(tl.wire_tag1);
const w2 = wireByTag.get(tl.wire_tag2);
if (!w1 || !w2) continue;
segments.push({
start: segmentCenter(w1, tl.segment1),
end: segmentCenter(w2, tl.segment2),
});
}
return segments;
}
10 changes: 6 additions & 4 deletions frontend/src/engine/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ function makeRequest(templateId: string, groundType: string): SimulateAdvancedRe
const t = templates.find((tmpl) => tmpl.id === templateId)!;
const params = getDefaultParams(t);
const wires = t.generateGeometry(params);
const ex = t.generateExcitation(params, wires);
const rawEx = t.generateExcitation(params, wires);
const excitations = Array.isArray(rawEx) ? rawEx : [rawEx];
const freq = t.defaultFrequencyRange(params);
return {
wires,
excitations: [ex],
excitations,
ground: { type: groundType as "free_space" | "average" | "perfect" },
frequency: freq,
};
Expand Down Expand Up @@ -123,12 +124,13 @@ describe("All templates produce valid NEC2 card decks", () => {
it(`${t.id} + ${ground.type}: valid NEC2 structure`, () => {
const params = getDefaultParams(t);
const wires = t.generateGeometry(params);
const ex = t.generateExcitation(params, wires);
const rawEx = t.generateExcitation(params, wires);
const excitations = Array.isArray(rawEx) ? rawEx : [rawEx];
const freq = t.defaultFrequencyRange(params);

const req: SimulateAdvancedRequest = {
wires,
excitations: [ex],
excitations,
ground,
frequency: freq,
};
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/pages/EditorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export function EditorPage() {
const moveAllWiresZ = useEditorStore((s) => s.moveAllWiresZ);
const clearAll = useEditorStore((s) => s.clearAll);
const setWires = useEditorStore((s) => s.setWires);
const addLoad = useEditorStore((s) => s.addLoad);
const addTransmissionLine = useEditorStore((s) => s.addTransmissionLine);

// Simulation store
const simStatus = useSimulationStore((s) => s.status);
Expand Down Expand Up @@ -237,17 +239,24 @@ export function EditorPage() {

const handleLoadTemplate = useCallback(() => {
const geom = selectedTemplate.generateGeometry(templateParams);
const exc = selectedTemplate.generateExcitation(templateParams, geom);
const rawExc = selectedTemplate.generateExcitation(templateParams, geom);
const excitations = Array.isArray(rawExc) ? rawExc : [rawExc];
const templateLoads = selectedTemplate.generateLoads?.(templateParams, geom) ?? [];
const templateTLs = selectedTemplate.generateTransmissionLines?.(templateParams, geom) ?? [];
const freqRange = selectedTemplate.defaultFrequencyRange(templateParams);
const freqParam = templateParams.frequency ?? templateParams.freq ?? 14.15;

// Clear editor and load template wires
// Clear editor and load template wires + excitations
clearAll();
setWires(
geom.map((w) => ({ ...w, selected: false })),
[exc]
excitations
);

// Carry over any template loads / transmission lines
templateLoads.forEach((load) => addLoad(load));
templateTLs.forEach((tl) => addTransmissionLine(tl));

// Update design frequency and sweep range
setDesignFrequency(freqParam);
setFrequencyRange(freqRange);
Expand All @@ -258,7 +267,7 @@ export function EditorPage() {

// Switch to wires section after loading
setEditorSection("wires");
}, [selectedTemplate, templateParams, clearAll, setWires, setDesignFrequency, setFrequencyRange, setGround, setMatching]);
}, [selectedTemplate, templateParams, clearAll, setWires, addLoad, addTransmissionLine, setDesignFrequency, setFrequencyRange, setGround, setMatching, setEditorSection]);

const handleBandSelect = useCallback(
(range: FrequencyRange, _band: HamBand) => {
Expand Down
Loading
Loading