Last active
July 2, 2024 14:00
-
-
Save haxwithaxe/bc3e224dffd022e04333ccdc588c6107 to your computer and use it in GitHub Desktop.
Random wire antenna length calculator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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