- First attempt at Analysis framework

pull/36/head
Rune Broberg 2019-10-04 12:51:20 +02:00
rodzic 0786bf64c6
commit 7fc5db8042
5 zmienionych plików z 271 dodań i 4 usunięć

Wyświetl plik

@ -0,0 +1,177 @@
# 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 logging
import math
from PyQt5 import QtWidgets
logger = logging.getLogger(__name__)
class Analysis:
_widget = None
def __init__(self, app):
from NanoVNASaver.NanoVNASaver import NanoVNASaver
self.app: NanoVNASaver = app
def widget(self) -> QtWidgets.QWidget:
return self._widget
def runAnalysis(self):
pass
def reset(self):
pass
class LowPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Please place " + self.app.markers[0].name + " in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.cutoff_label = QtWidgets.QLabel()
self.six_db_label = QtWidgets.QLabel()
self.sixty_db_label = QtWidgets.QLabel()
self.db_per_octave_label = QtWidgets.QLabel()
self.db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow("Cutoff frequency:", self.cutoff_label)
layout.addRow("-6 dB point:", self.six_db_label)
layout.addRow("-60 dB point:", self.sixty_db_label)
layout.addRow("Roll-off:", self.db_per_octave_label)
layout.addRow("Roll-off:", self.db_per_decade_label)
def reset(self):
self.result_label.clear()
self.cutoff_label.clear()
self.six_db_label.clear()
self.sixty_db_label.clear()
self.db_per_octave_label.clear()
self.db_per_decade_label.clear()
def runAnalysis(self):
from NanoVNASaver.NanoVNASaver import NanoVNASaver
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if pass_band_location < 0:
logger.debug("No location for %s", self.app.markers[0].name)
return
if len(self.app.data21) == 0:
logger.debug("No data to analyse")
return
pass_band_db = NanoVNASaver.gain(self.app.data21[pass_band_location])
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 = NanoVNASaver.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
break
if initial_cutoff_location < 0:
self.result_label.setText("Cutoff location not found.")
return
initial_cutoff_frequency = self.app.data21[initial_cutoff_location].freq
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = NanoVNASaver.gain(self.app.data21[initial_cutoff_location])
for i in range(0, initial_cutoff_location):
db = NanoVNASaver.gain(self.app.data21[i])
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data[peak_location].freq)
self.app.markers[0].setFrequency(str(self.app.data21[peak_location].freq))
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data21)):
db = NanoVNASaver.gain(self.app.data21[i])
if (pass_band_db - db) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data21[cutoff_location].freq
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
self.cutoff_label.setText(NanoVNASaver.formatFrequency(cutoff_frequency))
self.app.markers[1].setFrequency(str(cutoff_frequency))
six_db_location = -1
for i in range(cutoff_location, len(self.app.data21)):
db = NanoVNASaver.gain(self.app.data21[i])
if (pass_band_db - db) > 6:
# We found 6dB location
six_db_location = i
break
if six_db_location < 0:
self.result_label.setText("6 dB location not found.")
return
six_db_cutoff_frequency = self.app.data21[six_db_location].freq
self.six_db_label.setText(NanoVNASaver.formatFrequency(six_db_cutoff_frequency))
six_db_attenuation = NanoVNASaver.gain(self.app.data21[six_db_location])
max_attenuation = NanoVNASaver.gain(self.app.data21[len(self.app.data21) - 1])
frequency_factor = self.app.data21[len(self.app.data21) - 1].freq / six_db_cutoff_frequency
attenuation = (max_attenuation - six_db_attenuation)
logger.debug("Measured points: %d Hz and %d Hz", six_db_cutoff_frequency, self.app.data21[len(self.app.data21) - 1].freq)
logger.debug("%d dB over %f factor", attenuation, frequency_factor)
octave_attenuation = attenuation / (math.log10(frequency_factor) / math.log10(2))
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
decade_attenuation = attenuation / math.log10(frequency_factor)
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
sixty_db_location = -1
for i in range(six_db_location, len(self.app.data21)):
db = NanoVNASaver.gain(self.app.data21[i])
if (pass_band_db - db) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location < 0:
# # We derive 60 dB instead
# factor = 10 * (-54 / decade_attenuation)
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
self.sixty_db_label.setText("Not calculated")
else:
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)")

Wyświetl plik

@ -446,6 +446,7 @@ class CalibrationWindow(QtWidgets.QWidget):
self.app.calibration.throughLength = float(self.through_length.text())/10**12
self.app.calibration.useIdealThrough = False
logger.debug("Attempting calibration calculation.")
if self.app.calibration.calculateCorrections():
self.calibration_status_label.setText("Application calibration (" + str(len(self.app.calibration.s11short)) + " points)")
@ -722,6 +723,7 @@ class Calibration:
def calculateCorrections(self):
if not self.isValid1Port():
logger.warning("Tried to calibrate from insufficient data.")
return False
self.frequencies = [int] * len(self.s11short)
self.e00 = [np.complex] * len(self.s11short)
@ -729,6 +731,15 @@ class Calibration:
self.deltaE = [np.complex] * len(self.s11short)
self.e30 = [np.complex] * len(self.s11short)
self.e10e32 = [np.complex] * len(self.s11short)
logger.debug("Calculating calibration for %d points.", len(self.s11short))
if self.useIdealShort:
logger.debug("Using ideal values.")
else:
logger.debug("Using calibration set values.")
if self.isValid2Port():
logger.debug("Calculating 2-port calibration.")
else:
logger.debug("Calculating 1-port calibration.")
for i in range(len(self.s11short)):
self.frequencies[i] = self.s11short[i].freq
f = self.s11short[i].freq
@ -776,7 +787,7 @@ class Calibration:
except ZeroDivisionError:
self.isCalculated = False
logger.error("Division error - did you use the same measurement for two of short, open and load?")
return
return self.isCalculated
if self.isValid2Port():
self.e30[i] = np.complex(self.s21isolation[i].re, self.s21isolation[i].im)
@ -787,6 +798,7 @@ class Calibration:
self.e10e32[i] = (s21m - self.e30[i]) * (1 - (self.e11[i]*self.e11[i]))
self.isCalculated = True
logger.debug("Calibration correctly calculated.")
return self.isCalculated
def correct11(self, re, im, freq):

