Skip to content

Instantly share code, notes, and snippets.

@haxwithaxe
Last active July 2, 2024 14:00
Show Gist options
  • Save haxwithaxe/bc3e224dffd022e04333ccdc588c6107 to your computer and use it in GitHub Desktop.
Save haxwithaxe/bc3e224dffd022e04333ccdc588c6107 to your computer and use it in GitHub Desktop.
Random wire antenna length calculator.
#!/usr/bin/env python3
"""Random wire antenna length calculator.
Options:
`--help` - Show the help message.
`-i`, `--imperial` - Use imperial units (feet).
`-m`, `--metric` (default) - Use metric units (meters).
`-h`, `--harmonics <count>` - The amount of harmonics to include in the
calculations. Defaults to `5`.
`-c`, `--check <length>` - Check the given length to see if it is a good
length for `<bands>`.
Usage:
python3 rw.py [-i|-m|-h <count>|-c <length>|] <band> [<band> ...]
Example:
* Check if 33ft is a good length for use with 80m-40m
python3 rw.py 80m 60m 40m --check 33 --imperial
* List the gaps between harmonics for 80m-40m
python3 rw.py 80m 60m 40m
* List the gaps between harmonics for 80m-40m out to the 10th harmonics
python3 rw.py --harmonics 10 80m 60m 40m
"""
import sys
from dataclasses import dataclass
from typing import Any, Generator, Optional, Union
FEET_PER_METER = 3.280839895
@dataclass
class Band:
"""Amateur radio band spec."""
length: int
"""Wave length in meters."""
start: float
"""Start frequency in kHz."""
end: float
"""End frequency in kHz."""
@property
def quarter_wave_ft(self) -> float:
"""Quarter wavelength in feet."""
return (self.length / 4) * FEET_PER_METER
@property
def quarter_wave_m(self) -> float:
"""Quarter wavelength in meters."""
return self.length / 4
def quarter_wave(self, use_metric: bool) -> float:
"""Quarter wavelength in feet or meters."""
if use_metric:
return self.quarter_wave_m
return self.quarter_wave_ft
def __gt__(self, other: 'Band') -> bool: # noqa: D105
return self.length > other.length
def __lt__(self, other: 'Band') -> bool: # noqa: D105
return self.length < other.length
def __eq__(self, other: Union['Band', float, int]) -> bool: # noqa: D105
if isinstance(other, self.__class__):
return self.length == other.length
if isinstance(other, str):
try:
return self.length == int(other.replace('m', ''))
except TypeError:
return False
if isinstance(other, (float, int)):
return self.length == other
return False
class Bands:
"""A group of bands."""
_bands = [
Band(160, 1800, 2000),
Band(80, 3500, 4000),
Band(60, 5330.5, 5405),
Band(40, 7000, 7300),
Band(30, 10100, 10150),
Band(20, 14000, 14350),
Band(17, 18068, 18168),
Band(15, 21000, 21450),
Band(12, 24890, 24990),
Band(10, 28000, 29700),
Band(6, 50000, 54000),
]
_bands.sort(key=lambda x: x.length)
def get(self, key: Any) -> Optional[Band]:
"""Return a `Band` or `None` corresponding to `key`."""
for band in self._bands:
if band == key:
return band
return None
def index(self, band: Any) -> int:
"""Return the index of `band`."""
return self._bands.index(self.get(band))
def keys(self) -> list[int]:
"""Return a list of `Band` lengths."""
return [x.length for x in self._bands]
def __contains__(self, band: Band) -> bool: # noqa: D105
return band in self._bands
def __getitem__(self, key: Any) -> Band: # noqa: D105
if isinstance(key, slice):
print(key)
start = self.index(key.start)
stop = self.index(key.stop)
return self._bands[start:stop]
return self.get(key)
def __iter__(self) -> iter: # noqa: D105
return iter(self._bands)
def __repr__(self) -> str: # noqa: D105
return ', '.join([f'{x.length}m' for x in self._bands])
BANDS = Bands()
def _to_band(arg: str) -> Union[bool, str]:
band = arg.replace('m', '')
try:
int(band)
except TypeError:
return False
if band not in BANDS:
return False
return band
def get_halfwave_range(
min_kHz: float,
max_kHz: float,
multiple: int,
loFreq_MHz: float,
use_metric: bool,
) -> Generator[tuple[float, float], None, None]:
"""Generate a list of half wavelengths and harmonics."""
# Half wave length per frequency
if use_metric:
length_freq = 300
else:
length_freq = 468
lambda_1 = 0
for n in range(1, multiple):
lambda_0 = n * length_freq / (max_kHz * 1e-3)
lambda_1 = n * length_freq / (min_kHz * 1e-3)
yield (lambda_0, lambda_1)
def get_halfwaves(
bands: list[Band],
multiple: int,
use_metric: bool,
) -> Generator[tuple[float, float], None, None]:
"""Generate a list of half wavelengths and harmonics for `bands`."""
low_freq = min([x.start for x in bands])
for band in bands:
yield from get_halfwave_range(
band.start,
band.end,
multiple=multiple,
loFreq_MHz=low_freq,
use_metric=use_metric,
)
def get_blocks(
halfwaves: list[tuple[float, float]],
) -> list[tuple[float, float]]:
"""Return a list of block of bad frequencies."""
stack = []
for harmonic in halfwaves:
if not stack:
stack.append(list(harmonic))
continue
matched = False
for item in stack:
# harmonic inside item
if item[0] <= harmonic[0] and item[1] >= harmonic[1]:
matched = True
break
# harmonic outside item
if item[0] < harmonic[1] or item[1] > harmonic[0]:
continue
# harmonic contains item
if item[0] >= harmonic[0] and item[1] <= harmonic[1]:
item[0] = harmonic[0]
item[1] = harmonic[1]
matched = True
break
# harmonic low freq below item low freq and harmonic high freq
# inside item
if item[0] > harmonic[0] and item[0] < harmonic[1] < item[1]:
item[0] = harmonic[0]
matched = True
break
# harmonic high freq above item and harmonic low freq inside item
if item[1] < harmonic[1] and item[0] < harmonic[0] < item[1]:
item[1] = harmonic[1]
matched = True
break
if not matched:
stack.append(list(harmonic))
return stack
def get_gaps(
bands: list[Band],
multiple: int,
use_metric: bool,
) -> Generator[tuple, None, None]:
"""Generate a list of gaps in the blocks of bad lengths."""
longest_band = bands[-1]
blocks = get_blocks(get_halfwaves(bands, multiple, use_metric))
blocks.sort(key=lambda x: x[0])
for i, block in enumerate(blocks):
if i >= len(blocks) - 1:
break
if longest_band.quarter_wave(use_metric) > blocks[i + 1][0]:
continue
if longest_band.quarter_wave(use_metric) > block[1]:
yield (longest_band.quarter_wave(use_metric), blocks[i + 1][0])
else:
yield (block[1], blocks[i + 1][0])
def print_gaps(bands, multiple, use_metric) -> None:
"""Print the gaps where good lengths are at."""
print('Gaps for good random wire lengths')
print('min\t\tmax')
for min_len, max_len in get_gaps(bands, multiple, use_metric):
if use_metric:
print(f'{min_len:.5f}m\t{max_len:.5f}m')
else:
print(f'{min_len:.3f}ft\t{max_len:.3f}ft')
def length_check(
good_length: float,
bands: list[Bands],
multiple: int,
use_metric: bool,
) -> Union[tuple[float, float], bool]:
"""Check if `good_length` is a good length for `bands`."""
gaps = get_gaps(bands, multiple, use_metric)
for gap in gaps:
if gap[0] <= good_length <= gap[1]:
return gap
return False
def main(): # noqa: D103
if len(sys.argv) < 2:
print(
'Provide desired bands (space separated)',
'from the selection below.',
)
print(BANDS)
sys.exit(1)
harmonics = 5
good_length = -1
bands = []
use_metric = True
skip = False
args = sys.argv[1:]
for arg in list(args):
if skip:
skip = False
continue
if arg == '--help':
print(
sys.argv[0],
'[--help|--harmonics <count>|--check <length>]',
'<desired bands>',
)
print(
'--help - Show this message.',
'-i, --imperial - Use imperial units (feet).',
'-m, --metric (default) - Use metric units (meters).',
'-h, --harmonics <count> - The amount of harmonics to include '
'in the calculations. Defaults to `5`.',
'-c, --check <length> - Check the given length to see if it '
'is a good length for <bands>.',
end='\n',
)
print('Bands:', BANDS)
sys.exit(1)
if arg in ('-h', '--harmonics'):
harmonics = int(args.pop(args.index(arg) + 1))
args.pop(args.index(arg))
skip = True
continue
if arg in ('-c', '--check'):
good_length = float(args.pop(args.index(arg) + 1))
args.pop(args.index(arg))
skip = True
continue
if arg in ('-m', '--metric'):
use_metric = True
args.pop(args.index(arg))
continue
if arg in ('-i', '--imperial'):
use_metric = False
args.pop(args.index(arg))
continue
if '-' in arg:
band_range = args.pop(args.index(arg))
first_band, second_band = band_range.split('-', 1)
first_band = int(first_band.strip().replace('m', ''))
second_band = int(second_band.strip().replace('m', ''))
if first_band > second_band:
low_band = second_band
high_band = first_band
else:
low_band = first_band
high_band = second_band
bands.extend(BANDS[low_band:high_band])
skip = True
continue
if _to_band(arg):
bands.append(BANDS[arg])
skip = True
continue
print(f'Invalid argument: {arg}')
sys.exit(1)
if good_length > 0:
gap = length_check(
good_length,
[BANDS[x] for x in args],
multiple=harmonics,
use_metric=use_metric,
)
if gap:
if use_metric:
print(
f'{good_length}m is in gap {gap[0]:.3f}m to '
f'{gap[1]:.3f}m', # nofmt
)
else:
print(
f'{good_length}ft is in gap {gap[0]:.3f}ft to '
f'{gap[1]:.3f}ft', # nofmt
)
sys.exit(0)
else:
print(
f'{good_length}ft is not a good random wire length for '
f'{", ".join(args)}', # nofmt
)
print(
f'{good_length}ft is not a good random wire length for '
f'{", ".join(args)}', # nofmt
)
sys.exit(1)
bands.extend([BANDS[x] for x in args])
print_gaps(
bands,
multiple=harmonics,
use_metric=use_metric,
)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment