RFTools.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. import math
  20. import cmath
  21. from threading import Lock
  22. from typing import Iterator, List, NamedTuple, Tuple
  23. import numpy as np
  24. from scipy.interpolate import interp1d
  25. from .SITools import Format, clamp_value
  26. FMT_FREQ = Format()
  27. FMT_SHORT = Format(max_nr_digits=4)
  28. FMT_SWEEP = Format(max_nr_digits=9, allow_strip=True)
  29. class Datapoint(NamedTuple):
  30. freq: int
  31. re: float
  32. im: float
  33. @property
  34. def z(self) -> complex:
  35. """ return the datapoint impedance as complex number """
  36. # FIXME: not impedance, but s11 ?
  37. return complex(self.re, self.im)
  38. @property
  39. def phase(self) -> float:
  40. """ return the datapoint's phase value """
  41. return cmath.phase(self.z)
  42. @property
  43. def gain(self) -> float:
  44. mag = abs(self.z)
  45. if mag > 0:
  46. return 20 * math.log10(mag)
  47. return -math.inf
  48. @property
  49. def vswr(self) -> float:
  50. mag = abs(self.z)
  51. if mag == 1:
  52. return 1
  53. return (1 + mag) / (1 - mag)
  54. @property
  55. def wavelength(self) -> float:
  56. return 299792458 / self.freq
  57. def impedance(self, ref_impedance: float = 50) -> complex:
  58. return gamma_to_impedance(self.z, ref_impedance)
  59. def shuntImpedance(self, ref_impedance: float = 50) -> complex:
  60. try:
  61. return 0.5 * ref_impedance * self.z / (1 - self.z)
  62. except ZeroDivisionError:
  63. return math.inf
  64. def seriesImpedance(self, ref_impedance: float = 50) -> complex:
  65. try:
  66. return 2 * ref_impedance * (1 - self.z) / self.z
  67. except ZeroDivisionError:
  68. return math.inf
  69. def qFactor(self, ref_impedance: float = 50) -> float:
  70. imp = self.impedance(ref_impedance)
  71. if imp.real == 0.0:
  72. return -1
  73. return abs(imp.imag / imp.real)
  74. def capacitiveEquivalent(self, ref_impedance: float = 50) -> float:
  75. return impedance_to_capacitance(self.impedance(ref_impedance), self.freq)
  76. def inductiveEquivalent(self, ref_impedance: float = 50) -> float:
  77. return impedance_to_inductance(self.impedance(ref_impedance), self.freq)
  78. def gamma_to_impedance(gamma: complex, ref_impedance: float = 50) -> complex:
  79. """Calculate impedance from gamma"""
  80. try:
  81. return ((-gamma - 1) / (gamma - 1)) * ref_impedance
  82. except ZeroDivisionError:
  83. return math.inf
  84. def groupDelay(data: List[Datapoint], index: int) -> float:
  85. idx0 = clamp_value(index - 1, 0, len(data) - 1)
  86. idx1 = clamp_value(index + 1, 0, len(data) - 1)
  87. delta_angle = data[idx1].phase - data[idx0].phase
  88. delta_freq = data[idx1].freq - data[idx0].freq
  89. if delta_freq == 0:
  90. return 0
  91. val = -delta_angle / math.tau / delta_freq
  92. return val
  93. def impedance_to_capacitance(z: complex, freq: float) -> float:
  94. """Calculate capacitive equivalent for reactance"""
  95. if freq == 0:
  96. return -math.inf
  97. if z.imag == 0:
  98. return math.inf
  99. return -(1 / (freq * 2 * math.pi * z.imag))
  100. def impedance_to_inductance(z: complex, freq: float) -> float:
  101. """Calculate inductive equivalent for reactance"""
  102. if freq == 0:
  103. return 0
  104. return z.imag * 1 / (freq * 2 * math.pi)
  105. def impedance_to_norm(z: complex, ref_impedance: float = 50) -> complex:
  106. """Calculate normalized z from impedance"""
  107. return z / ref_impedance
  108. def norm_to_impedance(z: complex, ref_impedance: float = 50) -> complex:
  109. """Calculate impedance from normalized z"""
  110. return z * ref_impedance
  111. def parallel_to_serial(z: complex) -> complex:
  112. """Convert parallel impedance to serial impedance equivalent"""
  113. z_sq_sum = z.real ** 2 + z.imag ** 2
  114. # TODO: Fix divide by zero
  115. return complex(z.real * z.imag ** 2 / z_sq_sum,
  116. z.real ** 2 * z.imag / z_sq_sum)
  117. def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex:
  118. """Calculate reflection coefficient for z"""
  119. return (z - ref_impedance) / (z + ref_impedance)
  120. def serial_to_parallel(z: complex) -> complex:
  121. """Convert serial impedance to parallel impedance equivalent"""
  122. z_sq_sum = z.real ** 2 + z.imag ** 2
  123. if z.real == 0 and z.imag == 0:
  124. return complex(math.inf, math.inf)
  125. # only possible if real and imag == 0, therefor commented out
  126. # if z_sq_sum == 0:
  127. # return complex(0, 0)
  128. if z.imag == 0:
  129. return complex(z_sq_sum / z.real, math.copysign(math.inf, z_sq_sum))
  130. if z.real == 0:
  131. return complex(math.copysign(math.inf, z_sq_sum), z_sq_sum / z.imag)
  132. return complex(z_sq_sum / z.real, z_sq_sum / z.imag)
  133. def corr_att_data(data: List[Datapoint], att: float) -> List[Datapoint]:
  134. """Correct the ratio for a given attenuation on s21 input"""
  135. if att <= 0:
  136. return data
  137. else:
  138. att = 10**(att / 20)
  139. ndata = []
  140. for dp in data:
  141. corrected = dp.z * att
  142. ndata.append(Datapoint(dp.freq, corrected.real, corrected.imag))
  143. return ndata