kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
- First attempt at Analysis framework
rodzic
0786bf64c6
commit
7fc5db8042
|
@ -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)")
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue