# 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 logging
from PyQt5 import QtWidgets
from scipy import signal
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__)
class PeakSearchAnalysis(Analysis):
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.rbtn_data_group = QtWidgets.QButtonGroup()
self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR")
self.rbtn_data_resistance = QtWidgets.QRadioButton("Resistance")
self.rbtn_data_reactance = QtWidgets.QRadioButton("Reactance")
self.rbtn_data_s21_gain = QtWidgets.QRadioButton("S21 Gain")
self.rbtn_data_group.addButton(self.rbtn_data_vswr)
self.rbtn_data_group.addButton(self.rbtn_data_resistance)
self.rbtn_data_group.addButton(self.rbtn_data_reactance)
self.rbtn_data_group.addButton(self.rbtn_data_s21_gain)
self.rbtn_data_vswr.setChecked(True)
self.rbtn_peak_group = QtWidgets.QButtonGroup()
self.rbtn_peak_positive = QtWidgets.QRadioButton("Positive")
self.rbtn_peak_negative = QtWidgets.QRadioButton("Negative")
self.rbtn_peak_both = QtWidgets.QRadioButton("Both")
self.rbtn_peak_group.addButton(self.rbtn_peak_positive)
self.rbtn_peak_group.addButton(self.rbtn_peak_negative)
self.rbtn_peak_group.addButton(self.rbtn_peak_both)
self.rbtn_peak_positive.setChecked(True)
self.input_number_of_peaks = QtWidgets.QSpinBox()
self.input_number_of_peaks.setValue(1)
self.input_number_of_peaks.setMinimum(1)
self.input_number_of_peaks.setMaximum(10)
self.checkbox_move_markers = QtWidgets.QCheckBox()
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)
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)
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)
elif self.rbtn_peak_negative.isChecked():
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():
# 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)
# peaks = np.concatenate((peaks_max, peaks_min))
else:
# Both is not yet in
logger.warning(
"Searching for peaks,"
" but neither looking at positive nor negative?")
return
# Having found the peaks, get the prominence data
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))
# Find the peaks with the most extreme values
# Alternately, allow the user to select "most prominent"?
indices = np.argpartition(prominences, -count)[-count:]
logger.debug("%d indices", len(indices))
for i in indices:
logger.debug("Index %d", i)
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", 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):
logger.warning("More peaks found than there are markers")
for i in range(min(count, len(self.app.markers))):
self.app.markers[i].setFrequency(
str(self.app.data11[peaks[indices[i]]].freq))
self.app.markers[i].frequencyInput.setText(
str(self.app.data11[peaks[indices[i]]].freq))
max_val = -10**10
max_idx = -1
for p in peaks:
if data[p] > max_val:
max_val = data[p]
max_idx = p
logger.debug("Max peak at %d, value %f", max_idx, max_val)
def reset(self):
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)