2019-11-10 08:18:45 +00:00
|
|
|
# NanoVNASaver
|
|
|
|
# A python program to view and export Touchstone data from a NanoVNA
|
|
|
|
# Copyright (C) 2019. Rune B. Broberg
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2019-11-10 08:00:51 +00:00
|
|
|
import math
|
2019-11-21 21:25:40 +00:00
|
|
|
import decimal
|
2019-11-12 22:15:33 +00:00
|
|
|
from typing import NamedTuple
|
2019-11-10 08:00:51 +00:00
|
|
|
from numbers import Number
|
|
|
|
|
|
|
|
PREFIXES = ("y", "z", "a", "f", "p", "n", "µ", "m",
|
|
|
|
"", "k", "M", "G", "T", "P", "E", "Z", "Y")
|
|
|
|
|
2019-11-20 15:56:09 +00:00
|
|
|
|
2019-11-12 22:15:33 +00:00
|
|
|
class Format(NamedTuple):
|
|
|
|
max_nr_digits: int = 6
|
|
|
|
fix_decimals: bool = False
|
|
|
|
space_str: str = ""
|
|
|
|
assume_infinity: bool = True
|
|
|
|
min_offset: int = -8
|
|
|
|
max_offset: int = 8
|
2019-11-18 07:00:45 +00:00
|
|
|
allow_strip: bool = False
|
2019-11-12 22:15:33 +00:00
|
|
|
parse_sloppy_unit: bool = False
|
|
|
|
parse_sloppy_kilo: bool = False
|
2019-11-10 08:00:51 +00:00
|
|
|
|
|
|
|
|
2019-11-20 15:56:09 +00:00
|
|
|
class Value:
|
2019-11-21 21:25:40 +00:00
|
|
|
CTX = decimal.Context(prec=60, Emin=-27, Emax=27)
|
|
|
|
|
2019-11-10 08:00:51 +00:00
|
|
|
def __init__(self, value: Number = 0, unit: str = "", fmt=Format()):
|
2019-11-21 21:25:40 +00:00
|
|
|
assert 3 <= fmt.max_nr_digits <= 30
|
2019-11-12 22:15:33 +00:00
|
|
|
assert -8 <= fmt.min_offset <= fmt.max_offset <= 8
|
2019-11-21 21:25:40 +00:00
|
|
|
self._value = decimal.Decimal(value, context=Value.CTX)
|
2019-11-10 08:00:51 +00:00
|
|
|
self._unit = unit
|
|
|
|
self.fmt = fmt
|
|
|
|
|
2019-11-12 14:54:57 +00:00
|
|
|
def __repr__(self) -> str:
|
2019-11-21 21:25:40 +00:00
|
|
|
return (f"{self.__class__.__name__}({self.value}, "
|
|
|
|
f"'{self._unit}', {self.fmt})")
|
2019-11-10 08:00:51 +00:00
|
|
|
|
2019-11-12 14:54:57 +00:00
|
|
|
def __str__(self) -> str:
|
2019-11-10 08:00:51 +00:00
|
|
|
fmt = self.fmt
|
2019-11-21 21:25:40 +00:00
|
|
|
if fmt.assume_infinity and \
|
|
|
|
abs(self._value) >= 10 ** ((fmt.max_offset + 1) * 3):
|
|
|
|
return (("-" if self._value < 0 else "") +
|
|
|
|
"\N{INFINITY}" + fmt.space_str + self._unit)
|
2019-11-10 08:00:51 +00:00
|
|
|
|
2019-11-21 21:25:40 +00:00
|
|
|
if self._value == 0:
|
2019-11-10 08:00:51 +00:00
|
|
|
offset = 0
|
|
|
|
else:
|
2019-11-21 21:25:40 +00:00
|
|
|
offset = int(math.log10(abs(self._value)) // 3)
|
2019-11-10 08:00:51 +00:00
|
|
|
|
|
|
|
if offset < fmt.min_offset:
|
|
|
|
offset = fmt.min_offset
|
|
|
|
elif offset > fmt.max_offset:
|
|
|
|
offset = fmt.max_offset
|
|
|
|
|
2019-11-21 21:25:40 +00:00
|
|
|
real = self._value / (10 ** (offset * 3))
|
2019-11-10 08:00:51 +00:00
|
|
|
|
|
|
|
if fmt.max_nr_digits < 4:
|
|
|
|
formstr = ".0f"
|
|
|
|
else:
|
2019-11-10 17:40:37 +00:00
|
|
|
max_digits = fmt.max_nr_digits + (
|
2019-11-11 09:51:24 +00:00
|
|
|
(1 if not fmt.fix_decimals and abs(real) < 10 else 0) +
|
|
|
|
(1 if not fmt.fix_decimals and abs(real) < 100 else 0))
|
2019-11-10 17:40:37 +00:00
|
|
|
formstr = "." + str(max_digits - 3) + "f"
|
2019-11-10 08:00:51 +00:00
|
|
|
|
|
|
|
result = format(real, formstr)
|
|
|
|
|
|
|
|
if float(result) == 0.0:
|
|
|
|
offset = 0
|
|
|
|
|
2019-11-18 07:00:45 +00:00
|
|
|
if self.fmt.allow_strip and "." in result:
|
|
|
|
result = result.rstrip("0").rstrip(".")
|
2019-11-21 21:25:40 +00:00
|
|
|
|
2019-11-10 08:00:51 +00:00
|
|
|
return result + fmt.space_str + PREFIXES[offset + 8] + self._unit
|
|
|
|
|
2019-11-21 21:25:40 +00:00
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return float(self._value)
|
|
|
|
|
2019-11-12 14:54:57 +00:00
|
|
|
def parse(self, value: str) -> float:
|
2019-11-10 08:00:51 +00:00
|
|
|
value = value.replace(" ", "") # Ignore spaces
|
2019-11-12 14:54:57 +00:00
|
|
|
|
|
|
|
if self._unit and (
|
2019-11-12 18:55:54 +00:00
|
|
|
value.endswith(self._unit) or
|
|
|
|
(self.fmt.parse_sloppy_unit and
|
|
|
|
value.lower().endswith(self._unit.lower()))): # strip unit
|
2019-11-10 08:00:51 +00:00
|
|
|
value = value[:-len(self._unit)]
|
|
|
|
|
|
|
|
factor = 1
|
2019-11-12 14:54:57 +00:00
|
|
|
if self.fmt.parse_sloppy_kilo and value[-1] == "K": # fix for e.g. KHz
|
|
|
|
value = value[:-1] + "k"
|
2019-11-10 08:00:51 +00:00
|
|
|
if value[-1] in PREFIXES:
|
|
|
|
factor = 10 ** ((PREFIXES.index(value[-1]) - 8) * 3)
|
|
|
|
value = value[:-1]
|
2019-11-12 14:54:57 +00:00
|
|
|
|
|
|
|
if self.fmt.assume_infinity and value == "\N{INFINITY}":
|
|
|
|
self.value = math.inf
|
|
|
|
elif self.fmt.assume_infinity and value == "-\N{INFINITY}":
|
|
|
|
self.value = -math.inf
|
|
|
|
else:
|
2019-11-21 21:25:40 +00:00
|
|
|
self.value = decimal.Decimal(value, context=Value.CTX) * factor
|
|
|
|
return float(self.value)
|
2019-11-10 08:00:51 +00:00
|
|
|
|
|
|
|
@property
|
2019-11-12 14:54:57 +00:00
|
|
|
def unit(self) -> str:
|
2019-11-10 08:00:51 +00:00
|
|
|
return self._unit
|