Merge branch 'upstream_Development'

pull/106/head
Holger Mueller 2019-11-19 19:20:14 +01:00
commit d22bd74316
18 zmienionych plików z 2963 dodań i 330 usunięć

Wyświetl plik

@ -46,8 +46,8 @@ class Analysis:
return 0, 0
frequency1 = self.app.data21[location1].freq
frequency2 = self.app.data21[location2].freq
gain1 = RFTools.gain(self.app.data21[location1])
gain2 = RFTools.gain(self.app.data21[location2])
gain1 = self.app.data21[location1].gain
gain2 = self.app.data21[location2].gain
frequency_factor = frequency2 / frequency1
if frequency_factor < 1:
frequency_factor = 1 / frequency_factor
@ -105,13 +105,13 @@ class LowPassAnalysis(Analysis):
self.result_label.setText("Please place " + self.app.markers[0].name + " in the passband.")
return
pass_band_db = RFTools.gain(self.app.data21[pass_band_location])
pass_band_db = self.app.data21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
@ -126,9 +126,9 @@ class LowPassAnalysis(Analysis):
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = RFTools.gain(self.app.data21[initial_cutoff_location])
peak_db = self.app.data21[initial_cutoff_location].gain
for i in range(0, initial_cutoff_location):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
@ -141,14 +141,14 @@ class LowPassAnalysis(Analysis):
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data21[cutoff_location].freq
cutoff_gain = RFTools.gain(self.app.data21[cutoff_location]) - pass_band_db
cutoff_gain = self.app.data21[cutoff_location].gain - pass_band_db
if cutoff_gain < -4:
logger.debug("Cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
cutoff_gain)
@ -161,7 +161,7 @@ class LowPassAnalysis(Analysis):
six_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 6:
# We found 6dB location
six_db_location = i
@ -175,7 +175,7 @@ class LowPassAnalysis(Analysis):
ten_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 10:
# We found 6dB location
ten_db_location = i
@ -183,7 +183,7 @@ class LowPassAnalysis(Analysis):
twenty_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 20:
# We found 6dB location
twenty_db_location = i
@ -191,7 +191,7 @@ class LowPassAnalysis(Analysis):
sixty_db_location = -1
for i in range(six_db_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
@ -265,13 +265,13 @@ class HighPassAnalysis(Analysis):
self.result_label.setText("Please place " + self.app.markers[0].name + " in the passband.")
return
pass_band_db = RFTools.gain(self.app.data21[pass_band_location])
pass_band_db = self.app.data21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
@ -286,10 +286,9 @@ class HighPassAnalysis(Analysis):
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = RFTools.gain(self.app.data21[initial_cutoff_location])
peak_db = self.app.data21[initial_cutoff_location].gain
for i in range(len(self.app.data21) - 1, initial_cutoff_location - 1, -1):
db = RFTools.gain(self.app.data21[i])
if db > peak_db:
if self.app.data21[i].gain > peak_db:
peak_db = db
peak_location = i
@ -301,14 +300,13 @@ class HighPassAnalysis(Analysis):
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data21[cutoff_location].freq
cutoff_gain = RFTools.gain(self.app.data21[cutoff_location]) - pass_band_db
cutoff_gain = self.app.data21[cutoff_location].gain - pass_band_db
if cutoff_gain < -4:
logger.debug("Cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
cutoff_gain)
@ -322,8 +320,7 @@ class HighPassAnalysis(Analysis):
six_db_location = -1
for i in range(cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 6:
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
six_db_location = i
break
@ -336,24 +333,21 @@ class HighPassAnalysis(Analysis):
ten_db_location = -1
for i in range(cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 10:
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 20:
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(six_db_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 60:
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
@ -469,14 +463,13 @@ class BandPassAnalysis(Analysis):
self.result_label.setText("Please place " + self.app.markers[0].name + " in the passband.")
return
pass_band_db = RFTools.gain(self.app.data21[pass_band_location])
pass_band_db = self.app.data21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_lower_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found a cutoff location
initial_lower_cutoff_location = i
break
@ -491,8 +484,7 @@ class BandPassAnalysis(Analysis):
initial_upper_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data21), 1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found a cutoff location
initial_upper_cutoff_location = i
break
@ -506,9 +498,9 @@ class BandPassAnalysis(Analysis):
logger.debug("Found initial upper cutoff frequency at %d", initial_upper_cutoff_frequency)
peak_location = -1
peak_db = RFTools.gain(self.app.data21[initial_lower_cutoff_location])
peak_db = self.app.data21[initial_lower_cutoff_location].gain
for i in range(initial_lower_cutoff_location, initial_upper_cutoff_location, 1):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
@ -518,14 +510,13 @@ class BandPassAnalysis(Analysis):
lower_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = self.app.data21[lower_cutoff_location].freq
lower_cutoff_gain = RFTools.gain(self.app.data21[lower_cutoff_location]) - pass_band_db
lower_cutoff_gain = self.app.data21[lower_cutoff_location].gain - pass_band_db
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
@ -542,14 +533,13 @@ class BandPassAnalysis(Analysis):
upper_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data21), 1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data21[upper_cutoff_location].freq
upper_cutoff_gain = RFTools.gain(self.app.data21[upper_cutoff_location]) - pass_band_db
upper_cutoff_gain = self.app.data21[upper_cutoff_location].gain - pass_band_db
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
@ -576,8 +566,7 @@ class BandPassAnalysis(Analysis):
lower_six_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 6:
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
@ -590,24 +579,21 @@ class BandPassAnalysis(Analysis):
ten_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 10:
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 20:
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_six_db_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 60:
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
@ -636,8 +622,7 @@ class BandPassAnalysis(Analysis):
upper_six_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data21), 1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 6:
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
@ -654,24 +639,21 @@ class BandPassAnalysis(Analysis):
ten_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data21), 1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 10:
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data21), 1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 20:
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, len(self.app.data21), 1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 60:
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
@ -782,9 +764,9 @@ class BandStopAnalysis(Analysis):
return
peak_location = -1
peak_db = RFTools.gain(self.app.data21[0])
peak_db = self.app.data21[0].gain
for i in range(len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
db = self.app.data21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
@ -794,14 +776,13 @@ class BandStopAnalysis(Analysis):
lower_cutoff_location = -1
pass_band_db = peak_db
for i in range(len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = self.app.data21[lower_cutoff_location].freq
lower_cutoff_gain = RFTools.gain(self.app.data21[lower_cutoff_location]) - pass_band_db
lower_cutoff_gain = self.app.data21[lower_cutoff_location].gain - pass_band_db
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
@ -817,14 +798,13 @@ class BandStopAnalysis(Analysis):
upper_cutoff_location = -1
for i in range(len(self.app.data21)-1, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
if (pass_band_db - self.app.data21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data21[upper_cutoff_location].freq
upper_cutoff_gain = RFTools.gain(self.app.data21[upper_cutoff_location]) - pass_band_db
upper_cutoff_gain = self.app.data21[upper_cutoff_location].gain - pass_band_db
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
@ -851,8 +831,7 @@ class BandStopAnalysis(Analysis):
lower_six_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 6:
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
@ -865,24 +844,21 @@ class BandStopAnalysis(Analysis):
ten_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 10:
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 20:
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_six_db_location, len(self.app.data21)):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 60:
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
@ -910,8 +886,7 @@ class BandStopAnalysis(Analysis):
upper_six_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 6:
if (pass_band_db - self.app.data21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
@ -928,24 +903,21 @@ class BandStopAnalysis(Analysis):
ten_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 10:
if (pass_band_db - self.app.data21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 20:
if (pass_band_db - self.app.data21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, -1, -1):
db = RFTools.gain(self.app.data21[i])
if (pass_band_db - db) > 60:
if (pass_band_db - self.app.data21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
@ -1030,27 +1002,22 @@ class SimplePeakSearchAnalysis(Analysis):
suffix = ""
data = []
for d in self.app.data:
vswr = RFTools.calculateVSWR(d)
if vswr < 1:
vswr = float('inf')
data.append(vswr)
data.append(d.vswr)
elif self.rbtn_data_resistance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data:
re, im = RFTools.normalize50(d)
data.append(re)
data.append(d.impedance().real)
elif self.rbtn_data_reactance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data:
re, im = RFTools.normalize50(d)
data.append(im)
data.append(d.impedance().imag)
elif self.rbtn_data_s21_gain.isChecked():
suffix = " dB"
data = []
for d in self.app.data21:
data.append(RFTools.gain(d))
data.append(d.gain)
else:
logger.warning("Searching for peaks on unknown data")
return
@ -1137,11 +1104,11 @@ class PeakSearchAnalysis(Analysis):
if self.rbtn_data_vswr.isChecked():
data = []
for d in self.app.data:
data.append(RFTools.calculateVSWR(d))
data.append(d.vswr)
elif self.rbtn_data_s21_gain.isChecked():
data = []
for d in self.app.data21:
data.append(RFTools.gain(d))
data.append(d.gain)
else:
logger.warning("Searching for peaks on unknown data")
return
@ -1228,10 +1195,7 @@ class VSWRAnalysis(Analysis):
max_dips_shown = 3
data = []
for d in self.app.data:
vswr = RFTools.calculateVSWR(d)
if vswr < 1:
vswr = float('inf')
data.append(vswr)
data.append(d.vswr)
# min_idx = np.argmin(data)
#
# logger.debug("Minimum at %d", min_idx)

Wyświetl plik

@ -143,7 +143,7 @@ class Chart(QtWidgets.QWidget):
self.markerSize = size
self.update()
def getActiveMarker(self, event: QtGui.QMouseEvent) -> Marker:
def getActiveMarker(self) -> Marker:
if self.draggedMarker is not None:
return self.draggedMarker
for m in self.markers:
@ -409,6 +409,7 @@ class FrequencyChart(Chart):
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self))
self.menu.addAction(self.action_popout)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText("Start (" + Chart.shortenFrequency(self.minFrequency) + ")")
@ -583,8 +584,8 @@ class FrequencyChart(Chart):
val2 = self.valueAtPosition(y2)
if len(val1) == len(val2) == 1 and val1[0] != val2[0]:
self.minDisplayValue = round(min(val1[0], val2[0]), 2)
self.maxDisplayValue = round(max(val1[0], val2[0]), 2)
self.minDisplayValue = round(min(val1[0], val2[0]), 3)
self.maxDisplayValue = round(max(val1[0], val2[0]), 3)
self.setFixedValues(True)
freq1 = max(1, self.frequencyAtPosition(x1, limit=False))
@ -628,7 +629,7 @@ class FrequencyChart(Chart):
return
else:
a0.accept()
m = self.getActiveMarker(a0)
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(f))
m.frequencyInput.setText(str(f))
@ -792,6 +793,16 @@ class FrequencyChart(Chart):
new_chart.action_set_linear_x.setChecked(not self.logarithmicX)
return new_chart
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
m = self.getActiveMarker()
if m is not None and a0.modifiers() == QtCore.Qt.NoModifier:
if a0.key() == QtCore.Qt.Key_Down or a0.key() == QtCore.Qt.Key_Left:
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(a0.type(), QtCore.Qt.Key_Down, a0.modifiers()))
elif a0.key() == QtCore.Qt.Key_Up or a0.key() == QtCore.Qt.Key_Right:
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(a0.type(), QtCore.Qt.Key_Up, a0.modifiers()))
else:
super().keyPressEvent(a0)
class SquareChart(Chart):
def __init__(self, name):
@ -872,11 +883,11 @@ class PhaseChart(FrequencyChart):
if self.unwrap:
rawData = []
for d in self.data:
rawData.append(RFTools.phaseAngleRadians(d))
rawData.append(d.phase)
rawReference = []
for d in self.reference:
rawReference.append(RFTools.phaseAngleRadians(d))
rawReference.append(d.phase)
self.unwrappedData = np.degrees(np.unwrap(rawData))
self.unwrappedReference = np.degrees(np.unwrap(rawReference))
@ -955,9 +966,9 @@ class PhaseChart(FrequencyChart):
elif d in self.reference:
angle = self.unwrappedReference[self.reference.index(d)]
else:
angle = RFTools.phaseAngle(d)
angle = math.degrees(d.phase)
else:
angle = RFTools.phaseAngle(d)
angle = math.degrees(d.phase)
return self.topMargin + round((self.maxAngle - angle) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
@ -1054,7 +1065,7 @@ class VSWRChart(FrequencyChart):
minVSWR = 1
maxVSWR = 3
for d in self.data:
vswr = RFTools.calculateVSWR(d)
vswr = d.vswr
if vswr > maxVSWR:
maxVSWR = vswr
maxVSWR = min(self.maxDisplayValue, math.ceil(maxVSWR))
@ -1136,8 +1147,7 @@ class VSWRChart(FrequencyChart):
return self.topMargin + round((self.maxVSWR - vswr) / self.span * self.chartHeight)
def getYPosition(self, d: Datapoint) -> int:
vswr = RFTools.calculateVSWR(d)
return self.getYPositionFromValue(vswr)
return self.getYPositionFromValue(d.vswr)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
@ -1278,7 +1288,7 @@ class PolarChart(SquareChart):
positions.append(math.sqrt((x - thisx)**2 + (y - thisy)**2))
minimum_position = positions.index(min(positions))
m = self.getActiveMarker(a0)
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(round(target[minimum_position].freq)))
m.frequencyInput.setText(str(round(target[minimum_position].freq)))
@ -1424,7 +1434,7 @@ class SmithChart(SquareChart):
positions.append(math.sqrt((x - thisx)**2 + (y - thisy)**2))
minimum_position = positions.index(min(positions))
m = self.getActiveMarker(a0)
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(round(target[minimum_position].freq)))
m.frequencyInput.setText(str(round(target[minimum_position].freq)))
@ -1610,9 +1620,9 @@ class LogMagChart(FrequencyChart):
def logMag(self, p: Datapoint) -> float:
if self.isInverted:
return -RFTools.gain(p)
return -p.gain
else:
return RFTools.gain(p)
return p.gain
def copy(self):
new_chart: LogMagChart = super().copy()
@ -1761,9 +1771,9 @@ class SParameterChart(FrequencyChart):
def logMag(self, p: Datapoint) -> float:
if self.isInverted:
return -RFTools.gain(p)
return -p.gain
else:
return RFTools.gain(p)
return p.gain
def copy(self):
new_chart: LogMagChart = super().copy()
@ -2020,9 +2030,9 @@ class CombinedLogMagChart(FrequencyChart):
def logMag(self, p: Datapoint) -> float:
if self.isInverted:
return -RFTools.gain(p)
return -p.gain
else:
return RFTools.gain(p)
return p.gain
def copy(self):
new_chart: LogMagChart = super().copy()
@ -2074,7 +2084,7 @@ class QualityFactorChart(FrequencyChart):
minQ = 0
maxQ = 0
for d in self.data:
Q = RFTools.qualityFactor(d)
Q = d.q_factor()
if Q > maxQ:
maxQ = Q
scale = 0
@ -2147,7 +2157,7 @@ class QualityFactorChart(FrequencyChart):
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
Q = RFTools.qualityFactor(d)
Q = d.q_factor()
return self.topMargin + round((self.maxQ - Q) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
@ -2581,7 +2591,8 @@ class RealImaginaryChart(FrequencyChart):
max_real = 0
max_imag = -1000
for d in self.data:
re, im = RFTools.normalize50(d)
imp = d.impedance()
re, im = imp.real, imp.imag
if re > max_real:
max_real = re
if re < min_real:
@ -2593,7 +2604,8 @@ class RealImaginaryChart(FrequencyChart):
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < fstart or d.freq > fstop:
continue
re, im = RFTools.normalize50(d)
imp = d.impedance()
re, im = imp.real, imp.imag
if re > max_real:
max_real = re
if re < min_real:
@ -2793,11 +2805,11 @@ class RealImaginaryChart(FrequencyChart):
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m)+1)
def getImYPosition(self, d: Datapoint) -> int:
_, im = RFTools.normalize50(d)
im = d.impedance().imag
return self.topMargin + round((self.max_imag - im) / self.span_imag * self.chartHeight)
def getReYPosition(self, d: Datapoint) -> int:
re, _ = RFTools.normalize50(d)
re = d.impedance().real
return self.topMargin + round((self.max_real - re) / self.span_real * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
@ -3180,8 +3192,7 @@ class MagnitudeZChart(FrequencyChart):
@staticmethod
def magnitude(p: Datapoint) -> float:
re, im = RFTools.normalize50(p)
return math.sqrt(re**2 + im**2)
return abs(p.impedance())
def copy(self):
new_chart: LogMagChart = super().copy()
@ -3283,7 +3294,8 @@ class PermeabilityChart(FrequencyChart):
min_val = 1000
max_val = -1000
for d in self.data:
re, im = RFTools.normalize50(d)
imp = d.impedance()
re, im = imp.real, imp.imag
re = re * 10e6 / d.freq
im = im * 10e6 / d.freq
if re > max_val:
@ -3297,7 +3309,8 @@ class PermeabilityChart(FrequencyChart):
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < fstart or d.freq > fstop:
continue
re, im = RFTools.normalize50(d)
imp = d.impedance()
re, im = imp.real, imp.imag
re = re * 10e6 / d.freq
im = im * 10e6 / d.freq
if re > max_val:
@ -3462,7 +3475,7 @@ class PermeabilityChart(FrequencyChart):
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m)+1)
def getImYPosition(self, d: Datapoint) -> int:
_, im = RFTools.normalize50(d)
im = d.impedance().imag
im = im * 10e6 / d.freq
if self.logarithmicY:
min_val = self.max - self.span
@ -3475,7 +3488,7 @@ class PermeabilityChart(FrequencyChart):
return self.topMargin + round((self.max - im) / self.span * self.chartHeight)
def getReYPosition(self, d: Datapoint) -> int:
re, _ = RFTools.normalize50(d)
re = d.impedance().real
re = re * 10e6 / d.freq
if self.logarithmicY:
min_val = self.max - self.span
@ -3569,11 +3582,11 @@ class GroupDelayChart(FrequencyChart):
def calculateGroupDelay(self):
rawData = []
for d in self.data:
rawData.append(RFTools.phaseAngleRadians(d))
rawData.append(d.phase)
rawReference = []
for d in self.reference:
rawReference.append(RFTools.phaseAngleRadians(d))
rawReference.append(d.phase)
if len(self.data) > 0:
self.unwrappedData = np.degrees(np.unwrap(rawData))
@ -3707,3 +3720,249 @@ class GroupDelayChart(FrequencyChart):
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxDelay)
return [val]
class CapacitanceChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.minValue = -1
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin, self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (F)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue / 10e11
minValue = self.minDisplayValue / 10e11
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 1
maxValue = -1
for d in self.data:
val = d.to_capacitive_equivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
val = d.to_capacitive_equivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
self.maxValue = maxValue
self.minValue = minValue
span = maxValue - minValue
if span == 0:
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
span = 1e-15
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
fmt = Format(max_nr_digits=3)
for i in range(target_ticks):
val = minValue + (i / target_ticks) * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
valstr = str(Value(val, fmt=fmt))
qp.drawText(3, y + 3, valstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
qp.drawText(3, self.chartHeight+self.topMargin, str(Value(minValue, fmt=fmt)))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
return self.topMargin + round((self.maxValue - d.to_capacitive_equivalent()) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val * 10e11]
def copy(self):
new_chart: CapacitanceChart = super().copy()
new_chart.span = self.span
return new_chart
class InductanceChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
self.leftMargin = 30
self.chartWidth = 250
self.chartHeight = 250
self.minDisplayValue = 0
self.maxDisplayValue = 100
self.minValue = -1
self.maxValue = 1
self.span = 1
self.setMinimumSize(self.chartWidth + self.rightMargin + self.leftMargin, self.chartHeight + self.topMargin + self.bottomMargin)
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
qp.drawText(3, 15, self.name + " (H)")
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight, self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
pen = QtGui.QPen(self.sweepColor)
pen.setWidth(self.pointSize)
line_pen = QtGui.QPen(self.sweepColor)
line_pen.setWidth(self.lineThickness)
highlighter = QtGui.QPen(QtGui.QColor(20, 0, 255))
highlighter.setWidth(1)
if not self.fixedSpan:
if len(self.data) > 0:
fstart = self.data[0].freq
fstop = self.data[len(self.data)-1].freq
else:
fstart = self.reference[0].freq
fstop = self.reference[len(self.reference) - 1].freq
self.fstart = fstart
self.fstop = fstop
else:
fstart = self.fstart = self.minFrequency
fstop = self.fstop = self.maxFrequency
# Draw bands if required
if self.bands.enabled:
self.drawBands(qp, fstart, fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue / 10e11
minValue = self.minDisplayValue / 10e11
self.maxValue = maxValue
self.minValue = minValue
else:
# Find scaling
minValue = 1
maxValue = -1
for d in self.data:
val = d.to_inductive_equivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
val = d.to_inductive_equivalent()
if val > maxValue:
maxValue = val
if val < minValue:
minValue = val
self.maxValue = maxValue
self.minValue = minValue
span = maxValue - minValue
if span == 0:
logger.info("Span is zero for CapacitanceChart, setting to a small value.")
span = 1e-15
self.span = span
target_ticks = math.floor(self.chartHeight / 60)
fmt = Format(max_nr_digits=3)
for i in range(target_ticks):
val = minValue + (i / target_ticks) * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
qp.setPen(self.textColor)
if val != minValue:
valstr = str(Value(val, fmt=fmt))
qp.drawText(3, y + 3, valstr)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
qp.setPen(QtGui.QPen(self.foregroundColor))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.chartWidth, self.topMargin)
qp.setPen(self.textColor)
qp.drawText(3, self.topMargin + 4, str(Value(maxValue, fmt=fmt)))
qp.drawText(3, self.chartHeight+self.topMargin, str(Value(minValue, fmt=fmt)))
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
self.drawData(qp, self.reference, self.referenceColor)
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
return self.topMargin + round((self.maxValue - d.to_inductive_equivalent()) / self.span * self.chartHeight)
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
return [val * 10e11]
def copy(self):
new_chart: InductanceChart = super().copy()
new_chart.span = self.span
return new_chart

Wyświetl plik

@ -306,8 +306,9 @@ class Marker(QtCore.QObject):
def updateLabels(self, s11data: List[Datapoint], s21data: List[Datapoint]):
if self.location != -1:
re50, im50 = RFTools.normalize50(s11data[self.location])
vswr = RFTools.calculateVSWR(s11data[self.location])
imp = s11data[self.location].impedance()
re50, im50 = imp.real, imp.imag
vswr = s11data[self.location].vswr
if re50 > 0:
rp = (re50 ** 2 + im50 ** 2) / re50
rp = round(rp, 3 - max(0, math.floor(math.log10(abs(rp)))))
@ -362,9 +363,9 @@ class Marker(QtCore.QObject):
self.parallel_r_label.setText(rpstr + " \N{OHM SIGN}")
self.parallel_x_label.setText(xpstr)
if self.returnloss_is_positive:
returnloss = -round(RFTools.gain(s11data[self.location]), 3)
returnloss = -round(s11data[self.location].gain, 3)
else:
returnloss = round(RFTools.gain(s11data[self.location]), 3)
returnloss = round(s11data[self.location].gain, 3)
self.returnloss_label.setText(str(returnloss) + " dB")
capacitance = RFTools.capacitanceEquivalent(im50, s11data[self.location].freq)
inductance = RFTools.inductanceEquivalent(im50, s11data[self.location].freq)
@ -380,7 +381,7 @@ class Marker(QtCore.QObject):
if vswr < 0:
vswr = "-"
self.vswr_label.setText(str(vswr))
q = RFTools.qualityFactor(s11data[self.location])
q = s11data[self.location].q_factor()
if q > 10000 or q < 0:
q_str = "\N{INFINITY}"
elif q > 1000:
@ -393,14 +394,14 @@ class Marker(QtCore.QObject):
q_str = str(round(q, 3))
self.quality_factor_label.setText(q_str)
self.s11_phase_label.setText(
str(round(RFTools.phaseAngle(s11data[self.location]), 2)) + "\N{DEGREE SIGN}")
str(round(math.degrees(s11data[self.location].phase), 2)) + "\N{DEGREE SIGN}")
fmt = SITools.Format(max_nr_digits=5, space_str=" ")
self.s11_group_delay_label.setText(str(SITools.Value(RFTools.groupDelay(s11data, self.location), "s", fmt)))
if len(s21data) == len(s11data):
self.gain_label.setText(str(round(RFTools.gain(s21data[self.location]), 3)) + " dB")
self.gain_label.setText(str(round(s21data[self.location].gain, 3)) + " dB")
self.s21_phase_label.setText(
str(round(RFTools.phaseAngle(s21data[self.location]), 2)) + "\N{DEGREE SIGN}")
str(round(math.degrees(s21data[self.location].phase), 2)) + "\N{DEGREE SIGN}")
self.s21_group_delay_label.setText(str(SITools.Value(RFTools.groupDelay(s21data, self.location) / 2,
"s", fmt)))

Wyświetl plik

@ -32,7 +32,7 @@ from .Hardware import VNA, InvalidVNA, Version
from .RFTools import RFTools, Datapoint
from .Chart import Chart, PhaseChart, VSWRChart, PolarChart, SmithChart, LogMagChart, QualityFactorChart, TDRChart, \
RealImaginaryChart, MagnitudeChart, MagnitudeZChart, CombinedLogMagChart, SParameterChart, PermeabilityChart, \
GroupDelayChart
GroupDelayChart, CapacitanceChart, InductanceChart
from .Calibration import CalibrationWindow, Calibration
from .Marker import Marker
from .SweepWorker import SweepWorker
@ -135,6 +135,8 @@ class NanoVNASaver(QtWidgets.QWidget):
self.s11Phase = PhaseChart("S11 Phase")
self.s21Phase = PhaseChart("S21 Phase")
self.s11GroupDelay = GroupDelayChart("S11 Group Delay")
self.s11CapacitanceChart = CapacitanceChart("S11 Serial C")
self.s11InductanceChart = InductanceChart("S11 Serial L")
self.s21GroupDelay = GroupDelayChart("S21 Group Delay", reflective=False)
self.permabilityChart = PermeabilityChart("S11 R/\N{GREEK SMALL LETTER OMEGA} & X/\N{GREEK SMALL LETTER OMEGA}")
self.s11VSWR = VSWRChart("S11 VSWR")
@ -156,6 +158,8 @@ class NanoVNASaver(QtWidgets.QWidget):
self.s11charts.append(self.s11RealImaginary)
self.s11charts.append(self.s11QualityFactor)
self.s11charts.append(self.s11SParameterChart)
self.s11charts.append(self.s11CapacitanceChart)
self.s11charts.append(self.s11InductanceChart)
self.s11charts.append(self.permabilityChart)
# List of all the S21 charts, for selecting
@ -528,7 +532,7 @@ class NanoVNASaver(QtWidgets.QWidget):
def rescanSerialPort(self):
self.serialPortInput.clear()
for port in self.getPort():
self.serialPortInput.insertItem(1,port)
self.serialPortInput.insertItem(1, port)
# Get that windows port
@staticmethod
@ -625,7 +629,7 @@ class NanoVNASaver(QtWidgets.QWidget):
def startSerial(self):
if self.serialLock.acquire():
self.serialPort = self.serialPortInput.text()
self.serialPort = self.serialPortInput.currentText()
logger.info("Opening serial port %s", self.serialPort)
try:
self.serial = serial.Serial(port=self.serialPort, baudrate=115200)
@ -750,7 +754,7 @@ class NanoVNASaver(QtWidgets.QWidget):
min_vswr = 100
min_vswr_freq = -1
for d in self.data:
vswr = RFTools.calculateVSWR(d)
vswr = d.vswr
if min_vswr > vswr > 0:
min_vswr = vswr
min_vswr_freq = d.freq
@ -770,7 +774,7 @@ class NanoVNASaver(QtWidgets.QWidget):
max_gain = -100
max_gain_freq = -1
for d in self.data21:
gain = RFTools.gain(d)
gain = d.gain
if gain > max_gain:
max_gain = gain
max_gain_freq = d.freq