Wyświetl plik

@ -139,12 +139,31 @@ class Marker(QtCore.QObject):
# Set the frequency before loading any data
return
stepsize = data[1].freq-data[0].freq
min_freq = data[0].freq
max_freq = data[len(data)-1].freq
stepsize = data[1].freq - data[0].freq
if self.frequency + stepsize/2 < min_freq or self.frequency - stepsize/2 > max_freq:
return
for i in range(len(data)):
if abs(data[i].freq-self.frequency) <= (stepsize/2):
if abs(data[i].freq - self.frequency) <= (stepsize/2):
self.location = i
return
# No position found, but we are within the span
min_distance = max_freq
for i in range(len(data)):
if abs(data[i].freq - self.frequency) < min_distance:
min_distance = abs(data[i].freq - self.frequency)
else:
# We have now started moving away from the nearest point
self.location = i-1
return
# If we still didn't find a best spot, it was the last value
self.location = len(data)-1
return
def getGroupBox(self):
return self.group_box

Wyświetl plik

@ -34,6 +34,7 @@ from .Calibration import CalibrationWindow, Calibration
from .Marker import Marker
from .SweepWorker import SweepWorker
from .Touchstone import Touchstone
from .Analysis import Analysis, LowPassAnalysis
from .about import version as ver
Datapoint = collections.namedtuple('Datapoint', 'freq re im')
@ -312,6 +313,12 @@ class NanoVNASaver(QtWidgets.QWidget):
marker_column.addSpacerItem(QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding))
self.analysis_window = AnalysisWindow(self)
btn_show_analysis = QtWidgets.QPushButton("Analysis ...")
btn_show_analysis.clicked.connect(self.displayAnalysisWindow)
marker_column.addWidget(btn_show_analysis)
################################################################################################################
# TDR
################################################################################################################
@ -1050,6 +1057,10 @@ class NanoVNASaver(QtWidgets.QWidget):
self.tdr_window.show()
QtWidgets.QApplication.setActiveWindow(self.tdr_window)
def displayAnalysisWindow(self):
self.analysis_window.show()
QtWidgets.QApplication.setActiveWindow(self.analysis_window)
def showError(self, text):
error_message = QtWidgets.QErrorMessage(self)
error_message.showMessage(text)
@ -1641,6 +1652,7 @@ class SweepSettingsWindow(QtWidgets.QWidget):
if self.band_pad_limits.isChecked():
span = stop - start
start -= round(span / 10)
start = max(1, start)
stop += round(span / 10)
self.app.sweepStartInput.setText(str(start))
@ -1823,3 +1835,48 @@ class BandsModel(QtCore.QAbstractTableModel):
def setColor(self, color):
self.color = color
class AnalysisWindow(QtWidgets.QWidget):
analyses = []
analysis: Analysis = None
def __init__(self, app):
super().__init__()
self.app: NanoVNASaver = app
self.setWindowTitle("Sweep analysis")
self.setWindowIcon(self.app.icon)
self.setMinimumSize(400, 300)
shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
select_analysis_box = QtWidgets.QGroupBox("Select analysis")
select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box)
analysis_list = QtWidgets.QComboBox()
analysis_list.addItem("Low-pass filter")
analysis_list.addItem("Band-pass filter")
analysis_list.addItem("High-pass filter")
select_analysis_layout.addRow("Analysis type", analysis_list)
btn_run_analysis = QtWidgets.QPushButton("Run analysis")
btn_run_analysis.clicked.connect(self.runAnalysis)
select_analysis_layout.addRow(btn_run_analysis)
analysis_box = QtWidgets.QGroupBox("Analysis")
analysis_layout = QtWidgets.QVBoxLayout(analysis_box)
#### TEMPORARY ####
self.analysis = LowPassAnalysis(app)
analysis_layout.addWidget(self.analysis.widget())
layout.addWidget(select_analysis_box)
layout.addWidget(analysis_box)
def runAnalysis(self):
if self.analysis is not None:
self.analysis.runAnalysis()

Wyświetl plik

@ -271,7 +271,9 @@ class SweepWorker(QtCore.QRunnable):
def readSegment(self, start, stop):
logger.debug("Setting sweep range to %d to %d", start, stop)
self.app.setSweep(start, stop)
sleep(0.3)
sleep(1) # TODO This long delay seems to fix the weird data transitions we were seeing by getting partial
# sweeps. Clearly something needs to be done, maybe at firmware level, to address this fully.
# Let's check the frequencies first:
frequencies = self.readFreq()
# TODO: Set up checks for getting the right frequencies. Challenge: We don't set frequency to single-Hz