kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Merge branch 'upstream_Development'
commit
d22bd74316
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/>.
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
Ładowanie…
Reference in New Issue