Wyświetl plik

@ -1,4 +1,5 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# 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
@ -13,60 +14,81 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import collections
import math
from numbers import Number
from typing import List
import cmath
from numbers import Number, Real
from typing import List, NamedTuple
from NanoVNASaver.SITools import Value, Format
PREFIXES = ("", "k", "M", "G", "T")
Datapoint = collections.namedtuple('Datapoint', 'freq re im')
def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
assert rmin <= rmax
if value < rmin:
return rmin
if value > rmax:
return rmax
return value
class RFTools:
@staticmethod
def normalize50(data: Datapoint):
re = data.re
im = data.im
re50 = 50 * (1 - re * re - im * im) / (1 + re * re + im * im - 2 * re)
im50 = 50 * (2 * im) / (1 + re * re + im * im - 2 * re)
return re50, im50
class Datapoint(NamedTuple):
freq: int
re: float
im: float
@staticmethod
def gain(data: Datapoint) -> float:
# re50, im50 = normalize50(data)
# Calculate the gain / reflection coefficient
# mag = math.sqrt((re50 - 50) * (re50 - 50) + im50 * im50) / \
# math.sqrt((re50 + 50) * (re50 + 50) + im50 * im50)
#
# Magnitude = |Gamma|:
mag = math.sqrt(data.re**2 + data.im**2)
@property
def z(self):
""" return datapoint impedance as complex number """
return complex(self.re, self.im)
@property
def phase(self):
""" return datapoints phase value """
return cmath.phase(self.z)
@property
def gain(self) -> float:
mag = abs(self.z)
if mag > 0:
return 20 * math.log10(mag)
return 0
@staticmethod
def qualityFactor(data: Datapoint):
re50, im50 = RFTools.normalize50(data)
if re50 != 0:
Q = abs(im50 / re50)
else:
Q = -1
return Q
@property
def vswr(self) -> float:
mag = abs(self.z)
if mag == 1:
return 1
if mag > 1:
return math.inf
return (1 + mag) / (1 - mag)
@staticmethod
def calculateVSWR(data: Datapoint):
# re50, im50 = normalize50(data)
try:
# mag = math.sqrt((re50 - 50) * (re50 - 50) + im50 * im50) / \
# math.sqrt((re50 + 50) * (re50 + 50) + im50 * im50)
mag = math.sqrt(data.re**2 + data.im**2)
vswr = (1 + mag) / (1 - mag)
except ZeroDivisionError:
vswr = 1
return vswr
def impedance(self, ref_impedance: float = 50) -> complex:
return ref_impedance * ((-self.z - 1) / (self.z - 1))
def q_factor(self, ref_impedance: float = 50) -> float:
imp = self.impedance(ref_impedance)
if imp.real == 0.0:
return -1
return abs(imp.imag / imp.real)
def capacitive_equivalent(self, ref_impedance: float = 50) -> float:
if self.freq == 0:
return math.inf
imp = self.impedance(ref_impedance)
if imp.imag == 0:
return math.inf
return -(1 / (self.freq * 2 * math.pi * imp.imag))
def inductive_equivalent(self, ref_impedance: float = 50) -> float:
if self.freq == 0:
return math.inf
imp = self.impedance(ref_impedance)
if imp.imag == 0:
return 0
return imp.imag * 1 / (self.freq * 2 * math.pi)
class RFTools:
@staticmethod
def capacitanceEquivalent(im50, freq) -> str:
if im50 == 0 or freq == 0:
@ -91,7 +113,7 @@ class RFTools:
@staticmethod
def formatSweepFrequency(freq: Number) -> str:
return str(Value(freq, "Hz", Format(max_nr_digits=5)))
return str(Value(freq, "Hz", Format(max_nr_digits=9, allow_strip=True)))
@staticmethod
def parseFrequency(freq: str) -> int:
@ -101,40 +123,15 @@ class RFTools:
except (ValueError, IndexError):
return -1
@staticmethod
def phaseAngle(data: Datapoint) -> float:
re = data.re
im = data.im
return math.degrees(math.atan2(im, re))
@staticmethod
def phaseAngleRadians(data: Datapoint) -> float:
re = data.re
im = data.im
return math.atan2(im, re)
@staticmethod
def clamp_int(value: int, min: int, max: int) -> int:
assert min <= max
if value < min:
return min
if value > max:
return max
return value
@staticmethod
def groupDelay(data: List[Datapoint], index: int) -> float:
index0 = RFTools.clamp_int(index - 1, 0, len(data) - 1)
index1 = RFTools.clamp_int(index + 1, 0, len(data) - 1)
angle0 = RFTools.phaseAngleRadians(data[index0])
angle1 = RFTools.phaseAngleRadians(data[index1])
freq0 = data[index0].freq
freq1 = data[index1].freq
delta_angle = (angle1 - angle0)
idx0 = clamp_value(index - 1, 0, len(data) - 1)
idx1 = clamp_value(index + 1, 0, len(data) - 1)
delta_angle = (data[idx1].phase - data[idx0].phase)
if abs(delta_angle) > math.tau:
if delta_angle > 0:
delta_angle = delta_angle % math.tau
else:
delta_angle = -1 * (delta_angle % math.tau)
val = -delta_angle / math.tau / (freq1 - freq0)
val = -delta_angle / math.tau / (data[idx1].freq - data[idx0].freq)
return val

