nanovna-saver/src/NanoVNASaver/SITools.py

224 wiersze
6.4 KiB
Python

# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# 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/>.
from __future__ import annotations
import math
from decimal import Context, Decimal, InvalidOperation
from typing import NamedTuple
from numbers import Number, Real
PREFIXES = (
"q",
"r",
"y",
"z",
"a",
"f",
"p",
"n",
"µ",
"m",
"",
"k",
"M",
"G",
"T",
"P",
"E",
"Z",
"Y",
"R",
"Q",
)
def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
assert rmin <= rmax
return rmin if value < rmin else min(value, rmax)
def round_ceil(value: Real, digits: int = 0) -> Real:
factor = 10**-digits
return factor * math.ceil(value / factor)
def round_floor(value: Real, digits: int = 0) -> Real:
factor = 10**-digits
return factor * math.floor(value / factor)
def log_floor_125(x: float) -> float:
log_base = 10 ** (math.floor(math.log10(x)))
log_factor = x / log_base
if log_factor >= 5:
return 5 * log_base
return 2 * log_base if log_factor >= 2 else log_base
class Format(NamedTuple):
max_nr_digits: int = 6
fix_decimals: bool = False
space_str: str = ""
assume_infinity: bool = True
min_offset: int = -10
max_offset: int = 10
allow_strip: bool = False
allways_signed: bool = False
printable_min: float = -math.inf
printable_max: float = math.inf
unprintable_under: str = ""
unprintable_over: str = ""
parse_sloppy_unit: bool = False
parse_sloppy_kilo: bool = False
parse_clamp_min: float = -math.inf
parse_clamp_max: float = math.inf
class Value:
CTX = Context(prec=60, Emin=-33, Emax=33)
def __init__(self, value: Real = Decimal(0), unit: str = "", fmt=Format()):
assert 1 <= fmt.max_nr_digits <= 30
assert -10 <= fmt.min_offset <= fmt.max_offset <= 10
assert fmt.parse_clamp_min < fmt.parse_clamp_max
assert fmt.printable_min < fmt.printable_max
self._unit = unit
self.fmt = fmt
if isinstance(value, str):
self._value = Decimal(math.nan)
if value.lower() != "nan":
self.parse(value)
else:
self._value = Decimal(value, context=Value.CTX)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"{repr(self._value)}, '{self._unit}', {self.fmt})"
)
def __str__(self) -> str:
fmt = self.fmt
if math.isnan(self._value):
return f"-{fmt.space_str}{self._unit}"
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
)
if self._value < fmt.printable_min:
return fmt.unprintable_under + self._unit
if self._value > fmt.printable_max:
return fmt.unprintable_over + self._unit
offset = (
clamp_value(
int(math.log10(abs(self._value)) // 3),
fmt.min_offset,
fmt.max_offset,
)
if self._value
else 0
)
real = float(self._value) / (10 ** (offset * 3))
if fmt.max_nr_digits < 3:
formstr = ".0f"
else:
max_digits = fmt.max_nr_digits + (
(1 if not fmt.fix_decimals and abs(real) < 10 else 0)
+ (1 if not fmt.fix_decimals and abs(real) < 100 else 0)
)
formstr = f".{max_digits - 3}f"
if self.fmt.allways_signed:
formstr = f"+{formstr}"
result = format(real, formstr)
if float(result) == 0.0:
offset = 0
if self.fmt.allow_strip and "." in result:
result = result.rstrip("0").rstrip(".")
return result + fmt.space_str + PREFIXES[offset + 10] + self._unit
def __int__(self):
return round(self._value)
def __float__(self):
return float(self._value)
@property
def value(self):
return self._value
@value.setter
def value(self, value: Number):
self._value = Decimal(value, context=Value.CTX)
def parse(self, value: str) -> "Value":
if isinstance(value, Number):
self.value = value
return self
value = value.replace(" ", "") # Ignore spaces
if self._unit and (
value.endswith(self._unit)
or (
self.fmt.parse_sloppy_unit
and value.lower().endswith(self._unit.lower())
)
): # strip unit
value = value[: -len(self._unit)]
factor = 1
# fix for e.g. KHz, mHz gHz as milli-Hertz mostly makes no
# sense in NanoVNAs context
if self.fmt.parse_sloppy_kilo and value[-1] in ("K", "m", "g"):
value = value[:-1] + value[-1].swapcase()
if value[-1] in PREFIXES:
factor = 10 ** ((PREFIXES.index(value[-1]) - 10) * 3)
value = value[:-1]
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:
try:
self._value = Decimal(value, context=Value.CTX) * Decimal(
factor, context=Value.CTX
)
except InvalidOperation as exc:
raise ValueError() from exc
self._value = clamp_value(
self._value, self.fmt.parse_clamp_min, self.fmt.parse_clamp_max
)
return self
@property
def unit(self) -> str:
return self._unit