SITools.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. # NanoVNASaver
  2. #
  3. # A python program to view and export Touchstone data from a NanoVNA
  4. # Copyright (C) 2019, 2020 Rune B. Broberg
  5. # Copyright (C) 2020 NanoVNA-Saver Authors
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. from __future__ import annotations
  20. import math
  21. import decimal
  22. from typing import NamedTuple, Union
  23. from numbers import Number, Real
  24. PREFIXES = ("y", "z", "a", "f", "p", "n", "µ", "m",
  25. "", "k", "M", "G", "T", "P", "E", "Z", "Y")
  26. def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
  27. assert rmin <= rmax
  28. if value < rmin:
  29. return rmin
  30. if value > rmax:
  31. return rmax
  32. return value
  33. class Format(NamedTuple):
  34. max_nr_digits: int = 6
  35. fix_decimals: bool = False
  36. space_str: str = ""
  37. assume_infinity: bool = True
  38. min_offset: int = -8
  39. max_offset: int = 8
  40. allow_strip: bool = False
  41. allways_signed: bool = False
  42. printable_min: float = -math.inf
  43. printable_max: float = math.inf
  44. unprintable_under: str = ""
  45. unprintable_over: str = ""
  46. parse_sloppy_unit: bool = False
  47. parse_sloppy_kilo: bool = False
  48. parse_clamp_min: float = -math.inf
  49. parse_clamp_max: float = math.inf
  50. class Value:
  51. CTX = decimal.Context(prec=60, Emin=-27, Emax=27)
  52. def __init__(self,
  53. value: Union[Number, str] = 0,
  54. unit: str = "",
  55. fmt=Format()):
  56. assert 1 <= fmt.max_nr_digits <= 30
  57. assert -8 <= fmt.min_offset <= fmt.max_offset <= 8
  58. assert fmt.parse_clamp_min < fmt.parse_clamp_max
  59. assert fmt.printable_min < fmt.printable_max
  60. self._unit = unit
  61. self.fmt = fmt
  62. if isinstance(value, str):
  63. self._value = math.nan
  64. self.parse(value)
  65. else:
  66. self._value = decimal.Decimal(value, context=Value.CTX)
  67. def __repr__(self) -> str:
  68. return (f"{self.__class__.__name__}(" + repr(self._value) +
  69. f", '{self._unit}', {self.fmt})")
  70. def __str__(self) -> str:
  71. fmt = self.fmt
  72. if fmt.assume_infinity and abs(self._value) >= 10 ** ((fmt.max_offset + 1) * 3):
  73. return ("-" if self._value < 0 else "") + "\N{INFINITY}" + fmt.space_str + self._unit
  74. if self._value < fmt.printable_min:
  75. return fmt.unprintable_under + self._unit
  76. if self._value > fmt.printable_max:
  77. return fmt.unprintable_over + self._unit
  78. if self._value == 0:
  79. offset = 0
  80. else:
  81. offset = clamp_value(
  82. int(math.log10(abs(self._value)) // 3), fmt.min_offset, fmt.max_offset)
  83. real = float(self._value) / (10 ** (offset * 3))
  84. if fmt.max_nr_digits < 3:
  85. formstr = ".0f"
  86. else:
  87. max_digits = fmt.max_nr_digits + (
  88. (1 if not fmt.fix_decimals and abs(real) < 10 else 0) +
  89. (1 if not fmt.fix_decimals and abs(real) < 100 else 0))
  90. formstr = "." + str(max_digits - 3) + "f"
  91. if self.fmt.allways_signed:
  92. formstr = "+" + formstr
  93. result = format(real, formstr)
  94. if float(result) == 0.0:
  95. offset = 0
  96. if self.fmt.allow_strip and "." in result:
  97. result = result.rstrip("0").rstrip(".")
  98. return result + fmt.space_str + PREFIXES[offset + 8] + self._unit
  99. def __int__(self):
  100. return round(self._value)
  101. def __float__(self):
  102. return float(self._value)
  103. @property
  104. def value(self):
  105. return self._value
  106. @value.setter
  107. def value(self, value: Number):
  108. self._value = decimal.Decimal(value, context=Value.CTX)
  109. def parse(self, value: str) -> "Value":
  110. if isinstance(value, Number):
  111. self.value = value
  112. return self
  113. value = value.replace(" ", "") # Ignore spaces
  114. if self._unit and (
  115. value.endswith(self._unit) or
  116. (self.fmt.parse_sloppy_unit and
  117. value.lower().endswith(self._unit.lower()))): # strip unit
  118. value = value[:-len(self._unit)]
  119. factor = 1
  120. # fix for e.g. KHz, mHz gHz as milli-Hertz mostly makes no
  121. # sense in NanoVNAs context
  122. if self.fmt.parse_sloppy_kilo and value[-1] in ("K", "m", "g"):
  123. value = value[:-1] + value[-1].swapcase()
  124. if value[-1] in PREFIXES:
  125. factor = 10 ** ((PREFIXES.index(value[-1]) - 8) * 3)
  126. value = value[:-1]
  127. if self.fmt.assume_infinity and value == "\N{INFINITY}":
  128. self._value = math.inf
  129. elif self.fmt.assume_infinity and value == "-\N{INFINITY}":
  130. self._value = -math.inf
  131. else:
  132. try:
  133. self._value = (decimal.Decimal(value, context=Value.CTX)
  134. * decimal.Decimal(factor, context=Value.CTX))
  135. except decimal.InvalidOperation:
  136. raise ValueError
  137. self._value = clamp_value(
  138. self._value, self.fmt.parse_clamp_min, self.fmt.parse_clamp_max)
  139. return self
  140. @property
  141. def unit(self) -> str:
  142. return self._unit