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
26 changes: 23 additions & 3 deletions src/ars_wireworks/results/buildsheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,9 @@ def _fan_dipole_cut_list(

def _is_model_driven_cut_list(model: AntennaModel) -> bool:
"""Whether ``model`` gets a cut list built from the model, not the deck."""
return isinstance(model, (FanDipoleModel, VerticalModel)) or (
_dipole_family_with_loads(model)
)
return isinstance(
model, (FanDipoleModel, VerticalModel, OcfdModel)
) or _dipole_family_with_loads(model)


def _compose_cut_list(
Expand All @@ -293,6 +293,8 @@ def _compose_cut_list(
return _fan_dipole_cut_list(model, trim_margin)
if isinstance(model, VerticalModel):
return _vertical_cut_list(model, trim_margin)
if isinstance(model, OcfdModel):
return _ocfd_cut_list(model, trim_margin)
if _dipole_family_with_loads(model):
return _loaded_dipole_cut_list(model, trim_margin)
return _cut_list(_wire_lengths(deck), trim_margin)
Expand Down Expand Up @@ -404,6 +406,24 @@ def _loaded_dipole_cut_list(
)


def _ocfd_cut_list(
model: OcfdModel, trim_margin: float
) -> tuple[CutListItem, ...]:
"""The two unequal legs of an off-center-fed dipole.

An OCFD/Windom is built as a short leg and a long leg off a centre balun;
the off-centre feed is the whole point of the design, so the cut list gives
each leg's length rather than one wire that hides where the feed goes.
"""
total = model.path.total_length_m if model.path else model.length_m
one_side = model.feed_fraction * total
legs = sorted((one_side, total - one_side))
return (
_section_item("Short leg (feedpoint → end)", legs[0], 1, trim_margin),
_section_item("Long leg (feedpoint → end)", legs[1], 1, trim_margin),
)


def _vertical_cut_list(
model: VerticalModel, trim_margin: float
) -> tuple[CutListItem, ...]:
Expand Down
26 changes: 21 additions & 5 deletions src/ars_wireworks/results/sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

from ars_wireworks.cards.build import build_deck
from ars_wireworks.cards.deck import CardDeck
from ars_wireworks.model.antenna import AntennaModel, FanDipoleModel
from ars_wireworks.model.antenna import (
AntennaModel,
FanDipoleModel,
VerticalModel,
)
from ars_wireworks.units import UnitSystem, format_length

_Point3 = tuple[float, float, float]
Expand Down Expand Up @@ -43,7 +47,13 @@ def geometry_sketch(
segments = _wire_segments(deck)
if not segments:
return _svg([], 'No geometry to draw.')
projection = _choose_projection(segments)
# A vertical or inverted-L is a fundamentally vertical antenna: its radials
# spread widest in plan view, which would collapse the radiator (and an
# inverted-L's riser + bent top) to a point. Force a side elevation so the
# vertical structure shows.
projection = _choose_projection(
segments, elevation_only=isinstance(model, VerticalModel)
)
feed = _feedpoint(deck, projection)
return _render(segments, projection, feed, unit_system)

Expand Down Expand Up @@ -113,17 +123,23 @@ def _wire_segments(deck: CardDeck) -> list[_Segment]:
return segments


def _choose_projection(segments: Sequence[_Segment]) -> str:
def _choose_projection(
segments: Sequence[_Segment], *, elevation_only: bool = False
) -> str:
"""The coordinate plane that shows the most structure.

``max`` keeps the first entry on a tie, so an elevation view (which shows
height above ground) is preferred over a flat plan view.
height above ground) is preferred over a flat plan view. With
``elevation_only`` the flat plan view is excluded entirely — for an antenna
whose point is its height (a vertical, an inverted-L).
"""
points = [point for segment in segments for point in segment]
x = _extent(p[0] for p in points)
y = _extent(p[1] for p in points)
z = _extent(p[2] for p in points)
candidates = (("XZ", x + z), ("YZ", y + z), ("XY", x + y))
candidates = [("XZ", x + z), ("YZ", y + z)]
if not elevation_only:
candidates.append(("XY", x + y))
return max(candidates, key=lambda item: item[1])[0]


Expand Down
17 changes: 17 additions & 0 deletions tests/test_buildsheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,20 @@ def test_plain_vertical_labels_radiator_and_radials() -> None:
assert "Ground radials" in by_desc and by_desc["Ground radials"].quantity == 4
# the radiator and radials are never merged into one line
assert len(sheet.cut_list) == 2


def test_ocfd_cut_list_gives_the_short_and_long_legs() -> None:
from ars_wireworks.model.antenna import OcfdModel

model = OcfdModel(frequency_hz=FREQ_40M_HZ, height_m=12.0, feed_fraction=1 / 3)
sheet = build_sheet(model, _results())
by_desc = {item.description: item for item in sheet.cut_list}
assert set(by_desc) == {
"Short leg (feedpoint → end)",
"Long leg (feedpoint → end)",
}
short = by_desc["Short leg (feedpoint → end)"].modeled_length_m
long = by_desc["Long leg (feedpoint → end)"].modeled_length_m
# one-third / two-thirds split of the half-wave, so the feed offset shows
assert short == pytest.approx(model.length_m / 3.0, rel=1e-3)
assert long == pytest.approx(short * 2.0, rel=1e-3)
30 changes: 30 additions & 0 deletions tests/test_sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,33 @@ def test_geometry_sketch_works_for_every_kind_of_geometry():
svg = geometry_sketch(model)
assert svg.startswith("<svg")
assert "</svg>" in svg


def test_vertical_and_inverted_l_use_a_side_elevation() -> None:
from ars_wireworks.model.path import SupportPoint, WirePath

# A plain vertical: radials spread widest in plan, but a plan view would
# collapse the vertical radiator — so it must draw a side elevation.
vertical = VerticalModel(
frequency_hz=FREQ_40M_HZ,
base_height_m=0.3,
radials=(RadialGroup(count=8, length_m=10.1, height_m=0.0),),
)
assert "Side elevation" in geometry_sketch(vertical)
assert "Plan view" not in geometry_sketch(vertical)

# An inverted-L's riser + bent top only show in elevation.
path = WirePath(
(
SupportPoint(0.0, 0.0, 0.5),
SupportPoint(0.0, 0.0, 12.0),
SupportPoint(20.0, 0.0, 12.0),
)
)
inverted_l = VerticalModel(
frequency_hz=3.75e6,
base_height_m=0.5,
radials=(RadialGroup(count=16, length_m=20.0, height_m=0.0),),
path=path,
)
assert "Side elevation" in geometry_sketch(inverted_l)
Loading