Wyświetl plik

@ -28,6 +28,7 @@ class Format(NamedTuple):
assume_infinity: bool = True
min_offset: int = -8
max_offset: int = 8
allow_strip: bool = False
parse_sloppy_unit: bool = False
parse_sloppy_kilo: bool = False
@ -73,6 +74,9 @@ class Value():
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 + 8] + self._unit
def parse(self, value: str) -> float:

Wyświetl plik

@ -1,4 +1,5 @@
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# 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
@ -13,132 +14,167 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import collections
import logging
import math
import re
from typing import List
from .RFTools import Datapoint
import cmath
import io
from NanoVNASaver.RFTools import Datapoint
logger = logging.getLogger(__name__)
class Touchstone:
s11data: List[Datapoint] = []
s21data: List[Datapoint] = []
comments = []
filename = ""
class Options:
# Fun fact: In Touchstone 1.1 spec all params are optional unordered.
# Just the line has to start with "#"
UNIT_TO_FACTOR = {
"ghz": 10**9,
"mhz": 10**6,
"khz": 10**3,
"hz": 10**0,
}
VALID_UNITS = UNIT_TO_FACTOR.keys()
VALID_PARAMETERS = "syzgh"
VALID_FORMATS = ("ma", "db", "ri")
def __init__(self, filename):
def __init__(self,
unit: str = "GHZ",
parameter: str = "S",
t_format: str = "ma",
resistance: int = 50):
# set defaults
assert unit.lower() in Options.VALID_UNITS
assert parameter.lower() in Options.VALID_PARAMETERS
assert t_format.lower() in Options.VALID_FORMATS
assert resistance > 0
self.unit = unit.lower()
self.parameter = parameter.lower()
self.format = t_format.lower()
self.resistance = resistance
@property
def factor(self) -> int:
return Options.UNIT_TO_FACTOR[self.unit]
def __str__(self) -> str:
return (
f"# {self.unit} {self.parameter}"
f" {self.format} r {self.resistance}"
).upper()
def parse(self, line: str):
if not line.startswith("#"):
raise TypeError("Not an option line: " + line)
pfact = pparam = pformat = presist = False
params = iter(line[1:].lower().split())
for p in params:
if p in Options.VALID_UNITS and not pfact:
self.factor = Options.UNIT_TO_FACTOR[p]
pfact = True
elif p in Options.VALID_PARAMETERS and not pparam:
self.parameter = p
pparam = True
elif p in Options.VALID_FORMATS and not pformat:
self.format = p
pformat = True
elif p == "r" and not presist:
self.resistance = int(next(params))
else:
raise TypeError("Illegal option line: " + line)
class Touchstone:
def __init__(self, filename: str):
self.filename = filename
self.sdata = [[], [], [], []] # at max 4 data pairs
self.comments = []
self.opts = Options()
@property
def s11data(self) -> list:
return self.sdata[0]
@property
def s21data(self) -> list:
return self.sdata[1]
@property
def s12data(self) -> list:
return self.sdata[2]
@property
def s22data(self) -> list:
return self.sdata[3]
def _parse_comments(self, fp) -> str:
for line in fp:
line = line.strip()
if line.startswith("!"):
logger.info(line)
self.comments.append(line)
continue
return line
def _append_line_data(self, freq: float, data: list):
data_list = iter(self.sdata)
vals = iter(data)
for v in vals:
if self.opts.format == "ri":
next(data_list).append(
Datapoint(freq, float(v), float(next(vals))))
if self.opts.format == "ma":
z = cmath.polar(float(v),
math.radians(float(next(vals))))
next(data_list).append(Datapoint(freq, z.real, z.imag))
if self.opts.format == "db":
z = cmath.polar(math.exp(float(v) / 20),
math.radians(float(next(vals))))
next(data_list).append(Datapoint(freq, z.real, z.imag))
def load(self):
self.s11data = []
self.s21data = []
realimaginary = False
magnitudeangle = False
factor = 1
logger.info("Attempting to open file %s", self.filename)
try:
logger.info("Attempting to open file %s", self.filename)
file = open(self.filename, "r")
lines = file.readlines()
parsed_header = False
for line in lines:
line = line.strip()
if line.startswith("!"):
logger.info(line)
self.comments.append(line)
continue
if line.startswith("#") and not parsed_header:
pattern = "^# (.?HZ) (S )?RI( R 50)?$"
match = re.match(pattern, line.upper())
if match:
logger.debug("Found header for RealImaginary and %s", match.group(1))
match = match.group(1)
parsed_header = True
realimaginary = True
if match == "HZ":
factor = 1
elif match == "KHZ":
factor = 10**3
elif match == "MHZ":
factor = 10**6
elif match == "GHZ":
factor = 10**9
else:
factor = 10**9 # Default Touchstone frequency unit is GHz
continue
pattern = "^# (.?HZ) (S )?MA( R 50)?$"
match = re.match(pattern, line.upper())
if match:
logger.debug("Found header for MagnitudeAngle and %s", match.group(1))
match = match.group(1)
parsed_header = True
magnitudeangle = True
if match == "HZ":
factor = 1
elif match == "KHZ":
factor = 10**3
elif match == "MHZ":
factor = 10**6
elif match == "GHZ":
factor = 10**9
else:
factor = 10**9 # Default Touchstone frequency unit is GHz
continue
# else:
# This is some other comment line
logger.debug("Comment line: %s", line)
continue
if not parsed_header:
logger.warning("Read line without having read header: %s", line)
continue
try:
if realimaginary:
values = line.split(maxsplit=5)
freq = values[0]
re11 = values[1]
im11 = values[2]
freq = int(float(freq) * factor)
re11 = float(re11)
im11 = float(im11)
self.s11data.append(Datapoint(freq, re11, im11))
if len(values) > 3:
re21 = values[3]
im21 = values[4]
re21 = float(re21)
im21 = float(im21)
self.s21data.append(Datapoint(freq, re21, im21))
elif magnitudeangle:
values = line.split(maxsplit=5)
freq = values[0]
mag11 = float(values[1])
angle11 = float(values[2])
freq = int(float(freq) * factor)
re11 = float(mag11) * math.cos(math.radians(angle11))
im11 = float(mag11) * math.sin(math.radians(angle11))
self.s11data.append(Datapoint(freq, re11, im11))
if len(values) > 3:
mag21 = float(values[3])
angle21 = float(values[4])
re21 = float(mag21) * math.cos(math.radians(angle21))
im21 = float(mag21) * math.sin(math.radians(angle21))
self.s21data.append(Datapoint(freq, re21, im21))
continue
except ValueError as e:
logger.exception("Failed to parse line: %s (%s)", line, e)
file.close()
with open(self.filename) as infile:
self.loads(infile.read())
except TypeError as e:
logger.exception("Failed to parse %s: %s", self.filename, e)
except IOError as e:
logger.exception("Failed to open %s: %s", self.filename, e)
return
def setFilename(self, filename):
def loads(self, s: str):
"""Parse touchstone 1.1 string input
appends to existing sdata if Touchstone object exists
"""
with io.StringIO(s) as file:
opts_line = self._parse_comments(file)
self.opts.parse(opts_line)
prev_freq = 0.0
prev_len = 0
for line in file:
# ignore empty lines (even if not specified)
if not line.strip():
continue
# ignore comments at data end
data = line.split('!')[0]
data = data.split()
freq, data = float(data[0]) * self.opts.factor, data[1:]
data_len = len(data)
# consistency checks
if freq <= prev_freq:
raise TypeError("Frequency not ascending: " + line)
prev_freq = freq
if prev_len == 0:
prev_len = data_len
if data_len % 2:
raise TypeError("Data values aren't pairs: " + line)
elif data_len != prev_len:
raise TypeError("Inconsistent number of pairs: " + line)
self._append_line_data(freq, data)
def setFilename(self, filename: str):
self.filename = filename

Wyświetl plik

@ -14,5 +14,5 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
version = '0.2.0'
version = '0.2.1alpha'
debug = False

16
test/__init__.py 100644
Wyświetl plik

@ -0,0 +1,16 @@
# 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/>.

Wyświetl plik

@ -0,0 +1,9 @@
# Hz S RI R 50
140000000 -0.720544874 -0.074467673
140307234 -0.707615315 -0.045678697
140614468 -0.694235622 -0.017205553
140921702 -0.679476678 0.011064857
141228936 0.037949264
141536169 -0.645231842 0.06495472
141843404 -0.625548779 0.090901531
142150638 -0.605278372 0.116493001

Wyświetl plik

@ -0,0 +1,16 @@
! Vector Network Analyzer VNA R2
! Tucson Amateur Packet Radio
! Saturday, 9 November, 2019 17:48:47
! Frequency S11 S21 S12 S22
! ListType=Lin
# HZ S RI R 50
000500000 -3.33238E-001 1.80018E-004 6.74780E-001 -8.19510E-007 6.75290E-001 -8.20129E-007 -3.33238E-001 3.08078E-004
001382728 -3.33017E-001 6.89580E-004 6.74251E-001 -3.70855E-004 6.74761E-001 -5.04361E-004 -3.33016E-001 9.45694E-004
002265456 -3.33136E-001 1.06095E-003 6.74766E-001 -1.00228E-003 6.75276E-001 -1.00304E-003 -3.33136E-001 1.06095E-003
003148184 -3.33120E-001 1.97467E-003 6.74773E-001 -1.65230E-003 6.74773E-001 -1.65230E-003 -3.33121E-001 1.91064E-003
004030912 -3.32847E-001 2.45743E-003 6.74777E-001 -2.28839E-003 6.75288E-001 -2.15679E-003
004913640 -3.32746E-001 2.93382E-003 6.75260E-001 -2.94645E-003 6.75261E-001 -2.81312E-003 -3.32990E-001 3.06364E-003
005796368 -3.33479E-001 3.06528E-003 6.75798E-001 -2.32365E-003 6.76309E-001 -2.32540E-003 -3.33479E-001 3.06528E-003
006679097 -3.32609E-001 3.80377E-003 6.74764E-001 -4.08250E-003 6.74764E-001 -4.08250E-003 -3.32854E-001 3.80608E-003
007561825 -3.32448E-001 4.35906E-003 6.75247E-001 -4.96650E-003 6.75249E-001 -4.69986E-003 -3.32692E-001 4.36169E-003
008444553 -3.32510E-001 4.94361E-003 6.74737E-001 -5.33508E-003 6.75248E-001 -5.20579E-003 -3.32508E-001 5.13540E-003

Wyświetl plik

@ -0,0 +1,8 @@
# Hz S RI R 50
140000000 -0.720544874 -0.074467673
140307234 -0.707615315 -0.045678697
140921702 -0.679476678 0.011064857
140614468 -0.694235622 -0.017205553
141536169 -0.645231842 0.06495472
141843404 -0.625548779 0.090901531
142150638 -0.605278372 0.116493001

1011
test/data/valid.s1p 100644

Plik diff jest za duży Load Diff

1027
test/data/valid.s2p 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -0,0 +1,12 @@
# Hz S RI R 50
140000000 -0.720544874 -0.074467673
140307234 -0.707615315 -0.045678697
140614468 -0.694235622 -0.017205553
140921702 -0.679476678 0.011064857
141228936 -0.662805676 0.037949264
141536169 -0.645231842 0.06495472 ! just a test comment
141843404 -0.625548779 0.090901531
142150638 -0.605278372 0.116493001
142457872 -0.583680212 0.140287563
142765106 -0.560637235 0.16401714
143072339 -0.536502182 0.186390563

Wyświetl plik

@ -0,0 +1,87 @@
# 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/>.
import sys
import unittest
# Import targets to be tested
from NanoVNASaver import RFTools
rft = RFTools.RFTools()
class TestCases(unittest.TestCase):
def test_basicIntegerValues(self):
# simple well-formed integers with no trailing zeros. Most importantly
# there is no loss of accuracy in the result. Returned values are not
# truncated if result would lose meaningful data.
'''
Original Behavior:
self.assertEqual(rft.formatSweepFrequency(1), '1Hz')
self.assertEqual(rft.formatSweepFrequency(12), '12Hz')
self.assertEqual(rft.formatSweepFrequency(123), '123Hz')
self.assertEqual(rft.formatSweepFrequency(1234), '1.234kHz')
self.assertEqual(rft.formatSweepFrequency(12345), '12.345kHz')
self.assertEqual(rft.formatSweepFrequency(123456), '123.456kHz')
self.assertEqual(rft.formatSweepFrequency(1234567), '1.234567MHz')
self.assertEqual(rft.formatSweepFrequency(12345678), '12.345678MHz')
self.assertEqual(rft.formatSweepFrequency(123456789), '123.456789MHz')
'''
# New Behavior: results in loss of accuracy again.
self.assertEqual(rft.formatSweepFrequency(1), '1.0000Hz')
self.assertEqual(rft.formatSweepFrequency(12), '12.000Hz')
self.assertEqual(rft.formatSweepFrequency(123), '123.00Hz')
self.assertEqual(rft.formatSweepFrequency(1234), '1.2340kHz')
self.assertEqual(rft.formatSweepFrequency(12345), '12.345kHz')
self.assertEqual(rft.formatSweepFrequency(123456), '123.46kHz')
self.assertEqual(rft.formatSweepFrequency(1234567), '1.2346MHz')
self.assertEqual(rft.formatSweepFrequency(12345678), '12.346MHz')
self.assertEqual(rft.formatSweepFrequency(123456789), '123.46MHz')
'''
def test_defaultMinDigits(self):
# simple integers with trailing zeros.
# DEFAULT behavior retains 2 digits after the period, mindigits=2.
self.assertEqual(rft.formatSweepFrequency(1000), '1.00kHz')
self.assertEqual(rft.formatSweepFrequency(10000), '10.00kHz')
self.assertEqual(rft.formatSweepFrequency(100000), '100.00kHz')
self.assertEqual(rft.formatSweepFrequency(1000000), '1.00MHz')
def test_nonDefaultMinDigits(self):
# simple integers with trailing zeros. setting mindigit value to something
# other than default, where trailing zeros >= mindigits, the number of
# zeros shown is equal to mindigits value.
self.assertEqual(rft.formatSweepFrequency(1000000, mindigits=0), '1MHz')
self.assertEqual(rft.formatSweepFrequency(1000000, mindigits=1), '1.0MHz')
self.assertEqual(rft.formatSweepFrequency(1000000, mindigits=3), '1.000MHz')
self.assertEqual(rft.formatSweepFrequency(10000000, mindigits=4), '10.0000MHz')
self.assertEqual(rft.formatSweepFrequency(100000000, mindigits=5), '100.00000MHz')
self.assertEqual(rft.formatSweepFrequency(1000000000, mindigits=6), '1.000000GHz')
# where trailing zeros < mindigits, only available zeros are shown, if the
# result includes no decimal places (i.e. Hz values).
self.assertEqual(rft.formatSweepFrequency(1, mindigits=4), '1Hz')
self.assertEqual(rft.formatSweepFrequency(10, mindigits=4), '10Hz')
self.assertEqual(rft.formatSweepFrequency(100, mindigits=4), '100Hz')
# but where a decimal exists, and mindigits > number of available zeroes,
# this results in extra zeroes being padded into result, even into sub-Hz
# resolution. This is not useful for this application.
# TODO: Consider post-processing result for maxdigits based on SI unit.
self.assertEqual(rft.formatSweepFrequency(1000, mindigits=5), '1.00000kHz')
self.assertEqual(rft.formatSweepFrequency(1000, mindigits=10), '1.0000000000kHz')
'''
if __name__ == '__main__':
unittest.main(verbosity=2)

Wyświetl plik

@ -0,0 +1,145 @@
# 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/>.
import sys
import unittest
# Import targets to be tested
from NanoVNASaver import RFTools
rft = RFTools.RFTools()
class TestCases(unittest.TestCase):
def test_basicSIUnits(self):
# simple well-formed integers with correct SI units
self.assertEqual(rft.parseFrequency('123Hz'), 123)
self.assertEqual(rft.parseFrequency('123456Hz'), 123456)
self.assertEqual(rft.parseFrequency('123kHz'), 123000)
self.assertEqual(rft.parseFrequency('123456kHz'), 123456000)
self.assertEqual(rft.parseFrequency('123MHz'), 123000000)
self.assertEqual(rft.parseFrequency('123456MHz'), 123456000000)
self.assertEqual(rft.parseFrequency('123GHz'), 123000000000)
self.assertEqual(rft.parseFrequency('123456GHz'), 123456000000000)
def test_commonMistakeKHz_vs_kHz(self):
# some poorly formatted values that still work as expected
self.assertEqual(rft.parseFrequency('123kHz'), 123000)
self.assertEqual(rft.parseFrequency('123KHz'), 123000)
def test_illegalInputValues(self):
# poorly formatted inputs that are identified as illegal
self.assertEqual(rft.parseFrequency('Junk'), -1)
self.assertEqual(rft.parseFrequency('Garbage'), -1)
self.assertEqual(rft.parseFrequency('123.Junk'), -1)
def test_missingDigitsAfterPeriod(self):
# some poorly formatted values that still work as expected
self.assertEqual(rft.parseFrequency('123.'), 123)
self.assertEqual(rft.parseFrequency('123.Hz'), 123)
self.assertEqual(rft.parseFrequency('123.kHz'), 123000)
self.assertEqual(rft.parseFrequency('123.MHz'), 123000000)
self.assertEqual(rft.parseFrequency('123.GHz'), 123000000000)
def test_unusualSIUnits(self):
#######################################################################
# Current behavior: unusual SI values that are legal, but inappropriate
# for this application provide unexpected outputs. This behavior is
# based on the FULL set of SI prefixes defined in SITools (below).
#PREFIXES = ("y", "z", "a", "f", "p", "n", "µ", "m",
# "", "k", "M", "G", "T", "P", "E", "Z", "Y")
#######################################################################
self.assertEqual(rft.parseFrequency('123EHz'), 123000000000000000000)
self.assertEqual(rft.parseFrequency('123PHz'), 123000000000000000)
self.assertEqual(rft.parseFrequency('123THz'), 123000000000000)
self.assertEqual(rft.parseFrequency('123YHz'), 122999999999999998473273344)
self.assertEqual(rft.parseFrequency('123ZHz'), 123000000000000002097152)
self.assertEqual(rft.parseFrequency('123aHz'), 0)
self.assertEqual(rft.parseFrequency('123fHz'), 0)
self.assertEqual(rft.parseFrequency('123mHz'), 0)
self.assertEqual(rft.parseFrequency('123nHz'), 0)
self.assertEqual(rft.parseFrequency('123pHz'), 0)
self.assertEqual(rft.parseFrequency('123yHz'), 0)
self.assertEqual(rft.parseFrequency('123zHz'), 0)
#######################################################################
# Recommend: Reducing the legal SI values defined in SITools (see
# below). This makes it more likely that typos will result in a -1
# failure code instead of being interpreted as an SI unit.
# PREFIXES = ("", "k", "M", "G")
#######################################################################
'''
self.assertEqual(rft.parseFrequency('123EHz'), -1)
self.assertEqual(rft.parseFrequency('123PHz'), -1)
self.assertEqual(rft.parseFrequency('123THz'), -1)
self.assertEqual(rft.parseFrequency('123YHz'), -1)
self.assertEqual(rft.parseFrequency('123ZHz'), -1)
self.assertEqual(rft.parseFrequency('123aHz'), -1)
self.assertEqual(rft.parseFrequency('123fHz'), -1)
self.assertEqual(rft.parseFrequency('123mHz'), -1)
self.assertEqual(rft.parseFrequency('123nHz'), -1)
self.assertEqual(rft.parseFrequency('123pHz'), -1)
self.assertEqual(rft.parseFrequency('123yHz'), -1)
self.assertEqual(rft.parseFrequency('123zHz'), -1)
'''
def test_partialHzText(self):
#######################################################################
# The current behavior for accidentally missing the H in Hz, is a
# detection of 'z' SI unit (zepto = 10^-21), which then rounded to 0.
# After reduction of legal SI values in SITools, this would return
# a -1 failure code instead.
#######################################################################
self.assertEqual(rft.parseFrequency('123z'), 0)
self.assertEqual(rft.parseFrequency('123.z'), 0)
self.assertEqual(rft.parseFrequency('1.23z'), 0)
'''
self.assertEqual(rft.parseFrequency('123z'), -1)
self.assertEqual(rft.parseFrequency('123.z'), -1)
self.assertEqual(rft.parseFrequency('1.23z'), -1)
'''
def test_basicExponentialNotation(self):
# check basic exponential notation
self.assertEqual(rft.parseFrequency('123e3'), 123000)
self.assertEqual(rft.parseFrequency('123e6'), 123000000)
self.assertEqual(rft.parseFrequency('123e9'), 123000000000)
self.assertEqual(rft.parseFrequency('123e4'), 1230000)
self.assertEqual(rft.parseFrequency('123e12'), 123000000000000)
self.assertEqual(rft.parseFrequency('123e18'), 123000000000000000000)
def test_negativeExponentialNotation(self):
# negative exponential values resulting in N < 0, return 0
self.assertEqual(rft.parseFrequency('123e-3'), 0)
self.assertEqual(rft.parseFrequency('1234e-4'), 0)
self.assertEqual(rft.parseFrequency('12345e-5'), 0)
self.assertEqual(rft.parseFrequency('12345678e-8'), 0)
# negative exponential values resulting in N > 0, return N
self.assertEqual(rft.parseFrequency('100000e-5'), 1)
self.assertEqual(rft.parseFrequency('100000e-4'), 10)
self.assertEqual(rft.parseFrequency('100000e-3'), 100)
self.assertEqual(rft.parseFrequency('100000e-2'), 1000)
self.assertEqual(rft.parseFrequency('100000e-1'), 10000)
def test_multiplePeriods(self):
# multiple periods are properly detected as bad
self.assertEqual(rft.parseFrequency('123..Hz'), -1)
self.assertEqual(rft.parseFrequency('123...Hz'), -1)
self.assertEqual(rft.parseFrequency('123....Hz'), -1)
self.assertEqual(rft.parseFrequency('1.23.Hz'), -1)
if __name__ == '__main__':
unittest.main(verbosity=2)

37
test_master.py 100644
Wyświetl plik

@ -0,0 +1,37 @@
# 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/>.
import sys
import unittest
###############################################################################
#
# Executing this file initiates the discovery and execution of all unittests
# in the "test" directory, with filenames starting with "test" that have a
# TestCases(unittest.TestCase) Class, which has class functions starting with
# "test_". This provides a simple test framework that is easily expandable by
# simply adding new "test_xxx.py" files into the test directory.
#
###############################################################################
if __name__ == '__main__':
loader = unittest.TestLoader()
tests = loader.discover('.')
testRunner = unittest.runner.TextTestRunner(
failfast=False,
verbosity=2)
testRunner.run(tests)