From 2052718822215d86bb6d8d547fecf8252682c107 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Sat, 19 Dec 2020 13:38:53 +0100 Subject: [PATCH 01/26] Refactoring --- NanoVNASaver/Analysis/Analysis.py | 41 +++++++++++++++++++++++++-- NanoVNASaver/Analysis/VSWRAnalysis.py | 32 ++++++--------------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/NanoVNASaver/Analysis/Analysis.py b/NanoVNASaver/Analysis/Analysis.py index cf0fdd15..2292955d 100644 --- a/NanoVNASaver/Analysis/Analysis.py +++ b/NanoVNASaver/Analysis/Analysis.py @@ -27,6 +27,41 @@ class Analysis: _widget = None + @classmethod + def find_minimums(cls, data, threshold): + ''' + + Find values above threshold + return list of tuples (start, lowest, end) + indicating the index of data list + + + :param cls: + :param data: list of values + :param threshold: + ''' + + minimums = [] + min_start = -1 + min_idx = -1 + + min_val = threshold + for i, d in enumerate(data): + if d < threshold and i < len(data) - 1: + if d < min_val: + min_val = d + min_idx = i + if min_start == -1: + min_start = i + elif min_start != -1: + # We are above the threshold, and were in a section that was + # below + minimums.append((min_start, min_idx, i - 1)) + min_start = -1 + min_idx = -1 + min_val = threshold + return minimums + def __init__(self, app: QtWidgets.QWidget): self.app = app @@ -50,8 +85,10 @@ def calculateRolloff(self, location1, location2): if frequency_factor < 1: frequency_factor = 1 / frequency_factor attenuation = abs(gain1 - gain2) - logger.debug("Measured points: %d Hz and %d Hz", frequency1, frequency2) + logger.debug("Measured points: %d Hz and %d Hz", + frequency1, frequency2) logger.debug("%f dB over %f factor", attenuation, frequency_factor) - octave_attenuation = attenuation / (math.log10(frequency_factor) / math.log10(2)) + octave_attenuation = attenuation / \ + (math.log10(frequency_factor) / math.log10(2)) decade_attenuation = attenuation / math.log10(frequency_factor) return octave_attenuation, decade_attenuation diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index decfdeb9..4e842cfe 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -30,7 +30,7 @@ class VSWRAnalysis(Analysis): max_dips_shown = 3 vswr_limit_value = 1.5 - + class QHLine(QtWidgets.QFrame): def __init__(self): super().__init__() @@ -73,31 +73,17 @@ def runAnalysis(self): # self.app.markers[0].setFrequency(str(self.app.data11[min_idx].freq)) # self.app.markers[0].frequencyInput.setText(str(self.app.data11[min_idx].freq)) - minimums = [] - min_start = -1 - min_idx = -1 threshold = self.input_vswr_limit.value() - min_val = threshold - for i, d in enumerate(data): - if d < threshold and i < len(data)-1: - if d < min_val: - min_val = d - min_idx = i - if min_start == -1: - min_start = i - elif min_start != -1: - # We are above the threshold, and were in a section that was below - minimums.append((min_start, min_idx, i-1)) - min_start = -1 - min_idx = -1 - min_val = threshold - - logger.debug("Found %d sections under %f threshold", len(minimums), threshold) + minimums = self.find_minimums(data, threshold) + + logger.debug("Found %d sections under %f threshold", + len(minimums), threshold) results_header = self.layout.indexOf(self.results_label) - logger.debug("Results start at %d, out of %d", results_header, self.layout.rowCount()) + logger.debug("Results start at %d, out of %d", + results_header, self.layout.rowCount()) for i in range(results_header, self.layout.rowCount()): - self.layout.removeRow(self.layout.rowCount()-1) + self.layout.removeRow(self.layout.rowCount() - 1) if len(minimums) > max_dips_shown: self.layout.addRow(QtWidgets.QLabel("More than " + str(max_dips_shown) + @@ -141,7 +127,7 @@ def runAnalysis(self): format_frequency(self.app.data11[lowest].freq))) self.layout.addWidget(PeakSearchAnalysis.QHLine()) # Remove the final separator line - self.layout.removeRow(self.layout.rowCount()-1) + self.layout.removeRow(self.layout.rowCount() - 1) else: self.layout.addRow(QtWidgets.QLabel( "No areas found with VSWR below " + str(round(threshold, 2)) + ".")) From d06bb320823d3bb63b90edb9c8b95dca7d224dbf Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Sun, 20 Dec 2020 15:41:38 +0100 Subject: [PATCH 02/26] some fix ?!? --- NanoVNASaver/Analysis/PeakSearchAnalysis.py | 25 +++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/NanoVNASaver/Analysis/PeakSearchAnalysis.py b/NanoVNASaver/Analysis/PeakSearchAnalysis.py index c72465bc..1b78b388 100644 --- a/NanoVNASaver/Analysis/PeakSearchAnalysis.py +++ b/NanoVNASaver/Analysis/PeakSearchAnalysis.py @@ -87,23 +87,34 @@ def __init__(self, app): outer_layout.addRow(QtWidgets.QLabel("Results")) def runAnalysis(self): + self.reset() + data = [] count = self.input_number_of_peaks.value() if self.rbtn_data_vswr.isChecked(): - data = [] + for d in self.app.data11: - data11.append(d.vswr) + data.append(d.vswr) elif self.rbtn_data_s21_gain.isChecked(): - data = [] for d in self.app.data21: data.append(d.gain) + elif self.rbtn_data_resistance.isChecked(): + for d in self.app.data11: + data.append(d.impedance().real) + elif self.rbtn_data_reactance.isChecked(): + for d in self.app.data11: + data.append(d.impedance().imag) + else: logger.warning("Searching for peaks on unknown data") return if self.rbtn_peak_positive.isChecked(): - peaks, _ = signal.find_peaks(data, width=3, distance=3, prominence=1) + peaks, _ = signal.find_peaks( + data, width=3, distance=3, prominence=1) elif self.rbtn_peak_negative.isChecked(): - peaks, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1) + data = [x * -1 for x in data] + peaks, _ = signal.find_peaks( + data, width=3, distance=3, prominence=1) # elif self.rbtn_peak_both.isChecked(): # peaks_max, _ = signal.find_peaks(data, width=3, distance=3, prominence=1) # peaks_min, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1) @@ -117,8 +128,8 @@ def runAnalysis(self): # Having found the peaks, get the prominence data - for p in peaks: - logger.debug("Peak at %d", p) + for i, p in np.ndenumerate(peaks): + logger.debug("Peak %i at %d", i, p) prominences = signal.peak_prominences(data, peaks)[0] logger.debug("%d prominences", len(prominences)) From e95fea4cccbd31a187301003d60d9a5e531c0991 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Sun, 20 Dec 2020 15:42:57 +0100 Subject: [PATCH 03/26] add some analysis --- NanoVNASaver/Analysis/Analysis.py | 55 ++++++- NanoVNASaver/Analysis/VSWRAnalysis.py | 202 +++++++++++++++++++++++++ NanoVNASaver/RFTools.py | 3 +- NanoVNASaver/Windows/AnalysisWindow.py | 34 +++-- 4 files changed, 283 insertions(+), 11 deletions(-) diff --git a/NanoVNASaver/Analysis/Analysis.py b/NanoVNASaver/Analysis/Analysis.py index 2292955d..21fa2fc6 100644 --- a/NanoVNASaver/Analysis/Analysis.py +++ b/NanoVNASaver/Analysis/Analysis.py @@ -18,7 +18,8 @@ # along with this program. If not, see . import logging import math - +import numpy as np +from scipy.signal import argrelextrema from PyQt5 import QtWidgets logger = logging.getLogger(__name__) @@ -27,6 +28,42 @@ class Analysis: _widget = None + @classmethod + def find_crossing_zero(cls, data, threshold=0): + ''' + + Find values crossing zero + return list of tuples (before, crossing, after) + indicating the index of data list + crossing is where data == 0 + or data nearest 0 + + at maximum 1 value == 0 + data must not start or end with 0 + + + :param cls: + :param data: list of values + :param threshold: unused, for future manage flipping around 0 + ''' + my_data = np.array(data) + zeroes = np.where(my_data == 0)[0] + + if 0 in zeroes: + raise ValueError("Data must non start with 0") + + if len(data) - 1 in zeroes: + raise ValueError("Data must non end with 0") + crossing = [(n - 1, n, n + 1) for n in zeroes] + + for n in np.where((my_data[:-1] * my_data[1:]) < 0)[0]: + if abs(data[n]) <= abs(data[n + 1]): + crossing.append((n, n, n + 1)) + else: + crossing.append((n, n + 1, n + 1)) + + return crossing + @classmethod def find_minimums(cls, data, threshold): ''' @@ -62,6 +99,22 @@ def find_minimums(cls, data, threshold): min_val = threshold return minimums + @classmethod + def find_maximums(cls, data, threshold=0): + ''' + + Find peacs + + + :param cls: + :param data: list of values + :param threshold: + ''' + my_data = np.array(data) + maximums = argrelextrema(my_data, np.greater)[0] + + return maximums + def __init__(self, app: QtWidgets.QWidget): self.app = app diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index 4e842cfe..adbb48c5 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -23,6 +23,10 @@ from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis from NanoVNASaver.Formatting import format_frequency +from NanoVNASaver.Formatting import format_complex_imp +from NanoVNASaver.RFTools import reflection_coefficient +import os +import csv logger = logging.getLogger(__name__) @@ -61,6 +65,7 @@ def __init__(self, app): def runAnalysis(self): max_dips_shown = self.max_dips_shown data = [] + for d in self.app.data11: data.append(d.vswr) # min_idx = np.argmin(data) @@ -131,3 +136,200 @@ def runAnalysis(self): else: self.layout.addRow(QtWidgets.QLabel( "No areas found with VSWR below " + str(round(threshold, 2)) + ".")) + + +class ResonanceAnalysis(Analysis): + # max_dips_shown = 3 + + @classmethod + def vswr_transformed(cls, z, ratio=49) -> float: + refl = reflection_coefficient(z / ratio) + mag = abs(refl) + if mag == 1: + return 1 + return (1 + mag) / (1 - mag) + + class QHLine(QtWidgets.QFrame): + def __init__(self): + super().__init__() + self.setFrameShape(QtWidgets.QFrame.HLine) + + def __init__(self, app): + super().__init__(app) + + self._widget = QtWidgets.QWidget() + self.layout = QtWidgets.QFormLayout() + self._widget.setLayout(self.layout) + self.input_description = QtWidgets.QLineEdit("") + self.checkbox_move_marker = QtWidgets.QCheckBox() + self.layout.addRow(QtWidgets.QLabel("Settings")) + self.layout.addRow("Description", self.input_description) + self.layout.addRow(VSWRAnalysis.QHLine()) + + self.layout.addRow(VSWRAnalysis.QHLine()) + + self.results_label = QtWidgets.QLabel("Results") + self.layout.addRow(self.results_label) + + def _get_data(self, index): + my_data = {"freq": self.app.data11[index].freq, + "s11": self.app.data11[index].z, + "lambda": self.app.data11[index].wavelength, + "impedance": self.app.data11[index].impedance(), + "vswr": self.app.data11[index].vswr, + } + my_data["vswr_49"] = self.vswr_transformed( + my_data["impedance"], 49) + my_data["vswr_4"] = self.vswr_transformed( + my_data["impedance"], 4) + my_data["r"] = my_data["impedance"].real + my_data["x"] = my_data["impedance"].imag + + return my_data + + def _get_crossing(self): + + data = [] + for d in self.app.data11: + data.append(d.phase) + + crossing = sorted(self.find_crossing_zero(data)) + return crossing + + def runAnalysis(self): + self.results_label = QtWidgets.QLabel("Results") + # max_dips_shown = self.max_dips_shown + description = self.input_description.text() + if description: + filename = os.path.join("/tmp/", "{}.csv".format(description)) + else: + filename = None + + crossing = self._get_crossing() + + logger.debug("Found %d sections ", + len(crossing)) + + results_header = self.layout.indexOf(self.results_label) + logger.debug("Results start at %d, out of %d", + results_header, self.layout.rowCount()) + for i in range(results_header, self.layout.rowCount()): + self.layout.removeRow(self.layout.rowCount() - 1) + +# if len(crossing) > max_dips_shown: +# self.layout.addRow(QtWidgets.QLabel("More than " + str(max_dips_shown) + +# " dips found. Lowest shown.")) + +# self.crossing = crossing[:max_dips_shown] + extended_data = [] + if len(crossing) > 0: + + for m in crossing: + start, lowest, end = m + my_data = self._get_data(lowest) + + extended_data.append(my_data) + if start != end: + logger.debug( + "Section from %d to %d, lowest at %d", start, end, lowest) + + self.layout.addRow( + "Resonance", + QtWidgets.QLabel( + f"{format_frequency(self.app.data11[lowest].freq)}" + f" ({format_complex_imp(self.app.data11[lowest].impedance())})")) + else: + self.layout.addRow("Resonance", QtWidgets.QLabel( + format_frequency(self.app.data11[lowest].freq))) + self.layout.addWidget(PeakSearchAnalysis.QHLine()) + # Remove the final separator line + self.layout.removeRow(self.layout.rowCount() - 1) + if filename and extended_data: + + with open(filename, 'w', newline='') as csvfile: + fieldnames = extended_data[0].keys() + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for row in extended_data: + writer.writerow(row) + + else: + self.layout.addRow(QtWidgets.QLabel( + "No resonance found")) + + +class EFHWAnalysis(ResonanceAnalysis): + ''' + find only resonance when HI impedance + ''' + + def reset(self): + logger.debug("reset") + pass + + def runAnalysis(self): + self.results_label = QtWidgets.QLabel("Results") + # max_dips_shown = self.max_dips_shown + description = self.input_description.text() + if description: + filename = os.path.join("/tmp/", "{}.csv".format(description)) + else: + filename = None + + crossing = self._get_crossing() + + data = [] + for d in self.app.data11: + data.append(d.impedance()) + + maximums = sorted(self.find_maximums(data)) + + results_header = self.layout.indexOf(self.results_label) + logger.debug("Results start at %d, out of %d", + results_header, self.layout.rowCount()) + for i in range(results_header, self.layout.rowCount()): + self.layout.removeRow(self.layout.rowCount() - 1) + + extended_data = {} + + for m in crossing: + start, lowest, end = m + my_data = self._get_data(lowest) + + if lowest in extended_data: + extended_data[lowest].update(my_data) + else: + extended_data[lowest] = my_data + + logger.debug("maximumx %s of type %s", maximums, type(maximums)) + for m in maximums: + logger.debug("m %s of type %s", m, type(m)) + + my_data = self._get_data(m) + if m in extended_data: + extended_data[m].update(my_data) + else: + extended_data[m] = my_data + + for index in sorted(extended_data.keys()): + + self.layout.addRow( + "Resonance", + QtWidgets.QLabel( + f"{format_frequency(self.app.data11[index].freq)}" + f" ({format_complex_imp(self.app.data11[index].impedance())})")) + + # Remove the final separator line + self.layout.removeRow(self.layout.rowCount() - 1) + if filename and extended_data: + + with open(filename, 'w', newline='') as csvfile: + fieldnames = extended_data[sorted( + extended_data.keys())[0]].keys() + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for index in sorted(extended_data.keys()): + row = extended_data[index] + writer.writerow(row) diff --git a/NanoVNASaver/RFTools.py b/NanoVNASaver/RFTools.py index 3803bea0..f8923f70 100644 --- a/NanoVNASaver/RFTools.py +++ b/NanoVNASaver/RFTools.py @@ -39,6 +39,7 @@ class Datapoint(NamedTuple): @property def z(self) -> complex: """ return the datapoint impedance as complex number """ + # FIXME: not impedance, but s11 ? return complex(self.re, self.im) @property @@ -158,7 +159,7 @@ def corr_att_data(data: List[Datapoint], att: float) -> List[Datapoint]: if att <= 0: return data else: - att = 10**(att/20) + att = 10**(att / 20) ndata = [] for dp in data: corrected = dp.z * att diff --git a/NanoVNASaver/Windows/AnalysisWindow.py b/NanoVNASaver/Windows/AnalysisWindow.py index bac9206e..e6588ad7 100644 --- a/NanoVNASaver/Windows/AnalysisWindow.py +++ b/NanoVNASaver/Windows/AnalysisWindow.py @@ -23,6 +23,9 @@ from NanoVNASaver.Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, \ BandPassAnalysis, BandStopAnalysis, VSWRAnalysis, \ SimplePeakSearchAnalysis, MagLoopAnalysis +from NanoVNASaver.Analysis.VSWRAnalysis import ResonanceAnalysis +from NanoVNASaver.Analysis.VSWRAnalysis import EFHWAnalysis +from NanoVNASaver.Analysis import PeakSearchAnalysis logger = logging.getLogger(__name__) @@ -46,14 +49,24 @@ def __init__(self, app: QtWidgets.QWidget): select_analysis_box = QtWidgets.QGroupBox("Select analysis") select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box) self.analysis_list = QtWidgets.QComboBox() - self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app)) - self.analysis_list.addItem("Band-pass filter", BandPassAnalysis(self.app)) - self.analysis_list.addItem("High-pass filter", HighPassAnalysis(self.app)) - self.analysis_list.addItem("Band-stop filter", BandStopAnalysis(self.app)) + self.analysis_list.addItem( + "Low-pass filter", LowPassAnalysis(self.app)) + self.analysis_list.addItem( + "Band-pass filter", BandPassAnalysis(self.app)) + self.analysis_list.addItem( + "High-pass filter", HighPassAnalysis(self.app)) + self.analysis_list.addItem( + "Band-stop filter", BandStopAnalysis(self.app)) # self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app)) - self.analysis_list.addItem("Peak search", SimplePeakSearchAnalysis(self.app)) + self.analysis_list.addItem( + "Peak search", PeakSearchAnalysis(self.app)) self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app)) - self.analysis_list.addItem("MagLoop analysis", MagLoopAnalysis(self.app)) + self.analysis_list.addItem( + "Resonance analysis", ResonanceAnalysis(self.app)) + self.analysis_list.addItem( + "HWEF analysis", EFHWAnalysis(self.app)) + self.analysis_list.addItem( + "MagLoop analysis", MagLoopAnalysis(self.app)) select_analysis_layout.addRow("Analysis type", self.analysis_list) self.analysis_list.currentIndexChanged.connect(self.updateSelection) @@ -61,8 +74,10 @@ def __init__(self, app: QtWidgets.QWidget): btn_run_analysis.clicked.connect(self.runAnalysis) select_analysis_layout.addRow(btn_run_analysis) - self.checkbox_run_automatically = QtWidgets.QCheckBox("Run automatically") - self.checkbox_run_automatically.stateChanged.connect(self.toggleAutomaticRun) + self.checkbox_run_automatically = QtWidgets.QCheckBox( + "Run automatically") + self.checkbox_run_automatically.stateChanged.connect( + self.toggleAutomaticRun) select_analysis_layout.addRow(self.checkbox_run_automatically) analysis_box = QtWidgets.QGroupBox("Analysis") @@ -87,7 +102,8 @@ def updateSelection(self): old_item = self.analysis_layout.itemAt(0) if old_item is not None: old_widget = self.analysis_layout.itemAt(0).widget() - self.analysis_layout.replaceWidget(old_widget, self.analysis.widget()) + self.analysis_layout.replaceWidget( + old_widget, self.analysis.widget()) old_widget.hide() else: self.analysis_layout.addWidget(self.analysis.widget()) From db284fd57aa503733c19974e7726e6b7bf0b1486 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Sun, 20 Dec 2020 18:24:03 +0100 Subject: [PATCH 04/26] some tolerance --- NanoVNASaver/Analysis/Analysis.py | 10 ++-- NanoVNASaver/Analysis/VSWRAnalysis.py | 70 +++++++++++++++++++-------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/NanoVNASaver/Analysis/Analysis.py b/NanoVNASaver/Analysis/Analysis.py index 21fa2fc6..a3e4bfa6 100644 --- a/NanoVNASaver/Analysis/Analysis.py +++ b/NanoVNASaver/Analysis/Analysis.py @@ -21,6 +21,7 @@ import numpy as np from scipy.signal import argrelextrema from PyQt5 import QtWidgets +from scipy import signal logger = logging.getLogger(__name__) @@ -110,10 +111,13 @@ def find_maximums(cls, data, threshold=0): :param data: list of values :param threshold: ''' - my_data = np.array(data) - maximums = argrelextrema(my_data, np.greater)[0] + peaks, _ = signal.find_peaks( + data, width=2, distance=3, prominence=1) + +# my_data = np.array(data) +# maximums = argrelextrema(my_data, np.greater)[0] - return maximums + return peaks def __init__(self, app: QtWidgets.QWidget): self.app = app diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index adbb48c5..608c505d 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -197,7 +197,8 @@ def _get_crossing(self): return crossing def runAnalysis(self): - self.results_label = QtWidgets.QLabel("Results") + self.reset() + # self.results_label = QtWidgets.QLabel("Results") # max_dips_shown = self.max_dips_shown description = self.input_description.text() if description: @@ -269,7 +270,8 @@ def reset(self): pass def runAnalysis(self): - self.results_label = QtWidgets.QLabel("Results") + self.reset() + # self.results_label = QtWidgets.QLabel("Results") # max_dips_shown = self.max_dips_shown description = self.input_description.text() if description: @@ -281,7 +283,7 @@ def runAnalysis(self): data = [] for d in self.app.data11: - data.append(d.impedance()) + data.append(d.impedance().real) maximums = sorted(self.find_maximums(data)) @@ -293,24 +295,54 @@ def runAnalysis(self): extended_data = {} - for m in crossing: - start, lowest, end = m - my_data = self._get_data(lowest) + #both = np.intersect1d([i[1] for i in crossing], maximums) + both = [] + + tolerance = 2 + for i in maximums: + for l, _, h in crossing: + if l - tolerance <= i <= h + tolerance: + both.append(i) + continue + if l > i: + continue + + if both: + logger.info("%i crossing HW", len(both)) + logger.info(crossing) + logger.info(maximums) + logger.info(both) + for m in both: + my_data = self._get_data(m) + if m in extended_data: + extended_data[m].update(my_data) + else: + extended_data[m] = my_data + for i in range(min(len(both), len(self.app.markers))): + self.app.markers[i].setFrequency( + str(self.app.data11[both[i]].freq)) + self.app.markers[i].frequencyInput.setText( + str(self.app.data11[both[i]].freq)) + else: + logger.info("TO DO: find near data") + for m in crossing: + start, lowest, end = m + my_data = self._get_data(lowest) - if lowest in extended_data: - extended_data[lowest].update(my_data) - else: - extended_data[lowest] = my_data + if lowest in extended_data: + extended_data[lowest].update(my_data) + else: + extended_data[lowest] = my_data - logger.debug("maximumx %s of type %s", maximums, type(maximums)) - for m in maximums: - logger.debug("m %s of type %s", m, type(m)) + logger.debug("maximumx %s of type %s", maximums, type(maximums)) + for m in maximums: + logger.debug("m %s of type %s", m, type(m)) - my_data = self._get_data(m) - if m in extended_data: - extended_data[m].update(my_data) - else: - extended_data[m] = my_data + my_data = self._get_data(m) + if m in extended_data: + extended_data[m].update(my_data) + else: + extended_data[m] = my_data for index in sorted(extended_data.keys()): @@ -321,7 +353,7 @@ def runAnalysis(self): f" ({format_complex_imp(self.app.data11[index].impedance())})")) # Remove the final separator line - self.layout.removeRow(self.layout.rowCount() - 1) + # self.layout.removeRow(self.layout.rowCount() - 1) if filename and extended_data: with open(filename, 'w', newline='') as csvfile: From 7feff63c5eee9452259b51458becd78df39f5365 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Mon, 21 Dec 2020 20:41:15 +0100 Subject: [PATCH 05/26] simple fix markers --- NanoVNASaver/Analysis/PeakSearchAnalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NanoVNASaver/Analysis/PeakSearchAnalysis.py b/NanoVNASaver/Analysis/PeakSearchAnalysis.py index 1b78b388..37c4ed73 100644 --- a/NanoVNASaver/Analysis/PeakSearchAnalysis.py +++ b/NanoVNASaver/Analysis/PeakSearchAnalysis.py @@ -144,7 +144,7 @@ def runAnalysis(self): logger.debug("Frequency %d", self.app.data11[peaks[i]].freq) logger.debug("Value %f", data[peaks[i]]) - if self.checkbox_move_markers: + if self.checkbox_move_markers.isChecked(): if count > len(self.app.markers): logger.warning("More peaks found than there are markers") for i in range(min(count, len(self.app.markers))): From 922f24357f2ca5f9887319cccfc407def2a6c4d5 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Mon, 21 Dec 2020 20:48:36 +0100 Subject: [PATCH 06/26] both Simple peak an fixed peak --- NanoVNASaver/Windows/AnalysisWindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NanoVNASaver/Windows/AnalysisWindow.py b/NanoVNASaver/Windows/AnalysisWindow.py index e6588ad7..04b69950 100644 --- a/NanoVNASaver/Windows/AnalysisWindow.py +++ b/NanoVNASaver/Windows/AnalysisWindow.py @@ -57,7 +57,8 @@ def __init__(self, app: QtWidgets.QWidget): "High-pass filter", HighPassAnalysis(self.app)) self.analysis_list.addItem( "Band-stop filter", BandStopAnalysis(self.app)) - # self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app)) + self.analysis_list.addItem( + "Simple Peak search", SimplePeakSearchAnalysis(self.app)) self.analysis_list.addItem( "Peak search", PeakSearchAnalysis(self.app)) self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app)) From 2f2c0d621a417b09923a7441abc98ba02eb49870 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Mon, 21 Dec 2020 20:49:27 +0100 Subject: [PATCH 07/26] compare previus analysis --- NanoVNASaver/Analysis/VSWRAnalysis.py | 45 +++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index 608c505d..126647b5 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -27,6 +27,10 @@ from NanoVNASaver.RFTools import reflection_coefficient import os import csv +from NanoVNASaver.Marker.Values import Label +from NanoVNASaver.Marker.Widget import MarkerLabel +from NanoVNASaver.Marker.Widget import Marker +from collections import OrderedDict logger = logging.getLogger(__name__) @@ -264,10 +268,10 @@ class EFHWAnalysis(ResonanceAnalysis): ''' find only resonance when HI impedance ''' + old_data = [] def reset(self): logger.debug("reset") - pass def runAnalysis(self): self.reset() @@ -293,7 +297,7 @@ def runAnalysis(self): for i in range(results_header, self.layout.rowCount()): self.layout.removeRow(self.layout.rowCount() - 1) - extended_data = {} + extended_data = OrderedDict() #both = np.intersect1d([i[1] for i in crossing], maximums) both = [] @@ -319,6 +323,14 @@ def runAnalysis(self): else: extended_data[m] = my_data for i in range(min(len(both), len(self.app.markers))): + + # self.app.markers[i].label = {} + # for l in TYPES: + # self.app.markers[i][l.label_id] = MarkerLabel(l.name) + # self.app.markers[i].label['actualfreq'].setMinimumWidth( + # 100) + # self.app.markers[i].label['returnloss'].setMinimumWidth(80) + self.app.markers[i].setFrequency( str(self.app.data11[both[i]].freq)) self.app.markers[i].frequencyInput.setText( @@ -365,3 +377,32 @@ def runAnalysis(self): for index in sorted(extended_data.keys()): row = extended_data[index] writer.writerow(row) + + # saving and comparing + if self.old_data: + self.compare(self.old_data[-1], extended_data) + self.old_data.append(extended_data) + + def compare(self, old, new): + ''' + Compare data to help changes + + NB + must be same sweep + ( same index must be same frequence ) + :param old: + :param new: + ''' + old_idx = list(old.keys()) + new_idx = list(new.keys()) # 'odict_keys' object is not subscriptable + if len(old_idx) == len(new_idx): + logger.debug("may be the same antenna ... analyzing") + for i, k in enumerate(old.keys()): + logger.info("Risonance %s", i) + for d in ["freq", "r", "lambda"]: + logger.info("Delta %s = %s", d, + new[new_idx[i]][d] - old[k][d]) + + else: + logger.warning("resonances changed from %s to %s", + len(old.keys()), len(new.keys())) From 391b8881a8015da1e36c066716609b847fd8db31 Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Tue, 22 Dec 2020 15:18:52 +0100 Subject: [PATCH 08/26] compares even if different number of resonances --- NanoVNASaver/Analysis/VSWRAnalysis.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index 126647b5..8a7d3592 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -397,12 +397,21 @@ def compare(self, old, new): new_idx = list(new.keys()) # 'odict_keys' object is not subscriptable if len(old_idx) == len(new_idx): logger.debug("may be the same antenna ... analyzing") - for i, k in enumerate(old.keys()): - logger.info("Risonance %s", i) - for d in ["freq", "r", "lambda"]: - logger.info("Delta %s = %s", d, - new[new_idx[i]][d] - old[k][d]) - + i_max = len(old_idx) else: logger.warning("resonances changed from %s to %s", len(old.keys()), len(new.keys())) + + i_max = min(len(old_idx), len(new_idx)) + logger.debug("Trying to compare only first %s resonances", i_max) + + for i, k in enumerate(old.keys()): + if i < i_max: + logger.info("Risonance %s at %s", i, + format_frequency(old[k]["freq"])) + for d in ["freq", "r", "lambda"]: + logger.info("Delta %s = %s", d, + round(new[new_idx[i]][d] - old[k][d], 2)) + else: + logger.debug( + "Skipping Risonance %s because is missing in new", i) From 9c1031fa561d6aa7075f90f848da32f2e8cbed8d Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Tue, 22 Dec 2020 21:28:04 +0100 Subject: [PATCH 09/26] show diff --- NanoVNASaver/Analysis/VSWRAnalysis.py | 70 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index 8a7d3592..f7f93c3a 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -31,10 +31,16 @@ from NanoVNASaver.Marker.Widget import MarkerLabel from NanoVNASaver.Marker.Widget import Marker from collections import OrderedDict +from NanoVNASaver.Formatting import format_frequency_short +from NanoVNASaver.Formatting import format_resistance logger = logging.getLogger(__name__) +def round_2(x): + return round(x, 2) + + class VSWRAnalysis(Analysis): max_dips_shown = 3 vswr_limit_value = 1.5 @@ -356,13 +362,27 @@ def runAnalysis(self): else: extended_data[m] = my_data - for index in sorted(extended_data.keys()): + # saving and comparing + + fields = [("freq", format_frequency_short), + ("r", format_resistance), + ("lambda", round_2), + ] + if self.old_data: + diff = self.compare( + self.old_data[-1], extended_data, fields=fields) + else: + diff = self.compare({}, extended_data, fields=fields) + self.old_data.append(extended_data) + + for i, index in enumerate(extended_data.keys()): self.layout.addRow( - "Resonance", - QtWidgets.QLabel( - f"{format_frequency(self.app.data11[index].freq)}" - f" ({format_complex_imp(self.app.data11[index].impedance())})")) + f"{format_frequency_short(self.app.data11[index].freq)}", + QtWidgets.QLabel(f" ({diff[i]['freq']})" + f" {format_complex_imp(self.app.data11[index].impedance())}" + f" ({diff[i]['r']})" + f" {diff[i]['lambda']} m")) # Remove the final separator line # self.layout.removeRow(self.layout.rowCount() - 1) @@ -378,12 +398,7 @@ def runAnalysis(self): row = extended_data[index] writer.writerow(row) - # saving and comparing - if self.old_data: - self.compare(self.old_data[-1], extended_data) - self.old_data.append(extended_data) - - def compare(self, old, new): + def compare(self, old, new, fields=[("freq", str), ]): ''' Compare data to help changes @@ -393,25 +408,46 @@ def compare(self, old, new): :param old: :param new: ''' + old_idx = list(old.keys()) new_idx = list(new.keys()) # 'odict_keys' object is not subscriptable - if len(old_idx) == len(new_idx): + diff = {} + i_max = min(len(old_idx), len(new_idx)) + i_tot = max(len(old_idx), len(new_idx)) + + if i_max == i_tot: logger.debug("may be the same antenna ... analyzing") - i_max = len(old_idx) + else: logger.warning("resonances changed from %s to %s", - len(old.keys()), len(new.keys())) + len(old_idx), len(new_idx)) - i_max = min(len(old_idx), len(new_idx)) logger.debug("Trying to compare only first %s resonances", i_max) for i, k in enumerate(old.keys()): + my_diff = {} if i < i_max: logger.info("Risonance %s at %s", i, format_frequency(old[k]["freq"])) - for d in ["freq", "r", "lambda"]: + + for d, fn in fields: + my_diff[d] = fn(new[new_idx[i]][d] - old[k][d]) logger.info("Delta %s = %s", d, - round(new[new_idx[i]][d] - old[k][d], 2)) + my_diff[d]) + else: logger.debug( "Skipping Risonance %s because is missing in new", i) + for d in fields: + my_diff[d] = "-" + + diff[i] = my_diff + + for i in range(i_max, i_tot): + # add missing in old ... if any + my_diff = {} + for d, _ in fields: + my_diff[d] = "-" + diff[i] = my_diff + + return diff From 06912dac5594df45280108f6791e878497315a4a Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Wed, 23 Dec 2020 13:27:53 +0100 Subject: [PATCH 10/26] fix typo --- NanoVNASaver/NanoVNASaver.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py index b60e86a8..a2be7259 100644 --- a/NanoVNASaver/NanoVNASaver.py +++ b/NanoVNASaver/NanoVNASaver.py @@ -109,7 +109,6 @@ def __init__(self): self.calibration = Calibration() - logger.debug("Building user interface") self.baseTitle = f"NanoVNA Saver {NanoVNASaver.version}" @@ -196,7 +195,8 @@ def __init__(self): left_column = QtWidgets.QVBoxLayout() right_column = QtWidgets.QVBoxLayout() right_column.addLayout(self.charts_layout) - self.marker_frame.setHidden(not self.settings.value("MarkersVisible", True, bool)) + self.marker_frame.setHidden( + not self.settings.value("MarkersVisible", True, bool)) chart_widget = QtWidgets.QWidget() chart_widget.setLayout(right_column) self.splitter = QtWidgets.QSplitter() @@ -317,9 +317,11 @@ def __init__(self): tdr_control_box.setMaximumWidth(250) self.tdr_result_label = QtWidgets.QLabel() - tdr_control_layout.addRow("Estimated cable length:", self.tdr_result_label) + tdr_control_layout.addRow( + "Estimated cable length:", self.tdr_result_label) - self.tdr_button = QtWidgets.QPushButton("Time Domain Reflectometry ...") + self.tdr_button = QtWidgets.QPushButton( + "Time Domain Reflectometry ...") self.tdr_button.clicked.connect(lambda: self.display_window("tdr")) tdr_control_layout.addRow(self.tdr_button) @@ -525,7 +527,7 @@ def connect_device(self): logger.info("Connection %s", self.interface) try: self.interface.open() - self.interface.timeout = 0.05 + except (IOError, AttributeError) as exc: logger.error("Tried to open %s and failed: %s", self.interface, exc) @@ -533,13 +535,15 @@ def connect_device(self): if not self.interface.isOpen(): logger.error("Unable to open port %s", self.interface) return + self.interface.timeout = 0.05 sleep(0.1) try: self.vna = get_VNA(self.interface) except IOError as exc: logger.error("Unable to connect to VNA: %s", exc) - self.vna.validateInput = self.settings.value("SerialInputValidation", True, bool) + self.vna.validateInput = self.settings.value( + "SerialInputValidation", True, bool) # connected self.btnSerialToggle.setText("Disconnect") @@ -661,7 +665,7 @@ def dataUpdated(self): if s21data: min_gain = min(s21data, key=lambda data: data.gain) - max_gain = min(s21data, key=lambda data: data.gain) + max_gain = max(s21data, key=lambda data: data.gain) self.s21_min_gain_label.setText( f"{format_gain(min_gain.gain)}" f" @ {format_frequency(min_gain.freq)}") From ec08d068c2fa892faa8f90cd3c9eb82694856aad Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Wed, 23 Dec 2020 16:15:08 +0100 Subject: [PATCH 11/26] try to read initial frequencies --- NanoVNASaver/Hardware/NanoVNA.py | 19 +++++++++++++++++-- NanoVNASaver/Hardware/VNA.py | 8 ++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/NanoVNASaver/Hardware/NanoVNA.py b/NanoVNASaver/Hardware/NanoVNA.py index e6ac3c07..1569e9a8 100644 --- a/NanoVNASaver/Hardware/NanoVNA.py +++ b/NanoVNASaver/Hardware/NanoVNA.py @@ -40,10 +40,25 @@ def __init__(self, iface: Interface): super().__init__(iface) self.sweep_method = "sweep" self.read_features() - self.start = 27000000 - self.stop = 30000000 + logger.debug("Setting initial start,stop") + self.start, self.stop = self._get_running_frequencies() self._sweepdata = [] + def _get_running_frequencies(self): + + if self.name == "NanoVNA": + logger.debug("Reading values: frequencies") + try: + frequencies = super().readValues("frequencies") + return frequencies[0], frequencies[-1] + except Exception as e: + logger.warning("%s reading frequencies", e) + logger.info("falling back to generic") + else: + logger.debug("Name %s, fallback to generic", self.name) + + return VNA._get_running_frequencies(self) + def _capture_data(self) -> bytes: timeout = self.serial.timeout with self.serial.lock: diff --git a/NanoVNASaver/Hardware/VNA.py b/NanoVNASaver/Hardware/VNA.py index a16034e1..98504959 100644 --- a/NanoVNASaver/Hardware/VNA.py +++ b/NanoVNASaver/Hardware/VNA.py @@ -146,6 +146,14 @@ def readFrequencies(self) -> List[int]: def resetSweep(self, start: int, stop: int): pass + def _get_running_frequencies(self): + ''' + If possible, read frequencies already runnung + if not return default values + Overwrite in specific HW + ''' + return 27000000, 30000000 + def connected(self) -> bool: return self.serial.is_open From 654252e5bfeb152de25b1b04737ced77cbb96aec Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Wed, 23 Dec 2020 19:46:46 +0100 Subject: [PATCH 12/26] better compare ?!? --- NanoVNASaver/Analysis/Analysis.py | 8 ++-- NanoVNASaver/Analysis/VSWRAnalysis.py | 68 ++++++++++++++++++++------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/NanoVNASaver/Analysis/Analysis.py b/NanoVNASaver/Analysis/Analysis.py index a3e4bfa6..241f280f 100644 --- a/NanoVNASaver/Analysis/Analysis.py +++ b/NanoVNASaver/Analysis/Analysis.py @@ -101,7 +101,7 @@ def find_minimums(cls, data, threshold): return minimums @classmethod - def find_maximums(cls, data, threshold=0): + def find_maximums(cls, data, threshold=None): ''' Find peacs @@ -116,8 +116,10 @@ def find_maximums(cls, data, threshold=0): # my_data = np.array(data) # maximums = argrelextrema(my_data, np.greater)[0] - - return peaks + if threshold is None: + return peaks + else: + return [k for k in peaks if data[k] > threshold] def __init__(self, app: QtWidgets.QWidget): self.app = app diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index f7f93c3a..7769621d 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -41,6 +41,10 @@ def round_2(x): return round(x, 2) +def format_resistence_neg(x): + return format_resistance(x, allow_negative=True) + + class VSWRAnalysis(Analysis): max_dips_shown = 3 vswr_limit_value = 1.5 @@ -295,7 +299,7 @@ def runAnalysis(self): for d in self.app.data11: data.append(d.impedance().real) - maximums = sorted(self.find_maximums(data)) + maximums = sorted(self.find_maximums(data, threshold=500)) results_header = self.layout.indexOf(self.results_label) logger.debug("Results start at %d, out of %d", @@ -365,7 +369,7 @@ def runAnalysis(self): # saving and comparing fields = [("freq", format_frequency_short), - ("r", format_resistance), + ("r", format_resistence_neg), ("lambda", round_2), ] if self.old_data: @@ -409,6 +413,10 @@ def compare(self, old, new, fields=[("freq", str), ]): :param new: ''' + def no_compare(): + + return {k: "-" for k, _ in fields} + old_idx = list(old.keys()) new_idx = list(new.keys()) # 'odict_keys' object is not subscriptable diff = {} @@ -424,30 +432,54 @@ def compare(self, old, new, fields=[("freq", str), ]): logger.debug("Trying to compare only first %s resonances", i_max) - for i, k in enumerate(old.keys()): + split = 0 + max_delta_f = 1000000 # 1M + for i, k in enumerate(new_idx): my_diff = {} - if i < i_max: - logger.info("Risonance %s at %s", i, - format_frequency(old[k]["freq"])) - for d, fn in fields: - my_diff[d] = fn(new[new_idx[i]][d] - old[k][d]) - logger.info("Delta %s = %s", d, - my_diff[d]) + logger.info("Risonance %s at %s", i, + format_frequency(new[k]["freq"])) + + if len(old_idx) <= i + split: + diff[i] = no_compare() + continue + + delta_f = new[k]["freq"] - old[old_idx[i + split]]["freq"] + if abs(delta_f) < max_delta_f: + logger.debug("can compare") else: - logger.debug( - "Skipping Risonance %s because is missing in new", i) - for d in fields: - my_diff[d] = "-" + logger.debug("can't compare, %s is too much ", + format_frequency(delta_f)) + if delta_f > 0: + + logger.debug("possible missing band, ") + if (len(old_idx) > (i + split + 1)): + if abs(new[k]["freq"] - old[old_idx[i + split + 1]]["freq"]) < max_delta_f: + logger.debug("new is missing band, compare next ") + split += 1 + # FIXME: manage 2 or more band missing ?!? + else: + logger.debug("new band, non compare ") + diff[i] = no_compare() + continue + else: + logger.debug("new band, non compare ") + diff[i] = no_compare() + + split -= 1 + continue + + for d, fn in fields: + my_diff[d] = fn(new[k][d] - old[old_idx[i + split]][d]) + logger.info("Delta %s = %s", d, + my_diff[d]) diff[i] = my_diff for i in range(i_max, i_tot): # add missing in old ... if any - my_diff = {} - for d, _ in fields: - my_diff[d] = "-" - diff[i] = my_diff + + diff[i] = no_compare() return diff From 69db5f14d74b65d43b6d18d3b4f1f652a190adbb Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Wed, 23 Dec 2020 20:56:16 +0100 Subject: [PATCH 13/26] show peaks --- NanoVNASaver/Analysis/PeakSearchAnalysis.py | 59 ++++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/NanoVNASaver/Analysis/PeakSearchAnalysis.py b/NanoVNASaver/Analysis/PeakSearchAnalysis.py index 37c4ed73..0611f962 100644 --- a/NanoVNASaver/Analysis/PeakSearchAnalysis.py +++ b/NanoVNASaver/Analysis/PeakSearchAnalysis.py @@ -23,6 +23,10 @@ import numpy as np from NanoVNASaver.Analysis import Analysis +from NanoVNASaver.Formatting import format_vswr +from NanoVNASaver.Formatting import format_gain +from NanoVNASaver.Formatting import format_resistance +from NanoVNASaver.Formatting import format_frequency_short logger = logging.getLogger(__name__) @@ -38,8 +42,8 @@ def __init__(self, app): super().__init__(app) self._widget = QtWidgets.QWidget() - outer_layout = QtWidgets.QFormLayout() - self._widget.setLayout(outer_layout) + self.layout = QtWidgets.QFormLayout() + self._widget.setLayout(self.layout) self.rbtn_data_group = QtWidgets.QButtonGroup() self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR") @@ -70,37 +74,41 @@ def __init__(self, app): self.checkbox_move_markers = QtWidgets.QCheckBox() - outer_layout.addRow(QtWidgets.QLabel("Settings")) - outer_layout.addRow("Data source", self.rbtn_data_vswr) - outer_layout.addRow("", self.rbtn_data_resistance) - outer_layout.addRow("", self.rbtn_data_reactance) - outer_layout.addRow("", self.rbtn_data_s21_gain) - outer_layout.addRow(PeakSearchAnalysis.QHLine()) - outer_layout.addRow("Peak type", self.rbtn_peak_positive) - outer_layout.addRow("", self.rbtn_peak_negative) + self.layout.addRow(QtWidgets.QLabel("Settings")) + self.layout.addRow("Data source", self.rbtn_data_vswr) + self.layout.addRow("", self.rbtn_data_resistance) + self.layout.addRow("", self.rbtn_data_reactance) + self.layout.addRow("", self.rbtn_data_s21_gain) + self.layout.addRow(PeakSearchAnalysis.QHLine()) + self.layout.addRow("Peak type", self.rbtn_peak_positive) + self.layout.addRow("", self.rbtn_peak_negative) # outer_layout.addRow("", self.rbtn_peak_both) - outer_layout.addRow(PeakSearchAnalysis.QHLine()) - outer_layout.addRow("Max number of peaks", self.input_number_of_peaks) - outer_layout.addRow("Move markers", self.checkbox_move_markers) - outer_layout.addRow(PeakSearchAnalysis.QHLine()) - - outer_layout.addRow(QtWidgets.QLabel("Results")) + self.layout.addRow(PeakSearchAnalysis.QHLine()) + self.layout.addRow("Max number of peaks", self.input_number_of_peaks) + self.layout.addRow("Move markers", self.checkbox_move_markers) + self.layout.addRow(PeakSearchAnalysis.QHLine()) + self.layout.addRow(QtWidgets.QLabel("Results")) + self.results_header = self.layout.rowCount() def runAnalysis(self): self.reset() data = [] + sign = 1 count = self.input_number_of_peaks.value() if self.rbtn_data_vswr.isChecked(): - + fn = format_vswr for d in self.app.data11: data.append(d.vswr) elif self.rbtn_data_s21_gain.isChecked(): + fn = format_gain for d in self.app.data21: data.append(d.gain) elif self.rbtn_data_resistance.isChecked(): + fn = format_resistance for d in self.app.data11: data.append(d.impedance().real) elif self.rbtn_data_reactance.isChecked(): + fn = str for d in self.app.data11: data.append(d.impedance().imag) @@ -112,7 +120,8 @@ def runAnalysis(self): peaks, _ = signal.find_peaks( data, width=3, distance=3, prominence=1) elif self.rbtn_peak_negative.isChecked(): - data = [x * -1 for x in data] + sign = -1 + data = [x * sign for x in data] peaks, _ = signal.find_peaks( data, width=3, distance=3, prominence=1) # elif self.rbtn_peak_both.isChecked(): @@ -142,7 +151,11 @@ def runAnalysis(self): logger.debug("Prominence %f", prominences[i]) logger.debug("Index in sweep %d", peaks[i]) logger.debug("Frequency %d", self.app.data11[peaks[i]].freq) - logger.debug("Value %f", data[peaks[i]]) + logger.debug("Value %f", sign * data[peaks[i]]) + self.layout.addRow( + f"Freq {format_frequency_short(self.app.data11[peaks[i]].freq)}", + QtWidgets.QLabel(f" value {fn(sign * data[peaks[i]])}" + )) if self.checkbox_move_markers.isChecked(): if count > len(self.app.markers): @@ -163,4 +176,10 @@ def runAnalysis(self): logger.debug("Max peak at %d, value %f", max_idx, max_val) def reset(self): - pass + logger.debug("Reset analysis") + + logger.debug("Results start at %d, out of %d", + self.results_header, self.layout.rowCount()) + for i in range(self.results_header, self.layout.rowCount()): + logger.debug("deleting %s", self.layout.rowCount()) + self.layout.removeRow(self.layout.rowCount() - 1) From 8ab2709bf2abfa5bb6fedfb142f511497778e60b Mon Sep 17 00:00:00 2001 From: Mauro Gaioni Date: Thu, 24 Dec 2020 20:04:57 +0100 Subject: [PATCH 14/26] ensure sorting --- NanoVNASaver/Analysis/VSWRAnalysis.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py index 7769621d..200dda46 100644 --- a/NanoVNASaver/Analysis/VSWRAnalysis.py +++ b/NanoVNASaver/Analysis/VSWRAnalysis.py @@ -379,7 +379,7 @@ def runAnalysis(self): diff = self.compare({}, extended_data, fields=fields) self.old_data.append(extended_data) - for i, index in enumerate(extended_data.keys()): + for i, index in enumerate(sorted(extended_data.keys())): self.layout.addRow( f"{format_frequency_short(self.app.data11[index].freq)}", @@ -417,8 +417,9 @@ def no_compare(): return {k: "-" for k, _ in fields} - old_idx = list(old.keys()) - new_idx = list(new.keys()) # 'odict_keys' object is not subscriptable + old_idx = sorted(old.keys()) + # 'odict_keys' object is not subscriptable + new_idx = sorted(new.keys()) diff = {} i_max = min(len(old_idx), len(new_idx)) i_tot = max(len(old_idx), len(new_idx)) From 45294a104833898644e87a886b0dab55d70ffcaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20M=C3=BCller?= Date: Thu, 31 Dec 2020 14:01:55 +0100 Subject: [PATCH 15/26] v0.3.9-pre --- NanoVNASaver/About.py | 2 +- README.md | 18 ++---------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/NanoVNASaver/About.py b/NanoVNASaver/About.py index 0b195f83..18d510f1 100644 --- a/NanoVNASaver/About.py +++ b/NanoVNASaver/About.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -VERSION = "0.3.8" +VERSION = "0.3.9-pre" VERSION_URL = ( "https://raw.githubusercontent.com/" "NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py") diff --git a/README.md b/README.md index 3523c683..335f90a5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ points, and generally display and analyze the resulting data. Latest Changes -------------- +### Changes in v0.3.9 + ### Changes in v0.3.8 - Allow editing of bands above 2.4GHz @@ -24,22 +26,6 @@ Latest Changes - Support for Nanovna-F V2 - Fixes a crash with S21 hack -### Changes in v0.3.7 - -- Added a delta marker -- Segments can now have exponential different step widths - (see logarithmic sweeping) -- More different data points selectable - (shorter are useful on logarithmic sweeping) -- Scrollable marker column -- Markers initialize on start, middle, end -- Frequency input is now more "lazy" - 10m, 50K and 1g are now valid for 10MHz, 50kHz and 1GHz -- Added a wavelength field to Markers -- 32 bit windows binaries build in actions -- Stability improvements due to better exception handling -- Workaround for wrong first S21mag value on V2 devices - Introduction ------------ From 5fa16c65a3b5d639758f9d0e85835bc4112000b5 Mon Sep 17 00:00:00 2001 From: Galileo Date: Fri, 8 Jan 2021 20:53:07 +0100 Subject: [PATCH 16/26] 401 sweep points --- NanoVNASaver/Hardware/NanoVNA_H4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NanoVNASaver/Hardware/NanoVNA_H4.py b/NanoVNASaver/Hardware/NanoVNA_H4.py index c689de10..67a33c60 100644 --- a/NanoVNASaver/Hardware/NanoVNA_H4.py +++ b/NanoVNASaver/Hardware/NanoVNA_H4.py @@ -27,7 +27,7 @@ class NanoVNA_H4(NanoVNA_H): name = "NanoVNA-H4" screenwidth = 480 screenheight = 320 - valid_datapoints = (101, 11, 51, 201) + valid_datapoints = (101, 11, 51, 201, 401) def __init__(self, iface: Interface): super().__init__(iface) From c6878fce8f39474a62fe45bc0a31203550dd3918 Mon Sep 17 00:00:00 2001 From: Roel Jordans Date: Thu, 18 Feb 2021 16:57:49 +0100 Subject: [PATCH 17/26] Added measurement graphs showing impedance measured via S21 Fixes #165 - Allows for both |Z| and R + jX display - Measurements of shunt impedance (accurate for low impedance values) as well as series impedance (accurate for high impedance values) --- NanoVNASaver/Charts/MagnitudeZ.py | 9 +++++- NanoVNASaver/Charts/MagnitudeZSeries.py | 40 +++++++++++++++++++++++++ NanoVNASaver/Charts/MagnitudeZShunt.py | 40 +++++++++++++++++++++++++ NanoVNASaver/Charts/RI.py | 20 +++++++++---- NanoVNASaver/Charts/RISeries.py | 35 ++++++++++++++++++++++ NanoVNASaver/Charts/RIShunt.py | 35 ++++++++++++++++++++++ NanoVNASaver/Charts/__init__.py | 4 +++ NanoVNASaver/NanoVNASaver.py | 8 +++-- NanoVNASaver/RFTools.py | 12 ++++++++ 9 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 NanoVNASaver/Charts/MagnitudeZSeries.py create mode 100644 NanoVNASaver/Charts/MagnitudeZShunt.py create mode 100644 NanoVNASaver/Charts/RISeries.py create mode 100644 NanoVNASaver/Charts/RIShunt.py diff --git a/NanoVNASaver/Charts/MagnitudeZ.py b/NanoVNASaver/Charts/MagnitudeZ.py index 6b13bd46..447f94dc 100644 --- a/NanoVNASaver/Charts/MagnitudeZ.py +++ b/NanoVNASaver/Charts/MagnitudeZ.py @@ -89,6 +89,8 @@ def drawValues(self, qp: QtGui.QPainter): maxValue = 0 for d in self.data: mag = self.magnitude(d) + if math.isinf(mag): # Avoid infinite scales + continue if mag > maxValue: maxValue = mag if mag < minValue: @@ -97,6 +99,8 @@ def drawValues(self, qp: QtGui.QPainter): if d.freq < self.fstart or d.freq > self.fstop: continue mag = self.magnitude(d) + if math.isinf(mag): # Avoid infinite scales + continue if mag > maxValue: maxValue = mag if mag < minValue: @@ -142,7 +146,10 @@ def drawValues(self, qp: QtGui.QPainter): def getYPosition(self, d: Datapoint) -> int: mag = self.magnitude(d) - return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight) + if math.isfinite(mag): + return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight) + else: + return self.topMargin def valueAtPosition(self, y) -> List[float]: absy = y - self.topMargin diff --git a/NanoVNASaver/Charts/MagnitudeZSeries.py b/NanoVNASaver/Charts/MagnitudeZSeries.py new file mode 100644 index 00000000..1d276caf --- /dev/null +++ b/NanoVNASaver/Charts/MagnitudeZSeries.py @@ -0,0 +1,40 @@ + +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020 NanoVNA-Saver Authors +# +# 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 . +import math +import logging +from typing import List + +from PyQt5 import QtWidgets, QtGui + +from NanoVNASaver.RFTools import Datapoint +from .MagnitudeZ import MagnitudeZChart + + +logger = logging.getLogger(__name__) + + +class MagnitudeZSeriesChart(MagnitudeZChart): + def __init__(self, name=""): + super().__init__(name) + + @staticmethod + def magnitude(p: Datapoint) -> float: + return abs(p.seriesImpedance()) + diff --git a/NanoVNASaver/Charts/MagnitudeZShunt.py b/NanoVNASaver/Charts/MagnitudeZShunt.py new file mode 100644 index 00000000..0bd4d057 --- /dev/null +++ b/NanoVNASaver/Charts/MagnitudeZShunt.py @@ -0,0 +1,40 @@ + +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020 NanoVNA-Saver Authors +# +# 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 . +import math +import logging +from typing import List + +from PyQt5 import QtWidgets, QtGui + +from NanoVNASaver.RFTools import Datapoint +from .MagnitudeZ import MagnitudeZChart + + +logger = logging.getLogger(__name__) + + +class MagnitudeZShuntChart(MagnitudeZChart): + def __init__(self, name=""): + super().__init__(name) + + @staticmethod + def magnitude(p: Datapoint) -> float: + return abs(p.shuntImpedance()) + diff --git a/NanoVNASaver/Charts/RI.py b/NanoVNASaver/Charts/RI.py index e27ad6ac..09050f12 100644 --- a/NanoVNASaver/Charts/RI.py +++ b/NanoVNASaver/Charts/RI.py @@ -178,8 +178,10 @@ def drawValues(self, qp: QtGui.QPainter): max_real = 0 max_imag = -1000 for d in self.data: - imp = d.impedance() + imp = self.impedance(d) re, im = imp.real, imp.imag + if math.isinf(re): # Avoid infinite scales + continue if re > max_real: max_real = re if re < min_real: @@ -191,8 +193,10 @@ def drawValues(self, qp: QtGui.QPainter): for d in self.reference: # Also check min/max for the reference sweep if d.freq < fstart or d.freq > fstop: continue - imp = d.impedance() + imp = self.impedance(d) re, im = imp.real, imp.imag + if math.isinf(re): # Avoid infinite scales + continue if re > max_real: max_real = re if re < min_real: @@ -397,12 +401,15 @@ def drawValues(self, qp: QtGui.QPainter): self.drawMarker(x, y_im, qp, m.color, self.markers.index(m)+1) def getImYPosition(self, d: Datapoint) -> int: - im = d.impedance().imag + im = self.impedance(d).imag return self.topMargin + round((self.max_imag - im) / self.span_imag * self.chartHeight) def getReYPosition(self, d: Datapoint) -> int: - re = d.impedance().real - return self.topMargin + round((self.max_real - re) / self.span_real * self.chartHeight) + re = self.impedance(d).real + if math.isfinite(re): + return self.topMargin + round((self.max_real - re) / self.span_real * self.chartHeight) + else: + return self.topMargin def valueAtPosition(self, y) -> List[float]: absy = y - self.topMargin @@ -520,3 +527,6 @@ def contextMenuEvent(self, event): self.action_set_fixed_maximum_imag.setText( f"Maximum jX ({self.maxDisplayImag})") self.menu.exec_(event.globalPos()) + + def impedance(self, p: Datapoint) -> complex: + return p.impedance() diff --git a/NanoVNASaver/Charts/RISeries.py b/NanoVNASaver/Charts/RISeries.py new file mode 100644 index 00000000..b2ca4eff --- /dev/null +++ b/NanoVNASaver/Charts/RISeries.py @@ -0,0 +1,35 @@ +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020 NanoVNA-Saver Authors +# +# 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 . +import math +import logging +from typing import List + +from NanoVNASaver.RFTools import Datapoint + +from .RI import RealImaginaryChart + +logger = logging.getLogger(__name__) + + +class RealImaginarySeriesChart(RealImaginaryChart): + def __init__(self, name=""): + super().__init__(name) + + def impedance(self, p: Datapoint) -> complex: + return p.seriesImpedance() diff --git a/NanoVNASaver/Charts/RIShunt.py b/NanoVNASaver/Charts/RIShunt.py new file mode 100644 index 00000000..82e602f8 --- /dev/null +++ b/NanoVNASaver/Charts/RIShunt.py @@ -0,0 +1,35 @@ +# NanoVNASaver +# +# A python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019, 2020 Rune B. Broberg +# Copyright (C) 2020 NanoVNA-Saver Authors +# +# 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 . +import math +import logging +from typing import List + +from NanoVNASaver.RFTools import Datapoint + +from .RI import RealImaginaryChart + +logger = logging.getLogger(__name__) + + +class RealImaginaryShuntChart(RealImaginaryChart): + def __init__(self, name=""): + super().__init__(name) + + def impedance(self, p: Datapoint) -> complex: + return p.shuntImpedance() diff --git a/NanoVNASaver/Charts/__init__.py b/NanoVNASaver/Charts/__init__.py index ce8c34f2..fb0022be 100644 --- a/NanoVNASaver/Charts/__init__.py +++ b/NanoVNASaver/Charts/__init__.py @@ -9,10 +9,14 @@ from .CLogMag import CombinedLogMagChart from .Magnitude import MagnitudeChart from .MagnitudeZ import MagnitudeZChart +from .MagnitudeZShunt import MagnitudeZShuntChart +from .MagnitudeZSeries import MagnitudeZSeriesChart from .Permeability import PermeabilityChart from .Phase import PhaseChart from .QFactor import QualityFactorChart from .RI import RealImaginaryChart +from .RIShunt import RealImaginaryShuntChart +from .RISeries import RealImaginarySeriesChart from .Smith import SmithChart from .SParam import SParameterChart from .TDR import TDRChart diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py index b60e86a8..628fc396 100644 --- a/NanoVNASaver/NanoVNASaver.py +++ b/NanoVNASaver/NanoVNASaver.py @@ -40,9 +40,9 @@ CapacitanceChart, CombinedLogMagChart, GroupDelayChart, InductanceChart, LogMagChart, PhaseChart, - MagnitudeChart, MagnitudeZChart, + MagnitudeChart, MagnitudeZChart, MagnitudeZShuntChart, MagnitudeZSeriesChart, QualityFactorChart, VSWRChart, PermeabilityChart, PolarChart, - RealImaginaryChart, + RealImaginaryChart, RealImaginaryShuntChart, RealImaginarySeriesChart, SmithChart, SParameterChart, TDRChart, ) from .Calibration import Calibration @@ -155,6 +155,10 @@ def __init__(self): reflective=False)), ("log_mag", LogMagChart("S21 Gain")), ("magnitude", MagnitudeChart("|S21|")), + ("magnitude_z_shunt", MagnitudeZShuntChart("S21 |Z| shunt")), + ("magnitude_z_series", MagnitudeZSeriesChart("S21 |Z| series")), + ("real_imag_shunt", RealImaginaryShuntChart("S21 R+jX shunt")), + ("real_imag_series", RealImaginarySeriesChart("S21 R+jX series")), ("phase", PhaseChart("S21 Phase")), ("polar", PolarChart("S21 Polar Plot")), ("s_parameter", SParameterChart("S21 Real/Imaginary")), diff --git a/NanoVNASaver/RFTools.py b/NanoVNASaver/RFTools.py index 3803bea0..529aae54 100644 --- a/NanoVNASaver/RFTools.py +++ b/NanoVNASaver/RFTools.py @@ -67,6 +67,18 @@ def wavelength(self) -> float: def impedance(self, ref_impedance: float = 50) -> complex: return gamma_to_impedance(self.z, ref_impedance) + def shuntImpedance(self, ref_impedance: float = 50) -> complex: + try: + return 0.5 * ref_impedance * self.z / (1 - self.z) + except ZeroDivisionError: + return math.inf + + def seriesImpedance(self, ref_impedance: float = 50) -> complex: + try: + return 2 * ref_impedance * (1 - self.z) / self.z + except ZeroDivisionError: + return math.inf + def qFactor(self, ref_impedance: float = 50) -> float: imp = self.impedance(ref_impedance) if imp.real == 0.0: From 9c7f6a80f7731b537e4cf28ed87f7029e50c390f Mon Sep 17 00:00:00 2001 From: Roel Jordans Date: Sun, 21 Feb 2021 21:48:04 +0100 Subject: [PATCH 18/26] Adding support for plotting |Z| on logarithmic scale --- NanoVNASaver/Charts/Frequency.py | 37 +++++++++++++++++++++++++ NanoVNASaver/Charts/MagnitudeZ.py | 46 +++++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/NanoVNASaver/Charts/Frequency.py b/NanoVNASaver/Charts/Frequency.py index 7b99309e..66e9fd68 100644 --- a/NanoVNASaver/Charts/Frequency.py +++ b/NanoVNASaver/Charts/Frequency.py @@ -44,6 +44,7 @@ class FrequencyChart(Chart): fixedValues = False logarithmicX = False + logarithmicY = False leftMargin = 30 rightMargin = 20 @@ -132,6 +133,23 @@ def __init__(self, name): self.y_menu.addAction(self.action_set_fixed_maximum) self.y_menu.addAction(self.action_set_fixed_minimum) + if self.logarithmicYAllowed(): # This only works for some plot types + self.y_menu.addSeparator() + vertical_mode_group = QtWidgets.QActionGroup(self.y_menu) + self.action_set_linear_y = QtWidgets.QAction("Linear") + self.action_set_linear_y.setCheckable(True) + self.action_set_logarithmic_y = QtWidgets.QAction("Logarithmic") + self.action_set_logarithmic_y.setCheckable(True) + vertical_mode_group.addAction(self.action_set_linear_y) + vertical_mode_group.addAction(self.action_set_logarithmic_y) + self.action_set_linear_y.triggered.connect( + lambda: self.setLogarithmicY(False)) + self.action_set_logarithmic_y.triggered.connect( + lambda: self.setLogarithmicY(True)) + self.action_set_linear_y.setChecked(True) + self.y_menu.addAction(self.action_set_linear_y) + self.y_menu.addAction(self.action_set_logarithmic_y) + self.menu.addMenu(self.x_menu) self.menu.addMenu(self.y_menu) self.menu.addSeparator() @@ -178,12 +196,21 @@ def setFixedValues(self, fixed_values: bool): self.fixedValues = False self.y_action_automatic.setChecked(True) self.y_action_fixed_span.setChecked(False) + if fixed_values and self.minDisplayValue <= 0: + self.minDisplayValue = 0.01 self.update() def setLogarithmicX(self, logarithmic: bool): self.logarithmicX = logarithmic self.update() + def setLogarithmicY(self, logarithmic: bool): + self.logarithmicY = logarithmic and self.logarithmicYAllowed() + self.update() + + def logarithmicYAllowed(self) -> bool: + return False + def setMinimumFrequency(self): min_freq_str, selected = QtWidgets.QInputDialog.getText( self, "Start frequency", @@ -217,6 +244,8 @@ def setMinimumValue(self): return if not (self.fixedValues and min_val >= self.maxDisplayValue): self.minDisplayValue = min_val + if self.logarithmicY and min_val <= 0: + self.minDisplayValue = 0.01 if self.fixedValues: self.update() @@ -239,6 +268,9 @@ def resetDisplayLimits(self): self.action_automatic.setChecked(True) self.logarithmicX = False self.action_set_linear_x.setChecked(True) + self.logarithmicY = False + if self.logarithmicYAllowed(): + self.action_set_linear_y.setChecked(True) self.update() def getXPosition(self, d: Datapoint) -> int: @@ -585,6 +617,11 @@ def copy(self): new_chart.setLogarithmicX(self.logarithmicX) new_chart.action_set_logarithmic_x.setChecked(self.logarithmicX) new_chart.action_set_linear_x.setChecked(not self.logarithmicX) + + new_chart.setLogarithmicY(self.logarithmicY) + if self.logarithmicYAllowed(): + new_chart.action_set_logarithmic_y.setChecked(self.logarithmicY) + new_chart.action_set_linear_y.setChecked(not self.logarithmicY) return new_chart def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None: diff --git a/NanoVNASaver/Charts/MagnitudeZ.py b/NanoVNASaver/Charts/MagnitudeZ.py index 447f94dc..c2109aa7 100644 --- a/NanoVNASaver/Charts/MagnitudeZ.py +++ b/NanoVNASaver/Charts/MagnitudeZ.py @@ -82,7 +82,10 @@ def drawValues(self, qp: QtGui.QPainter): maxValue = self.maxDisplayValue minValue = self.minDisplayValue self.maxValue = maxValue - self.minValue = minValue + if self.logarithmicY and minValue <= 0: + self.minValue = 0.01 + else: + self.minValue = minValue else: # Find scaling minValue = 100 @@ -107,7 +110,10 @@ def drawValues(self, qp: QtGui.QPainter): minValue = mag minValue = 10*math.floor(minValue/10) + if self.logarithmicY and minValue <= 0: + minValue = 0.01 self.minValue = minValue + maxValue = 10*math.ceil(maxValue/10) self.maxValue = maxValue @@ -118,17 +124,21 @@ def drawValues(self, qp: QtGui.QPainter): target_ticks = math.floor(self.chartHeight / 60) - for i in range(target_ticks): - val = minValue + (i / target_ticks) * span - y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight) + for i in range(1, target_ticks): + val = minValue + (i / target_ticks) * self.span + if self.logarithmicY: + y = self.topMargin + (self.maxValue - val) / self.span * self.chartHeight + val = self.valueAtPosition(y)[0] + else: + y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight) + qp.setPen(self.textColor) - if val != minValue: - digits = max(0, min(2, math.floor(3 - math.log10(abs(val))))) - if digits == 0: - vswrstr = str(round(val)) - else: - vswrstr = str(round(val, digits)) - qp.drawText(3, y + 3, vswrstr) + digits = max(0, min(2, math.floor(5 - math.log10(abs(val))))) + if digits == 0: + vswrstr = str(round(val)) + else: + vswrstr = str(round(val, digits)) + qp.drawText(3, y + 3, vswrstr) qp.setPen(QtGui.QPen(self.foregroundColor)) qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y) @@ -146,20 +156,32 @@ def drawValues(self, qp: QtGui.QPainter): def getYPosition(self, d: Datapoint) -> int: mag = self.magnitude(d) + if self.logarithmicY and mag == 0: + return self.topMargin - self.chartHeight if math.isfinite(mag): + if self.logarithmicY: + span = math.log(self.maxValue) - math.log(self.minValue) + return self.topMargin + round((math.log(self.maxValue) - math.log(mag)) / span * self.chartHeight) return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight) else: return self.topMargin def valueAtPosition(self, y) -> List[float]: absy = y - self.topMargin - val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue) + if self.logarithmicY: + span = math.log(self.maxValue) - math.log(self.minValue) + val = math.exp(math.log(self.maxValue) - absy * span / self.chartHeight) + else: + val = self.maxValue - (absy / self.chartHeight * self.span) return [val] @staticmethod def magnitude(p: Datapoint) -> float: return abs(p.impedance()) + def logarithmicYAllowed(self) -> bool: + return True; + def copy(self): new_chart: LogMagChart = super().copy() new_chart.span = self.span From 60ea55b6cf030d704d7a6ca443de8829ec0b3db5 Mon Sep 17 00:00:00 2001 From: Roel Jordans Date: Sun, 21 Feb 2021 21:48:28 +0100 Subject: [PATCH 19/26] Cleaning up code duplication for log scale plotting --- NanoVNASaver/Charts/Permeability.py | 21 ++------------------- NanoVNASaver/Charts/VSWR.py | 23 +++-------------------- 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/NanoVNASaver/Charts/Permeability.py b/NanoVNASaver/Charts/Permeability.py index 997019ba..76607f28 100644 --- a/NanoVNASaver/Charts/Permeability.py +++ b/NanoVNASaver/Charts/Permeability.py @@ -40,7 +40,6 @@ def __init__(self, name=""): self.fstop = 0 self.span = 0.01 self.max = 0 - self.logarithmicY = True self.maxDisplayValue = 100 self.minDisplayValue = -100 @@ -59,27 +58,11 @@ def __init__(self, name=""): self.setPalette(pal) self.setAutoFillBackground(True) - self.y_menu.addSeparator() - self.y_log_lin_group = QtWidgets.QActionGroup(self.y_menu) - self.y_action_linear = QtWidgets.QAction("Linear") - self.y_action_linear.setCheckable(True) - self.y_action_logarithmic = QtWidgets.QAction("Logarithmic") - self.y_action_logarithmic.setCheckable(True) - self.y_action_logarithmic.setChecked(True) - self.y_action_linear.triggered.connect(lambda: self.setLogarithmicY(False)) - self.y_action_logarithmic.triggered.connect(lambda: self.setLogarithmicY(True)) - self.y_log_lin_group.addAction(self.y_action_linear) - self.y_log_lin_group.addAction(self.y_action_logarithmic) - self.y_menu.addAction(self.y_action_linear) - self.y_menu.addAction(self.y_action_logarithmic) - - def setLogarithmicY(self, logarithmic: bool): - self.logarithmicY = logarithmic - self.update() + def logarithmicYAllowed(self) -> bool: + return True; def copy(self): new_chart: PermeabilityChart = super().copy() - new_chart.logarithmicY = self.logarithmicY return new_chart def drawChart(self, qp: QtGui.QPainter): diff --git a/NanoVNASaver/Charts/VSWR.py b/NanoVNASaver/Charts/VSWR.py index 737d96a6..7ce8dd47 100644 --- a/NanoVNASaver/Charts/VSWR.py +++ b/NanoVNASaver/Charts/VSWR.py @@ -29,7 +29,6 @@ class VSWRChart(FrequencyChart): - logarithmicY = False maxVSWR = 3 span = 2 @@ -51,27 +50,12 @@ def __init__(self, name=""): pal.setColor(QtGui.QPalette.Background, self.backgroundColor) self.setPalette(pal) self.setAutoFillBackground(True) - self.y_menu.addSeparator() - self.y_log_lin_group = QtWidgets.QActionGroup(self.y_menu) - self.y_action_linear = QtWidgets.QAction("Linear") - self.y_action_linear.setCheckable(True) - self.y_action_linear.setChecked(True) - self.y_action_logarithmic = QtWidgets.QAction("Logarithmic") - self.y_action_logarithmic.setCheckable(True) - self.y_action_linear.triggered.connect(lambda: self.setLogarithmicY(False)) - self.y_action_logarithmic.triggered.connect(lambda: self.setLogarithmicY(True)) - self.y_log_lin_group.addAction(self.y_action_linear) - self.y_log_lin_group.addAction(self.y_action_logarithmic) - self.y_menu.addAction(self.y_action_linear) - self.y_menu.addAction(self.y_action_logarithmic) - - def setLogarithmicY(self, logarithmic: bool): - self.logarithmicY = logarithmic - self.update() + + def logarithmicYAllowed(self) -> bool: + return True def copy(self): new_chart: VSWRChart = super().copy() - new_chart.logarithmicY = self.logarithmicY return new_chart def drawValues(self, qp: QtGui.QPainter): @@ -211,5 +195,4 @@ def valueAtPosition(self, y) -> List[float]: def resetDisplayLimits(self): self.maxDisplayValue = 25 - self.logarithmicY = False super().resetDisplayLimits() From dbe735a677b8b834500dc11bee514a7ae0a6f888 Mon Sep 17 00:00:00 2001 From: Roel Jordans Date: Sun, 21 Feb 2021 22:52:04 +0100 Subject: [PATCH 20/26] Fancy formatting (mkM) for impedance plots (|Z| and R+jX) --- NanoVNASaver/Charts/MagnitudeZ.py | 39 ++++++++++++------------------- NanoVNASaver/Charts/RI.py | 10 ++++---- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/NanoVNASaver/Charts/MagnitudeZ.py b/NanoVNASaver/Charts/MagnitudeZ.py index c2109aa7..40390bb5 100644 --- a/NanoVNASaver/Charts/MagnitudeZ.py +++ b/NanoVNASaver/Charts/MagnitudeZ.py @@ -23,6 +23,7 @@ from PyQt5 import QtWidgets, QtGui from NanoVNASaver.RFTools import Datapoint +from NanoVNASaver.SITools import Format, Value from .Frequency import FrequencyChart from .LogMag import LogMagChart @@ -122,32 +123,22 @@ def drawValues(self, qp: QtGui.QPainter): span = 0.01 self.span = span - target_ticks = math.floor(self.chartHeight / 60) + # We want one horizontal tick per 50 pixels, at most + horizontal_ticks = math.floor(self.chartHeight/50) + fmt = Format(max_nr_digits=4) + for i in range(horizontal_ticks): + y = self.topMargin + round(i * self.chartHeight / horizontal_ticks) + qp.setPen(QtGui.QPen(self.foregroundColor)) + qp.drawLine(self.leftMargin - 5, y, + self.leftMargin + self.chartWidth + 5, y) + qp.setPen(QtGui.QPen(self.textColor)) + val = Value(self.valueAtPosition(y)[0], fmt=fmt) + qp.drawText(3, y + 4, str(val)) - for i in range(1, target_ticks): - val = minValue + (i / target_ticks) * self.span - if self.logarithmicY: - y = self.topMargin + (self.maxValue - val) / self.span * self.chartHeight - val = self.valueAtPosition(y)[0] - else: - y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight) + qp.drawText(3, + self.chartHeight + self.topMargin, + str(Value(self.minValue, fmt=fmt))) - qp.setPen(self.textColor) - digits = max(0, min(2, math.floor(5 - math.log10(abs(val))))) - if digits == 0: - vswrstr = str(round(val)) - else: - vswrstr = str(round(val, digits)) - qp.drawText(3, y + 3, vswrstr) - 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(maxValue)) - qp.drawText(3, self.chartHeight+self.topMargin, str(minValue)) self.drawFrequencyTicks(qp) self.drawData(qp, self.data, self.sweepColor) diff --git a/NanoVNASaver/Charts/RI.py b/NanoVNASaver/Charts/RI.py index 09050f12..bdd4ff23 100644 --- a/NanoVNASaver/Charts/RI.py +++ b/NanoVNASaver/Charts/RI.py @@ -24,6 +24,7 @@ from NanoVNASaver.Marker import Marker from NanoVNASaver.RFTools import Datapoint +from NanoVNASaver.SITools import Format, Value from .Chart import Chart from .Frequency import FrequencyChart @@ -254,6 +255,7 @@ def drawValues(self, qp: QtGui.QPainter): # We want one horizontal tick per 50 pixels, at most horizontal_ticks = math.floor(self.chartHeight/50) + fmt = Format(max_nr_digits=4) for i in range(horizontal_ticks): y = self.topMargin + round(i * self.chartHeight / horizontal_ticks) qp.setPen(QtGui.QPen(self.foregroundColor)) @@ -261,13 +263,13 @@ def drawValues(self, qp: QtGui.QPainter): qp.setPen(QtGui.QPen(self.textColor)) re = max_real - i * span_real / horizontal_ticks im = max_imag - i * span_imag / horizontal_ticks - qp.drawText(3, y + 4, str(round(re, 1))) - qp.drawText(self.leftMargin + self.chartWidth + 8, y + 4, str(round(im, 1))) + qp.drawText(3, y + 4, str(Value(re, fmt=fmt))) + qp.drawText(self.leftMargin + self.chartWidth + 8, y + 4, str(Value(im, fmt=fmt))) - qp.drawText(3, self.chartHeight + self.topMargin, str(round(min_real, 1))) + qp.drawText(3, self.chartHeight + self.topMargin, str(Value(im, fmt=fmt))) qp.drawText(self.leftMargin + self.chartWidth + 8, self.chartHeight + self.topMargin, - str(round(min_imag, 1))) + str(Value(min_imag, fmt=fmt))) self.drawFrequencyTicks(qp) From dd8e86555cdfa70577250c7a29f6f6fa2ec0457c Mon Sep 17 00:00:00 2001 From: Sascha Silbe Date: Sat, 3 Apr 2021 16:48:48 +0200 Subject: [PATCH 21/26] Hardware: Add information about maximum stop frequency for all models The various models of VNAs supported by NanoVNASaver support very different frequency ranges. Make the maximum stop frequency for each model available as an attribute so we can use it later on. The settings are based on information from: - AVNA: http://www.janbob.com/electron/AVNA1/AVNA-UsersManual.pdf - NanoVNA (v1): https://www.dropbox.com/s/0t1ms1y0uyaa90s/NanoVNA-a1-20170115.pdf?dl=1 (linked from https://ttrf.tk/kit/nanovna/) - NanoVNA-F: https://github.com/flyoob/NanoVNA-F - NanoVNA-F V2: http://www.sysjoint.com/pdf/NanoVNA-F%2520V2%2520Portable%2520Vector%2520Network%2520Analyzer%2520User%2520Guide%2520V1.0.pdf - NanoVNA-H: https://nanovna.com/, https://item.taobao.com/item.htm?id=588815021483 - NanoVNA-H4: https://nanovna.com/ - NanoVNA V2: https://nanorfe.com/nanovna-v2.html --- NanoVNASaver/Hardware/AVNA.py | 1 + NanoVNASaver/Hardware/NanoVNA.py | 1 + NanoVNASaver/Hardware/NanoVNA_F.py | 5 +++++ NanoVNASaver/Hardware/NanoVNA_F_V2.py | 5 +++++ NanoVNASaver/Hardware/NanoVNA_H.py | 5 +++++ NanoVNASaver/Hardware/NanoVNA_H4.py | 1 + NanoVNASaver/Hardware/NanoVNA_V2.py | 9 ++++++++- NanoVNASaver/Hardware/VNA.py | 1 + 8 files changed, 27 insertions(+), 1 deletion(-) diff --git a/NanoVNASaver/Hardware/AVNA.py b/NanoVNASaver/Hardware/AVNA.py index 441bb9df..a26ed9a1 100644 --- a/NanoVNASaver/Hardware/AVNA.py +++ b/NanoVNASaver/Hardware/AVNA.py @@ -29,6 +29,7 @@ class AVNA(VNA): def __init__(self, iface: Interface): super().__init__(iface) + self.sweep_max_freq_Hz = 40e3 self.features.add("Customizable data points") def isValid(self): diff --git a/NanoVNASaver/Hardware/NanoVNA.py b/NanoVNASaver/Hardware/NanoVNA.py index 1569e9a8..d0cea41c 100644 --- a/NanoVNASaver/Hardware/NanoVNA.py +++ b/NanoVNASaver/Hardware/NanoVNA.py @@ -42,6 +42,7 @@ def __init__(self, iface: Interface): self.read_features() logger.debug("Setting initial start,stop") self.start, self.stop = self._get_running_frequencies() + self.sweep_max_freq_Hz = 300e6 self._sweepdata = [] def _get_running_frequencies(self): diff --git a/NanoVNASaver/Hardware/NanoVNA_F.py b/NanoVNASaver/Hardware/NanoVNA_F.py index 66928232..4852fbda 100644 --- a/NanoVNASaver/Hardware/NanoVNA_F.py +++ b/NanoVNASaver/Hardware/NanoVNA_F.py @@ -23,6 +23,7 @@ from PyQt5 import QtGui from NanoVNASaver.Hardware.NanoVNA import NanoVNA +from NanoVNASaver.Hardware.Serial import Interface logger = logging.getLogger(__name__) @@ -31,3 +32,7 @@ class NanoVNA_F(NanoVNA): name = "NanoVNA-F" screenwidth = 800 screenheight = 480 + + def __init__(self, iface: Interface): + super().__init__(iface) + self.sweep_max_freq_Hz = 1500e6 diff --git a/NanoVNASaver/Hardware/NanoVNA_F_V2.py b/NanoVNASaver/Hardware/NanoVNA_F_V2.py index ae228044..15a0bd60 100644 --- a/NanoVNASaver/Hardware/NanoVNA_F_V2.py +++ b/NanoVNASaver/Hardware/NanoVNA_F_V2.py @@ -6,6 +6,7 @@ from PyQt5 import QtGui from NanoVNASaver.Hardware.NanoVNA import NanoVNA +from NanoVNASaver.Hardware.Serial import Interface logger = logging.getLogger(__name__) @@ -15,6 +16,10 @@ class NanoVNA_F_V2(NanoVNA): screenwidth = 800 screenheight = 480 + def __init__(self, iface: Interface): + super().__init__(iface) + self.sweep_max_freq_Hz = 3e9 + def getScreenshot(self) -> QtGui.QPixmap: logger.debug("Capturing screenshot...") if not self.connected(): diff --git a/NanoVNASaver/Hardware/NanoVNA_H.py b/NanoVNASaver/Hardware/NanoVNA_H.py index 0615094f..a8ae05ae 100644 --- a/NanoVNASaver/Hardware/NanoVNA_H.py +++ b/NanoVNASaver/Hardware/NanoVNA_H.py @@ -19,9 +19,14 @@ import logging from NanoVNASaver.Hardware.NanoVNA import NanoVNA +from NanoVNASaver.Hardware.Serial import Interface logger = logging.getLogger(__name__) class NanoVNA_H(NanoVNA): name = "NanoVNA-H" + + def __init__(self, iface: Interface): + super().__init__(iface) + self.sweep_max_freq_Hz = 1500e6 diff --git a/NanoVNASaver/Hardware/NanoVNA_H4.py b/NanoVNASaver/Hardware/NanoVNA_H4.py index 67a33c60..be0c14c8 100644 --- a/NanoVNASaver/Hardware/NanoVNA_H4.py +++ b/NanoVNASaver/Hardware/NanoVNA_H4.py @@ -31,6 +31,7 @@ class NanoVNA_H4(NanoVNA_H): def __init__(self, iface: Interface): super().__init__(iface) + self.sweep_max_freq_Hz = 1500e6 self.sweep_method = "scan" if "Scan mask command" in self.features: self.sweep_method = "scan_mask" diff --git a/NanoVNASaver/Hardware/NanoVNA_V2.py b/NanoVNASaver/Hardware/NanoVNA_V2.py index c2c52619..284cc544 100644 --- a/NanoVNASaver/Hardware/NanoVNA_V2.py +++ b/NanoVNASaver/Hardware/NanoVNA_V2.py @@ -94,6 +94,11 @@ def read_features(self): self.features.add("Customizable data points") # TODO: more than one dp per freq self.features.add("Multi data points") + self.board_revision = self.read_board_revision() + if self.board_revision >= Version("2.0.4"): + self.sweep_max_freq_Hz = 4400e6 + else: + self.sweep_max_freq_Hz = 3000e6 if self.version <= Version("1.0.1"): logger.debug("Hack for s21 oddity in first sweeppoint") self.features.add("S21 hack") @@ -210,7 +215,9 @@ def read_board_revision(self) -> 'Version': if len(resp) != 2: logger.error("Timeout reading version registers") return None - return Version(f"{resp[0]}.0.{resp[1]}") + result = Version(f"{resp[0]}.0.{resp[1]}") + logger.debug("read_board_revision: %s", result) + return result def setSweep(self, start, stop): diff --git a/NanoVNASaver/Hardware/VNA.py b/NanoVNASaver/Hardware/VNA.py index 98504959..16bfe1d7 100644 --- a/NanoVNASaver/Hardware/VNA.py +++ b/NanoVNASaver/Hardware/VNA.py @@ -57,6 +57,7 @@ def __init__(self, iface: Interface): self.datapoints = self.valid_datapoints[0] self.bandwidth = 1000 self.bw_method = "ttrftech" + self.sweep_max_freq_Hz = None if self.connected(): self.version = self.readVersion() self.read_features() From a4f29565c47263ce6b8ac00b4f4dd5b456f5f02a Mon Sep 17 00:00:00 2001 From: Sascha Silbe Date: Tue, 6 Apr 2021 11:05:13 +0200 Subject: [PATCH 22/26] Add support for setting TX power on NanoVNA v2 As of firmware version 1.0.2 (tag 20210214) the output power of the TX ADF4350 can be configured via USB (register 0x42). Add support for this setting in NanoVNASaver. The SI5351 does not support setting the output voltage / power (only the maximum current which controls rise / fall time rather than output power) so we can only affect frequencies from 140MHz up. Clearly show this in the UI so there are no surprises. Because we can only detect this capability by talking to the VNA, the setting is populated during connect. *WARNING*: The automatic first sweep after connecting to the VNA will be performed at maximum power (default). Sensitive equipment should not be connect until after NanoVNASaver has connected to the NanoVNA v2 and the setting has been changed. --- NanoVNASaver/Hardware/NanoVNA_V2.py | 34 +++++++++++++++++++++++++++ NanoVNASaver/Hardware/VNA.py | 6 +++++ NanoVNASaver/NanoVNASaver.py | 5 ++++ NanoVNASaver/Windows/SweepSettings.py | 20 ++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/NanoVNASaver/Hardware/NanoVNA_V2.py b/NanoVNASaver/Hardware/NanoVNA_V2.py index 284cc544..d45a04cc 100644 --- a/NanoVNASaver/Hardware/NanoVNA_V2.py +++ b/NanoVNASaver/Hardware/NanoVNA_V2.py @@ -57,6 +57,15 @@ WRITE_SLEEP = 0.05 +_ADF4350_TXPOWER_DESC_MAP = { + 0: '9dB attenuation', + 1: '6dB attenuation', + 2: '3dB attenuation', + 3: 'Maximum', +} +_ADF4350_TXPOWER_DESC_REV_MAP = { + value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()} + class NanoVNA_V2(VNA): name = "NanoVNA-V2" valid_datapoints = (101, 11, 51, 201, 301, 501, 1023) @@ -102,6 +111,13 @@ def read_features(self): if self.version <= Version("1.0.1"): logger.debug("Hack for s21 oddity in first sweeppoint") self.features.add("S21 hack") + if self.version >= Version("1.0.2"): + self.features.update({"Set TX power partial", "Set Average"}) + # Can only set ADF4350 power, i.e. for >= 140MHz + self.txPowerRanges = [ + ((140e6, self.sweep_max_freq_Hz), + [_ADF4350_TXPOWER_DESC_MAP[value] for value in (3, 2, 1, 0)]), + ] def readFirmware(self) -> str: result = f"HW: {self.read_board_revision()}\nFW: {self.version}" @@ -245,3 +261,21 @@ def _updateSweep(self): with self.serial.lock: self.serial.write(cmd) sleep(WRITE_SLEEP) + + def setTXPower(self, freq_range, power_desc): + if freq_range[0] != 140e6: + raise ValueError('Invalid TX power frequency range') + # 140MHz..max => ADF4350 + self._set_register(0x42, _ADF4350_TXPOWER_DESC_REV_MAP[power_desc], 1) + + def _set_register(self, addr, value, size): + if size == 1: + packet = pack(" 'Version': def setSweep(self, start, stop): list(self.exec_command(f"sweep {start} {stop} {self.datapoints}")) + + def setTXPower(self, freq_range, power_desc): + raise NotImplementedError() diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py index 8d72ece0..6a94c1b1 100644 --- a/NanoVNASaver/NanoVNASaver.py +++ b/NanoVNASaver/NanoVNASaver.py @@ -571,6 +571,8 @@ def connect_device(self): self.sweep_control.update_center_span() self.sweep_control.update_step_size() + self.windows["sweep_settings"].vna_connected() + logger.debug("Starting initial sweep") self.sweep_start() @@ -840,3 +842,6 @@ def changeFont(self, font: QtGui.QFont) -> None: def update_sweep_title(self): for c in self.subscribing_charts: c.setSweepTitle(self.sweep.properties.name) + + def set_tx_power(self, freq_range, power_desc): + self.vna.setTXPower(freq_range, power_desc) diff --git a/NanoVNASaver/Windows/SweepSettings.py b/NanoVNASaver/Windows/SweepSettings.py index 212b4f85..710d519c 100644 --- a/NanoVNASaver/Windows/SweepSettings.py +++ b/NanoVNASaver/Windows/SweepSettings.py @@ -44,6 +44,10 @@ def __init__(self, app: QtWidgets.QWidget): layout.addWidget(self.title_box()) layout.addWidget(self.settings_box()) + # We can only populate this box after the VNA has been connected. + self._power_box = QtWidgets.QGroupBox("Power") + self._power_layout = QtWidgets.QFormLayout(self._power_box) + layout.addWidget(self._power_box) layout.addWidget(self.sweep_box()) self.update_band() @@ -155,6 +159,17 @@ def sweep_box(self) -> 'QtWidgets.QWidget': layout.addRow(btn_set_band_sweep) return box + def vna_connected(self): + while self._power_layout.rowCount(): + self._power_layout.removeRow(0) + for freq_range, power_descs in self.app.vna.txPowerRanges: + power_sel = QtWidgets.QComboBox() + power_sel.addItems(power_descs) + power_sel.currentTextChanged.connect( + partial(self.update_tx_power, freq_range)) + self._power_layout.addRow("TX power {}..{}".format( + *map(format_frequency_short, freq_range)), power_sel) + def update_band(self, apply: bool = False): logger.debug("update_band(%s)", apply) index_start = self.band_list.model().index(self.band_list.currentIndex(), 1) @@ -233,3 +248,8 @@ def update_title(self, title: str = ""): with self.app.sweep.lock: self.app.sweep.properties.name = title self.app.update_sweep_title() + + def update_tx_power(self, freq_range, power_desc): + logger.debug("update_tx_power(%r)", power_desc) + with self.app.sweep.lock: + self.app.set_tx_power(freq_range, power_desc) From 9e85730f14872e42ca9d4e99d98f55d80a33dc63 Mon Sep 17 00:00:00 2001 From: Roel Jordans Date: Fri, 23 Apr 2021 21:32:59 +0200 Subject: [PATCH 23/26] Fixing minimum axis label value in RI charts Resolves plotting issue as discussed in #165 --- NanoVNASaver/Charts/RI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NanoVNASaver/Charts/RI.py b/NanoVNASaver/Charts/RI.py index bdd4ff23..9d5d2d4b 100644 --- a/NanoVNASaver/Charts/RI.py +++ b/NanoVNASaver/Charts/RI.py @@ -266,7 +266,7 @@ def drawValues(self, qp: QtGui.QPainter): qp.drawText(3, y + 4, str(Value(re, fmt=fmt))) qp.drawText(self.leftMargin + self.chartWidth + 8, y + 4, str(Value(im, fmt=fmt))) - qp.drawText(3, self.chartHeight + self.topMargin, str(Value(im, fmt=fmt))) + qp.drawText(3, self.chartHeight + self.topMargin, str(Value(min_real, fmt=fmt))) qp.drawText(self.leftMargin + self.chartWidth + 8, self.chartHeight + self.topMargin, str(Value(min_imag, fmt=fmt))) From b3068543a9c33f1ff3752250f4bbec163e875bc2 Mon Sep 17 00:00:00 2001 From: Daniel Lingvay Date: Sun, 4 Apr 2021 23:51:48 +0300 Subject: [PATCH 24/26] Add new s21 markers for |Z| and R+jX --- .gitignore | 1 + NanoVNASaver/Marker/Values.py | 4 ++++ NanoVNASaver/Marker/Widget.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 5c11ba05..77b21b34 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ settings.json .gitignore .coverage +/nanovna-saver.exe.spec \ No newline at end of file diff --git a/NanoVNASaver/Marker/Values.py b/NanoVNASaver/Marker/Values.py index 0cf4e70c..e1b4193b 100644 --- a/NanoVNASaver/Marker/Values.py +++ b/NanoVNASaver/Marker/Values.py @@ -54,6 +54,10 @@ class Label(NamedTuple): Label("s21phase", "S21 Phase", "S21 Phase", True), Label("s21polar", "S21 Polar", "S21 Polar", False), Label("s21groupdelay", "S21 Group Delay", "S21 Group Delay", False), + Label("s21magshunt", "S21 |Z| shunt", "S21 Z Magnitude shunt", False), + Label("s21magseries", "S21 |Z| series", "S21 Z Magnitude series", False), + Label("s21realimagshunt", "S21 R+jX shunt", "S21 Z Real+Imag shunt", False), + Label("s21realimagseries", "S21 R+jX series", "S21 Z Real+Imag series", False), ) diff --git a/NanoVNASaver/Marker/Widget.py b/NanoVNASaver/Marker/Widget.py index 8cac4952..5708f426 100644 --- a/NanoVNASaver/Marker/Widget.py +++ b/NanoVNASaver/Marker/Widget.py @@ -351,3 +351,7 @@ def updateLabels(self, self.label['s21phase'].setText(format_phase(s21.phase)) self.label['s21polar'].setText( str(round(abs(s21.z), 2)) + "∠" + format_phase(s21.phase)) + self.label['s21magshunt'].setText(format_magnitude(abs(s21.shuntImpedance()))) + self.label['s21magseries'].setText(format_magnitude(abs(s21.seriesImpedance()))) + self.label['s21realimagshunt'].setText(format_complex_imp(s21.shuntImpedance(), allow_negative=True)) + self.label['s21realimagseries'].setText(format_complex_imp(s21.seriesImpedance(), allow_negative=True)) From 28c62b707a4b64e8c510e99f59f59d6ec70b9f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20M=C3=BCller?= Date: Wed, 19 May 2021 15:52:48 +0200 Subject: [PATCH 25/26] v0.3.9-rc01 --- NanoVNASaver/About.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NanoVNASaver/About.py b/NanoVNASaver/About.py index 18d510f1..3a8023b6 100644 --- a/NanoVNASaver/About.py +++ b/NanoVNASaver/About.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -VERSION = "0.3.9-pre" +VERSION = "0.3.9-rc01" VERSION_URL = ( "https://raw.githubusercontent.com/" "NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py") From 0c352eed6413c4f5dff96c9decd875de5d50d0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20M=C3=BCller?= Date: Sun, 20 Jun 2021 13:16:18 +0200 Subject: [PATCH 26/26] v0.3.9 --- .gitattributes | 7 +++ CHANGELOG.md | 8 +++ NanoVNASaver/About.py | 2 +- NanoVNASaver/Hardware/NanoVNA_F_V2.py | 78 +++++++++++++-------------- 4 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..487e667b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Default for all text files +* text=auto whitespace=trailing-space,tab-in-indent,tabwidth=2 +*.py text=auto whitespace=trailing-space,tab-in-indent,tabwidth=4 + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary diff --git a/CHANGELOG.md b/CHANGELOG.md index 841431ad..1ccc1761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Changelog ========= +v0.3.9 +------ + +- TX Power on V2 +- New analysis +- Magnitude Z Chart +- VSWR Chart improvements + v0.3.8 ------ diff --git a/NanoVNASaver/About.py b/NanoVNASaver/About.py index 3a8023b6..ec594301 100644 --- a/NanoVNASaver/About.py +++ b/NanoVNASaver/About.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -VERSION = "0.3.9-rc01" +VERSION = "0.3.9" VERSION_URL = ( "https://raw.githubusercontent.com/" "NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py") diff --git a/NanoVNASaver/Hardware/NanoVNA_F_V2.py b/NanoVNASaver/Hardware/NanoVNA_F_V2.py index 15a0bd60..c3fcd92a 100644 --- a/NanoVNASaver/Hardware/NanoVNA_F_V2.py +++ b/NanoVNASaver/Hardware/NanoVNA_F_V2.py @@ -1,39 +1,39 @@ -import logging -from NanoVNASaver.Hardware.Serial import drain_serial, Interface -import serial -import struct -import numpy as np -from PyQt5 import QtGui - -from NanoVNASaver.Hardware.NanoVNA import NanoVNA -from NanoVNASaver.Hardware.Serial import Interface - -logger = logging.getLogger(__name__) - - -class NanoVNA_F_V2(NanoVNA): - name = "NanoVNA-F_V2" - screenwidth = 800 - screenheight = 480 - - def __init__(self, iface: Interface): - super().__init__(iface) - self.sweep_max_freq_Hz = 3e9 - - def getScreenshot(self) -> QtGui.QPixmap: - logger.debug("Capturing screenshot...") - if not self.connected(): - return QtGui.QPixmap() - try: - rgba_array = self._capture_data() - image = QtGui.QImage( - rgba_array, - self.screenwidth, - self.screenheight, - QtGui.QImage.Format_RGB16) - logger.debug("Captured screenshot") - return QtGui.QPixmap(image) - except serial.SerialException as exc: - logger.exception( - "Exception while capturing screenshot: %s", exc) - return QtGui.QPixmap() +import logging +from NanoVNASaver.Hardware.Serial import drain_serial, Interface +import serial +import struct +import numpy as np +from PyQt5 import QtGui + +from NanoVNASaver.Hardware.NanoVNA import NanoVNA +from NanoVNASaver.Hardware.Serial import Interface + +logger = logging.getLogger(__name__) + + +class NanoVNA_F_V2(NanoVNA): + name = "NanoVNA-F_V2" + screenwidth = 800 + screenheight = 480 + + def __init__(self, iface: Interface): + super().__init__(iface) + self.sweep_max_freq_Hz = 3e9 + + def getScreenshot(self) -> QtGui.QPixmap: + logger.debug("Capturing screenshot...") + if not self.connected(): + return QtGui.QPixmap() + try: + rgba_array = self._capture_data() + image = QtGui.QImage( + rgba_array, + self.screenwidth, + self.screenheight, + QtGui.QImage.Format_RGB16) + logger.debug("Captured screenshot") + return QtGui.QPixmap(image) + except serial.SerialException as exc: + logger.exception( + "Exception while capturing screenshot: %s", exc) + return QtGui.QPixmap()