kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Simplified analysis
rodzic
01eb028f9f
commit
d1ea20f989
|
@ -1,7 +1,8 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020ff 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
|
||||
|
@ -16,14 +17,9 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from PyQt5.Qt import QTimer
|
||||
|
||||
'''
|
||||
Created on May 30th 2020
|
||||
|
||||
@author: mauro
|
||||
'''
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
|
@ -67,6 +63,7 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
self.layout.addRow("", QtWidgets.QLabel(
|
||||
"Multiple minimums, not magloop or try to lower VSWR limit"))
|
||||
return
|
||||
|
||||
if len(self.minimums) == 1:
|
||||
m = self.minimums[0]
|
||||
start, lowest, end = m
|
||||
|
@ -104,6 +101,7 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
logger.debug(
|
||||
"no minimum found, looking for higher value %s",
|
||||
self.vswr_limit_value)
|
||||
|
||||
new_start = max(self.min_freq, new_start)
|
||||
new_end = min(self.max_freq, new_end)
|
||||
logger.debug("next search will be %s - %s for vswr %s",
|
||||
|
@ -113,16 +111,9 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
|
||||
self.app.sweep_control.set_start(new_start)
|
||||
self.app.sweep_control.set_end(new_end)
|
||||
# set timer to let finish all stuff before new sweep
|
||||
QTimer.singleShot(2000, self._safe_sweep)
|
||||
|
||||
def _safe_sweep(self):
|
||||
"""
|
||||
sweep only if button enabled
|
||||
to prevent multiple/concurrent sweep
|
||||
"""
|
||||
|
||||
# TODO: get info if sweep is running instead of just sleeping
|
||||
# a guessed time
|
||||
sleep(2.0)
|
||||
if self.app.sweep_control.btn_start.isEnabled():
|
||||
self.app.sweep_start()
|
||||
else:
|
||||
logger.error("sweep alredy running")
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020ff 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
|
||||
|
@ -18,36 +18,29 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, Tuple
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis
|
||||
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
||||
from NanoVNASaver.Formatting import format_frequency
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CUTOFF_VALS = (3.0, 6.0, 10.0, 20.0, 60.0)
|
||||
|
||||
|
||||
class BandPassAnalysis(Analysis):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
|
||||
self.label = {
|
||||
label: QtWidgets.QLabel() for label in
|
||||
('titel', 'result', 'octave_l', 'octave_r', 'decade_l', 'decade_r',
|
||||
'freq_center', 'span_3.0dB', 'span_6.0dB', 'q_factor')
|
||||
}
|
||||
for label in ('octave_l', 'octave_r', 'decade_l', 'decade_r',
|
||||
'freq_center', 'span_3.0dB', 'span_6.0dB', 'q_factor'):
|
||||
self.label[label] = QtWidgets.QLabel()
|
||||
for attn in CUTOFF_VALS:
|
||||
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
|
||||
self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout = self.layout
|
||||
layout.addRow(self.label['titel'])
|
||||
layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
|
@ -79,17 +72,10 @@ class BandPassAnalysis(Analysis):
|
|||
|
||||
self.set_titel("Band pass filter analysis")
|
||||
|
||||
def set_titel(self, name):
|
||||
self.label['titel'].setText(name)
|
||||
|
||||
def reset(self):
|
||||
for label in self.label.values():
|
||||
label.clear()
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s21:
|
||||
logger.debug("No data to analyse")
|
||||
self.label['result'].setText("No data to analyse.")
|
||||
self.set_result("No data to analyse.")
|
||||
return
|
||||
|
||||
self.reset()
|
||||
|
@ -124,10 +110,10 @@ class BandPassAnalysis(Analysis):
|
|||
}
|
||||
result['q_factor'] = result['freq_center'] / result['span_3.0dB']
|
||||
|
||||
result['octave_l'], result['decade_l'] = self.calculateRolloff(
|
||||
cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"])
|
||||
result['octave_r'], result['decade_r'] = self.calculateRolloff(
|
||||
cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"])
|
||||
result['octave_l'], result['decade_l'] = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"])
|
||||
result['octave_r'], result['decade_r'] = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"])
|
||||
|
||||
for label, val in cutoff_freq.items():
|
||||
self.label[label].setText(
|
||||
|
@ -141,22 +127,18 @@ class BandPassAnalysis(Analysis):
|
|||
self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
|
||||
|
||||
self.app.markers[0].setFrequency(f"{result['freq_center']}")
|
||||
self.app.markers[0].frequencyInput.setText(f"{result['freq_center']}")
|
||||
self.app.markers[1].setFrequency(f"{cutoff_freq['3.0dB_l']}")
|
||||
self.app.markers[1].frequencyInput.setText(f"{cutoff_freq['3.0dB_l']}")
|
||||
self.app.markers[2].setFrequency(f"{cutoff_freq['3.0dB_r']}")
|
||||
self.app.markers[2].frequencyInput.setText(f"{cutoff_freq['3.0dB_r']}")
|
||||
|
||||
if cutoff_gain['3.0dB_l'] < -4 or cutoff_gain['3.0dB_r'] < -4:
|
||||
logger.warning(
|
||||
"Data points insufficient for true -3 dB points."
|
||||
"Cutoff gains: %fdB, %fdB", cutoff_gain['3.0dB_l'], cutoff_gain['3.0dB_r'])
|
||||
self.label['result'].setText(
|
||||
self.set_result(
|
||||
f"Analysis complete ({len(s21)} points)\n"
|
||||
f"Insufficient data for analysis. Increase segment count.")
|
||||
return
|
||||
self.label['result'].setText(
|
||||
f"Analysis complete ({len(s21)} points)")
|
||||
self.set_result(f"Analysis complete ({len(s21)} points)")
|
||||
|
||||
def derive_60dB(self,
|
||||
cutoff_pos: Dict[str, int],
|
||||
|
@ -186,13 +168,12 @@ class BandPassAnalysis(Analysis):
|
|||
if marker.location <= 0 or marker.location >= len(gains) - 1:
|
||||
logger.debug("No valid location for %s (%s)",
|
||||
marker.name, marker.location)
|
||||
self.label['result'].setText(
|
||||
f"Please place {marker.name} in the passband.")
|
||||
self.set_result(f"Please place {marker.name} in the passband.")
|
||||
return -1
|
||||
|
||||
# find center of passband based on marker pos
|
||||
if (peak := at.center_from_idx(gains, marker.location)) < 0:
|
||||
self.label['result'].setText("Bandpass center not found")
|
||||
self.set_result("Bandpass center not found")
|
||||
return -1
|
||||
return peak
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ import logging
|
|||
from typing import Dict, List
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.BandPassAnalysis import (
|
||||
BandPassAnalysis, CUTOFF_VALS)
|
||||
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
||||
from NanoVNASaver.Analysis.BandPassAnalysis import BandPassAnalysis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020ff 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
|
||||
|
@ -17,16 +17,13 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
import scipy
|
||||
from typing import Dict
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CUTOFF_VALS = (3.0, 6.0, 10.0, 20.0, 60.0)
|
||||
|
||||
|
||||
class QHLine(QtWidgets.QFrame):
|
||||
def __init__(self):
|
||||
|
@ -35,100 +32,15 @@ class QHLine(QtWidgets.QFrame):
|
|||
|
||||
|
||||
class Analysis:
|
||||
_widget = None
|
||||
|
||||
@classmethod
|
||||
def find_crossing_zero(cls, data):
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def find_maximums(cls, data, threshold=None):
|
||||
"""
|
||||
|
||||
Find peacs
|
||||
|
||||
|
||||
:param cls:
|
||||
:param data: list of values
|
||||
:param threshold:
|
||||
"""
|
||||
peaks, _ = scipy.signal.find_peaks(
|
||||
data, width=2, distance=3, prominence=1)
|
||||
|
||||
# my_data = np.array(data)
|
||||
# maximums = argrelextrema(my_data, np.greater)[0]
|
||||
if threshold is None:
|
||||
return peaks
|
||||
return [k for k in peaks if data[k] > threshold]
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
self.app = app
|
||||
self.label: Dict[str, QtWidgets.QLabel] = {
|
||||
'titel': QtWidgets.QLabel(),
|
||||
'result': QtWidgets.QLabel(),
|
||||
}
|
||||
self.layout = QtWidgets.QFormLayout()
|
||||
self._widget = QtWidgets.QWidget()
|
||||
self._widget.setLayout(self.layout)
|
||||
|
||||
def widget(self) -> QtWidgets.QWidget:
|
||||
return self._widget
|
||||
|
@ -137,20 +49,11 @@ class Analysis:
|
|||
pass
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
for label in self.label.values():
|
||||
label.clear()
|
||||
|
||||
def calculateRolloff(self, idx_1: int, idx_2: int) -> Tuple[float, float]:
|
||||
if idx_1 == idx_2:
|
||||
return (math.nan, math.nan)
|
||||
s21 = self.app.data.s21
|
||||
freq_1 = s21[idx_1].freq
|
||||
freq_2 = s21[idx_2].freq
|
||||
gain1 = s21[idx_1].gain
|
||||
gain2 = s21[idx_2].gain
|
||||
factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1
|
||||
attn = abs(gain1 - gain2)
|
||||
logger.debug("Measured points: %d Hz and %d Hz\n%fdB over %f factor",
|
||||
freq_1, freq_2, attn, factor)
|
||||
octave_attn = attn / (math.log10(factor) / math.log10(2))
|
||||
decade_attn = attn / math.log10(factor)
|
||||
return (octave_attn, decade_attn)
|
||||
def set_result(self, text):
|
||||
self.label['result'].setText(text)
|
||||
|
||||
def set_titel(self, text):
|
||||
self.label['titel'].setText(text)
|
||||
|
|
|
@ -18,10 +18,12 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from NanoVNASaver.Analysis.Base import Analysis
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
||||
from NanoVNASaver.Formatting import format_frequency
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -31,174 +33,89 @@ class HighPassAnalysis(Analysis):
|
|||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
self.label["octave"] = QtWidgets.QLabel()
|
||||
self.label["decade"] = QtWidgets.QLabel()
|
||||
for attn in CUTOFF_VALS:
|
||||
self.label[f"{attn:.1f}dB"] = QtWidgets.QLabel()
|
||||
self.label[f"{attn:.1f}dB"] = QtWidgets.QLabel()
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout.addRow(QtWidgets.QLabel("High pass filter analysis"))
|
||||
layout = self.layout
|
||||
layout.addRow(self.label["titel"])
|
||||
layout.addRow(QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."))
|
||||
self.result_label = QtWidgets.QLabel()
|
||||
self.cutoff_label = QtWidgets.QLabel()
|
||||
self.six_db_label = QtWidgets.QLabel()
|
||||
self.sixty_db_label = QtWidgets.QLabel()
|
||||
self.db_per_octave_label = QtWidgets.QLabel()
|
||||
self.db_per_decade_label = QtWidgets.QLabel()
|
||||
layout.addRow("Result:", self.result_label)
|
||||
layout.addRow("Cutoff frequency:", self.cutoff_label)
|
||||
layout.addRow("-6 dB point:", self.six_db_label)
|
||||
layout.addRow("-60 dB point:", self.sixty_db_label)
|
||||
layout.addRow("Roll-off:", self.db_per_octave_label)
|
||||
layout.addRow("Roll-off:", self.db_per_decade_label)
|
||||
layout.addRow("Result:", self.label["result"])
|
||||
layout.addRow("Cutoff frequency:", self.label["3.0dB"])
|
||||
layout.addRow("-6 dB point:", self.label["6.0dB"])
|
||||
layout.addRow("-60 dB point:", self.label["60.0dB"])
|
||||
layout.addRow("Roll-off:", self.label["octave"])
|
||||
layout.addRow("Roll-off:", self.label["decade"])
|
||||
|
||||
def reset(self):
|
||||
self.result_label.clear()
|
||||
self.cutoff_label.clear()
|
||||
self.six_db_label.clear()
|
||||
self.sixty_db_label.clear()
|
||||
self.db_per_octave_label.clear()
|
||||
self.db_per_decade_label.clear()
|
||||
self.set_titel('Highpass analysis')
|
||||
|
||||
def runAnalysis(self):
|
||||
self.reset()
|
||||
pass_band_location = self.app.markers[0].location
|
||||
logger.debug("Pass band location: %d", pass_band_location)
|
||||
|
||||
if len(self.app.data.s21) == 0:
|
||||
if not self.app.data.s21:
|
||||
logger.debug("No data to analyse")
|
||||
self.result_label.setText("No data to analyse.")
|
||||
return
|
||||
|
||||
if pass_band_location < 0:
|
||||
logger.debug("No location for %s", self.app.markers[0].name)
|
||||
self.result_label.setText(
|
||||
f"Please place {self.app.markers[0].name } in the passband.")
|
||||
self.reset()
|
||||
s21 = self.app.data.s21
|
||||
gains = [d.gain for d in s21]
|
||||
|
||||
if (peak := self.find_level(gains)) < 0:
|
||||
return
|
||||
peak_db = gains[peak]
|
||||
logger.debug("Passband position: %d(%fdB)", peak, peak_db)
|
||||
|
||||
pass_band_db = self.app.data.s21[pass_band_location].gain
|
||||
cutoff_pos = self.find_cutoffs(gains, peak, peak_db)
|
||||
cutoff_freq = {
|
||||
att: s21[val].freq if val >= 0 else math.nan
|
||||
for att, val in cutoff_pos.items()
|
||||
}
|
||||
cutoff_gain = {
|
||||
att: gains[val] if val >= 0 else math.nan
|
||||
for att, val in cutoff_pos.items()
|
||||
}
|
||||
logger.debug("Cuttoff frequencies: %s", cutoff_freq)
|
||||
logger.debug("Cuttoff gains: %s", cutoff_gain)
|
||||
|
||||
logger.debug("Initial passband gain: %d", pass_band_db)
|
||||
octave, decade = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"])
|
||||
|
||||
initial_cutoff_location = -1
|
||||
for i in range(pass_band_location, -1, -1):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 3:
|
||||
# We found a cutoff location
|
||||
initial_cutoff_location = i
|
||||
break
|
||||
|
||||
if initial_cutoff_location < 0:
|
||||
self.result_label.setText("Cutoff location not found.")
|
||||
return
|
||||
|
||||
initial_cutoff_frequency = (
|
||||
self.app.data.s21[initial_cutoff_location].freq)
|
||||
|
||||
logger.debug("Found initial cutoff frequency at %d",
|
||||
initial_cutoff_frequency)
|
||||
|
||||
peak_location = -1
|
||||
peak_db = self.app.data.s21[initial_cutoff_location].gain
|
||||
for i in range(len(self.app.data.s21) - 1,
|
||||
initial_cutoff_location - 1, -1):
|
||||
if self.app.data.s21[i].gain > peak_db:
|
||||
peak_db = db
|
||||
peak_location = i
|
||||
|
||||
logger.debug("Found peak of %f at %d", peak_db,
|
||||
self.app.data.s11[peak_location].freq)
|
||||
|
||||
self.app.markers[0].setFrequency(
|
||||
str(self.app.data.s21[peak_location].freq))
|
||||
self.app.markers[0].frequencyInput.setText(
|
||||
str(self.app.data.s21[peak_location].freq))
|
||||
|
||||
cutoff_location = -1
|
||||
pass_band_db = peak_db
|
||||
for i in range(peak_location, -1, -1):
|
||||
if (pass_band_db - self.app.data.s21[i].gain) > 3:
|
||||
# We found the cutoff location
|
||||
cutoff_location = i
|
||||
break
|
||||
|
||||
cutoff_frequency = self.app.data.s21[cutoff_location].freq
|
||||
cutoff_gain = self.app.data.s21[cutoff_location].gain - pass_band_db
|
||||
if cutoff_gain < -4:
|
||||
if cutoff_gain['3.0dB'] < -4:
|
||||
logger.debug("Cutoff frequency found at %f dB"
|
||||
" - insufficient data points for true -3 dB point.",
|
||||
cutoff_gain)
|
||||
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
|
||||
logger.debug("Found true cutoff frequency at %d", cutoff_freq['3.0dB'])
|
||||
|
||||
self.cutoff_label.setText(
|
||||
f"{format_frequency(cutoff_frequency)}"
|
||||
f" {round(cutoff_gain, 1)} dB)")
|
||||
self.app.markers[1].setFrequency(str(cutoff_frequency))
|
||||
self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
|
||||
for label, val in cutoff_freq.items():
|
||||
self.label[label].setText(
|
||||
f"{format_frequency(val)}"
|
||||
f" ({cutoff_gain[label]:.1f} dB)")
|
||||
|
||||
six_db_location = -1
|
||||
for i in range(cutoff_location, -1, -1):
|
||||
if (pass_band_db - self.app.data.s21[i].gain) > 6:
|
||||
# We found 6dB location
|
||||
six_db_location = i
|
||||
break
|
||||
self.label['octave'].setText(f'{octave:.3f}dB/octave')
|
||||
self.label['decade'].setText(f'{decade:.3f}dB/decade')
|
||||
|
||||
if six_db_location < 0:
|
||||
self.result_label.setText("6 dB location not found.")
|
||||
return
|
||||
six_db_cutoff_frequency = self.app.data.s21[six_db_location].freq
|
||||
self.six_db_label.setText(
|
||||
format_frequency(six_db_cutoff_frequency))
|
||||
self.app.markers[0].setFrequency(str(s21[peak].freq))
|
||||
self.app.markers[1].setFrequency(str(cutoff_freq['3.0dB']))
|
||||
self.app.markers[2].setFrequency(str(cutoff_freq['6.0dB']))
|
||||
|
||||
ten_db_location = -1
|
||||
for i in range(cutoff_location, -1, -1):
|
||||
if (pass_band_db - self.app.data.s21[i].gain) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
self.set_result(f"Analysis complete ({len(s21)}) points)")
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(cutoff_location, -1, -1):
|
||||
if (pass_band_db - self.app.data.s21[i].gain) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
def find_level(self, gains: List[float]) -> int:
|
||||
marker = self.app.markers[0]
|
||||
logger.debug("Pass band location: %d", marker.location)
|
||||
if marker.location < 0:
|
||||
self.set_result(f"Please place {marker.name} in the passband.")
|
||||
return -1
|
||||
return at.center_from_idx(gains, marker.location)
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(six_db_location, -1, -1):
|
||||
if (pass_band_db - self.app.data.s21[i].gain) > 60:
|
||||
# We found 60dB location! Wow.
|
||||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if sixty_db_location > 0:
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = (
|
||||
self.app.data.s21[sixty_db_location].freq)
|
||||
self.sixty_db_label.setText(
|
||||
format_frequency(sixty_db_cutoff_frequency))
|
||||
elif ten_db_location != -1 and twenty_db_location != -1:
|
||||
ten = self.app.data.s21[ten_db_location].freq
|
||||
twenty = self.app.data.s21[twenty_db_location].freq
|
||||
sixty_db_frequency = ten * \
|
||||
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
|
||||
self.sixty_db_label.setText(
|
||||
f"{format_frequency(sixty_db_frequency)} (derived)")
|
||||
else:
|
||||
self.sixty_db_label.setText("Not calculated")
|
||||
|
||||
if (ten_db_location > 0 and
|
||||
twenty_db_location > 0 and
|
||||
ten_db_location != twenty_db_location):
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(
|
||||
ten_db_location, twenty_db_location)
|
||||
self.db_per_octave_label.setText(
|
||||
str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.db_per_decade_label.setText(
|
||||
str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
self.db_per_octave_label.setText("Not calculated")
|
||||
self.db_per_decade_label.setText("Not calculated")
|
||||
|
||||
self.result_label.setText(
|
||||
f"Analysis complete ({len(self.app.data.s11)}) points)")
|
||||
def find_cutoffs(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
return {
|
||||
f"{attn:.1f}dB": at.cut_off_left(
|
||||
gains, peak, peak_db, attn)
|
||||
for attn in CUTOFF_VALS
|
||||
}
|
||||
|
|
|
@ -17,195 +17,26 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from NanoVNASaver.Analysis.Base import Analysis
|
||||
from NanoVNASaver.Formatting import format_frequency
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
||||
from NanoVNASaver.Analysis.HighPassAnalysis import HighPassAnalysis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LowPassAnalysis(Analysis):
|
||||
class LowPassAnalysis(HighPassAnalysis):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
self.set_titel('Lowpass filter analysis')
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout.addRow(QtWidgets.QLabel("Low pass filter analysis"))
|
||||
layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."))
|
||||
self.result_label = QtWidgets.QLabel()
|
||||
self.cutoff_label = QtWidgets.QLabel()
|
||||
self.six_db_label = QtWidgets.QLabel()
|
||||
self.sixty_db_label = QtWidgets.QLabel()
|
||||
self.db_per_octave_label = QtWidgets.QLabel()
|
||||
self.db_per_decade_label = QtWidgets.QLabel()
|
||||
layout.addRow("Result:", self.result_label)
|
||||
layout.addRow("Cutoff frequency:", self.cutoff_label)
|
||||
layout.addRow("-6 dB point:", self.six_db_label)
|
||||
layout.addRow("-60 dB point:", self.sixty_db_label)
|
||||
layout.addRow("Roll-off:", self.db_per_octave_label)
|
||||
layout.addRow("Roll-off:", self.db_per_decade_label)
|
||||
|
||||
def reset(self):
|
||||
self.result_label.clear()
|
||||
self.cutoff_label.clear()
|
||||
self.six_db_label.clear()
|
||||
self.sixty_db_label.clear()
|
||||
self.db_per_octave_label.clear()
|
||||
self.db_per_decade_label.clear()
|
||||
|
||||
def runAnalysis(self):
|
||||
self.reset()
|
||||
pass_band_location = self.app.markers[0].location
|
||||
logger.debug("Pass band location: %d", pass_band_location)
|
||||
|
||||
if len(self.app.data.s21) == 0:
|
||||
logger.debug("No data to analyse")
|
||||
self.result_label.setText("No data to analyse.")
|
||||
return
|
||||
|
||||
if pass_band_location < 0:
|
||||
logger.debug("No location for %s",
|
||||
self.app.markers[0].name)
|
||||
self.result_label.setText(
|
||||
f"Please place {self.app.markers[0].name} in the passband.")
|
||||
return
|
||||
|
||||
pass_band_db = self.app.data.s21[pass_band_location].gain
|
||||
|
||||
logger.debug("Initial passband gain: %d", pass_band_db)
|
||||
|
||||
initial_cutoff_location = -1
|
||||
for i in range(pass_band_location, len(self.app.data.s21)):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 3:
|
||||
# We found a cutoff location
|
||||
initial_cutoff_location = i
|
||||
break
|
||||
|
||||
if initial_cutoff_location < 0:
|
||||
self.result_label.setText("Cutoff location not found.")
|
||||
return
|
||||
|
||||
initial_cutoff_frequency = (
|
||||
self.app.data.s21[initial_cutoff_location].freq)
|
||||
|
||||
logger.debug("Found initial cutoff frequency at %d",
|
||||
initial_cutoff_frequency)
|
||||
|
||||
peak_location = -1
|
||||
peak_db = self.app.data.s21[initial_cutoff_location].gain
|
||||
for i in range(0, initial_cutoff_location):
|
||||
db = self.app.data.s21[i].gain
|
||||
if db > peak_db:
|
||||
peak_db = db
|
||||
peak_location = i
|
||||
|
||||
logger.debug("Found peak of %f at %d", peak_db,
|
||||
self.app.data.s11[peak_location].freq)
|
||||
|
||||
self.app.markers[0].setFrequency(
|
||||
str(self.app.data.s21[peak_location].freq))
|
||||
self.app.markers[0].frequencyInput.setText(
|
||||
str(self.app.data.s21[peak_location].freq))
|
||||
|
||||
cutoff_location = -1
|
||||
pass_band_db = peak_db
|
||||
for i in range(peak_location, len(self.app.data.s21)):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 3:
|
||||
# We found the cutoff location
|
||||
cutoff_location = i
|
||||
break
|
||||
|
||||
cutoff_frequency = self.app.data.s21[cutoff_location].freq
|
||||
cutoff_gain = self.app.data.s21[cutoff_location].gain - pass_band_db
|
||||
if cutoff_gain < -4:
|
||||
logger.debug(
|
||||
"Cutoff frequency found at %f dB"
|
||||
" - insufficient data points for true -3 dB point.",
|
||||
cutoff_gain)
|
||||
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
|
||||
|
||||
self.cutoff_label.setText(
|
||||
f"{format_frequency(cutoff_frequency)}"
|
||||
f" ({round(cutoff_gain, 1)} dB)")
|
||||
self.app.markers[1].setFrequency(str(cutoff_frequency))
|
||||
self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
|
||||
|
||||
six_db_location = -1
|
||||
for i in range(cutoff_location, len(self.app.data.s21)):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 6:
|
||||
# We found 6dB location
|
||||
six_db_location = i
|
||||
break
|
||||
|
||||
if six_db_location < 0:
|
||||
self.result_label.setText("6 dB location not found.")
|
||||
return
|
||||
six_db_cutoff_frequency = self.app.data.s21[six_db_location].freq
|
||||
self.six_db_label.setText(
|
||||
format_frequency(six_db_cutoff_frequency))
|
||||
|
||||
ten_db_location = -1
|
||||
for i in range(cutoff_location, len(self.app.data.s21)):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(cutoff_location, len(self.app.data.s21)):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(six_db_location, len(self.app.data.s21)):
|
||||
db = self.app.data.s21[i].gain
|
||||
if (pass_band_db - db) > 60:
|
||||
# We found 60dB location! Wow.
|
||||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = (
|
||||
self.app.data.s21[sixty_db_location].freq)
|
||||
self.sixty_db_label.setText(
|
||||
format_frequency(sixty_db_cutoff_frequency))
|
||||
elif ten_db_location != -1 and twenty_db_location != -1:
|
||||
ten = self.app.data.s21[ten_db_location].freq
|
||||
twenty = self.app.data.s21[twenty_db_location].freq
|
||||
sixty_db_frequency = ten * \
|
||||
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
|
||||
self.sixty_db_label.setText(
|
||||
f"{format_frequency(sixty_db_frequency)} (derived)")
|
||||
else:
|
||||
self.sixty_db_label.setText("Not calculated")
|
||||
|
||||
if (ten_db_location > 0 and
|
||||
twenty_db_location > 0 and
|
||||
ten_db_location != twenty_db_location):
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(
|
||||
ten_db_location, twenty_db_location)
|
||||
self.db_per_octave_label.setText(
|
||||
str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.db_per_decade_label.setText(
|
||||
str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
self.db_per_octave_label.setText("Not calculated")
|
||||
self.db_per_decade_label.setText("Not calculated")
|
||||
|
||||
self.result_label.setText(
|
||||
"Analysis complete (" + str(len(self.app.data.s11)) + " points)")
|
||||
def find_cutoffs(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
return {
|
||||
f"{attn:.1f}dB": at.cut_off_right(
|
||||
gains, peak, peak_db, attn)
|
||||
for attn in CUTOFF_VALS
|
||||
}
|
||||
|
|
|
@ -22,7 +22,10 @@ from PyQt5 import QtWidgets
|
|||
import scipy
|
||||
import numpy as np
|
||||
|
||||
from NanoVNASaver.Analysis.Base import Analysis
|
||||
from NanoVNASaver.Analysis.Base import QHLine
|
||||
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
|
||||
SimplePeakSearchAnalysis)
|
||||
|
||||
from NanoVNASaver.Formatting import format_vswr
|
||||
from NanoVNASaver.Formatting import format_gain
|
||||
from NanoVNASaver.Formatting import format_resistance
|
||||
|
@ -32,99 +35,64 @@ 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)
|
||||
class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
self.layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(self.layout)
|
||||
self.peak_cnt = QtWidgets.QSpinBox()
|
||||
self.peak_cnt.setValue(1)
|
||||
self.peak_cnt.setMinimum(1)
|
||||
self.peak_cnt.setMaximum(10)
|
||||
|
||||
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("<b>Settings</b>"))
|
||||
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("Max number of peaks", self.peak_cnt)
|
||||
self.layout.addRow(QHLine())
|
||||
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
|
||||
self.results_header = self.layout.rowCount()
|
||||
|
||||
self.set_titel('Peak search')
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s11:
|
||||
return
|
||||
self.reset()
|
||||
data = []
|
||||
sign = 1
|
||||
count = self.input_number_of_peaks.value()
|
||||
if self.rbtn_data_vswr.isChecked():
|
||||
|
||||
s11 = self.app.data.s11
|
||||
s21 = self.app.data.s21
|
||||
|
||||
if not s21:
|
||||
self.button['gain'].setEnabled(False)
|
||||
if self.button['gain'].isChecked():
|
||||
self.button['vswr'].setChecked(True)
|
||||
else:
|
||||
self.button['gain'].setEnabled(True)
|
||||
|
||||
count = self.peak_cnt.value()
|
||||
if self.button['vswr'].isChecked():
|
||||
fn = format_vswr
|
||||
data.extend(d.vswr for d in self.app.data.s11)
|
||||
elif self.rbtn_data_s21_gain.isChecked():
|
||||
data = [d.vswr for d in s11]
|
||||
elif self.button['gain'].isChecked():
|
||||
fn = format_gain
|
||||
data.extend(d.gain for d in self.app.data.s21)
|
||||
elif self.rbtn_data_resistance.isChecked():
|
||||
data = [d.gain for d in s21]
|
||||
elif self.button['resistance'].isChecked():
|
||||
fn = format_resistance
|
||||
data.extend(d.impedance().real for d in self.app.data.s11)
|
||||
elif self.rbtn_data_reactance.isChecked():
|
||||
fn = str
|
||||
data.extend(d.impedance().imag for d in self.app.data.s11)
|
||||
data = [d.impedance().real for d in s11]
|
||||
elif self.button['reactance'].isChecked():
|
||||
fn = format_resistance
|
||||
data = [d.impedance().imag for d in s11]
|
||||
else:
|
||||
logger.warning("Searching for peaks on unknown data")
|
||||
return
|
||||
|
||||
if self.rbtn_peak_positive.isChecked():
|
||||
sign = 1
|
||||
if self.button['peak_h'].isChecked():
|
||||
peaks, _ = scipy.signal.find_peaks(
|
||||
data, width=3, distance=3, prominence=1)
|
||||
elif self.rbtn_peak_negative.isChecked():
|
||||
elif self.button['peak_l'].isChecked():
|
||||
sign = -1
|
||||
data = [x * sign for x in data]
|
||||
peaks, _ = scipy.signal.find_peaks(
|
||||
data, width=3, distance=3, prominence=1)
|
||||
# elif self.rbtn_peak_both.isChecked():
|
||||
# peaks_max, _ = scipy.signal.find_peaks(
|
||||
# data, width=3, distance=3, prominence=1)
|
||||
# peaks_min, _ = scipy.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(
|
||||
|
@ -147,22 +115,20 @@ class PeakSearchAnalysis(Analysis):
|
|||
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.data.s11[peaks[i]].freq)
|
||||
logger.debug("Frequency %d", s11[peaks[i]].freq)
|
||||
logger.debug("Value %f", sign * data[peaks[i]])
|
||||
self.layout.addRow(
|
||||
f"Freq"
|
||||
f" {format_frequency_short(self.app.data.s11[peaks[i]].freq)}",
|
||||
f" {format_frequency_short(s11[peaks[i]].freq)}",
|
||||
QtWidgets.QLabel(f" value {fn(sign * data[peaks[i]])}"
|
||||
))
|
||||
|
||||
if self.checkbox_move_markers.isChecked():
|
||||
if self.button['move_marker'].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.data.s11[peaks[indices[i]]].freq))
|
||||
self.app.markers[i].frequencyInput.setText(
|
||||
str(self.app.data.s11[peaks[indices[i]]].freq))
|
||||
str(s11[peaks[indices[i]]].freq))
|
||||
|
||||
max_val = -10**10
|
||||
max_idx = -1
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
import csv
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency, format_complex_imp,
|
||||
|
@ -62,12 +62,14 @@ class ResonanceAnalysis(Analysis):
|
|||
self.layout.addRow(self.results_label)
|
||||
|
||||
def _get_data(self, index):
|
||||
my_data = {"freq": self.app.data.s11[index].freq,
|
||||
"s11": self.app.data.s11[index].z,
|
||||
"lambda": self.app.data.s11[index].wavelength,
|
||||
"impedance": self.app.data.s11[index].impedance(),
|
||||
"vswr": self.app.data.s11[index].vswr,
|
||||
}
|
||||
s11 = self.app.data.s11
|
||||
my_data = {
|
||||
"freq": s11[index].freq,
|
||||
"s11": s11[index].z,
|
||||
"lambda": s11[index].wavelength,
|
||||
"impedance": s11[index].impedance(),
|
||||
"vswr": s11[index].vswr,
|
||||
}
|
||||
my_data["vswr_49"] = vswr_transformed(
|
||||
my_data["impedance"], 49)
|
||||
my_data["vswr_4"] = vswr_transformed(
|
||||
|
@ -79,7 +81,7 @@ class ResonanceAnalysis(Analysis):
|
|||
|
||||
def _get_crossing(self):
|
||||
data = [d.phase for d in self.app.data.s11]
|
||||
return sorted(self.find_crossing_zero(data))
|
||||
return at.zero_crossings(data)
|
||||
|
||||
def runAnalysis(self):
|
||||
self.reset()
|
||||
|
@ -99,41 +101,40 @@ class ResonanceAnalysis(Analysis):
|
|||
for _ in range(results_header, self.layout.rowCount()):
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
|
||||
if crossing:
|
||||
extended_data = []
|
||||
for m in crossing:
|
||||
start, lowest, end = m
|
||||
my_data = self._get_data(lowest)
|
||||
s11_low = self.app.data.s11[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(s11_low.freq)}"
|
||||
f" ({format_complex_imp(s11_low.impedance())})"))
|
||||
else:
|
||||
self.layout.addRow("Resonance", QtWidgets.QLabel(
|
||||
format_frequency(self.app.data.s11[lowest].freq)))
|
||||
self.layout.addWidget(QHLine())
|
||||
# Remove the final separator line
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
if filename and extended_data:
|
||||
with open(filename, 'w', encoding='utf-8', 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:
|
||||
if not crossing:
|
||||
self.layout.addRow(QtWidgets.QLabel(
|
||||
"No resonance found"))
|
||||
|
||||
extended_data = []
|
||||
for m in crossing:
|
||||
start, lowest, end = m
|
||||
my_data = self._get_data(lowest)
|
||||
s11_low = self.app.data.s11[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(s11_low.freq)}"
|
||||
f" ({format_complex_imp(s11_low.impedance())})"))
|
||||
else:
|
||||
self.layout.addRow("Resonance", QtWidgets.QLabel(
|
||||
format_frequency(self.app.data.s11[lowest].freq)))
|
||||
self.layout.addWidget(QHLine())
|
||||
# Remove the final separator line
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
if filename and extended_data:
|
||||
with open(filename, 'w', encoding='utf-8', newline='') as csvfile:
|
||||
fieldnames = extended_data[0].keys()
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
|
||||
writer.writeheader()
|
||||
for row in extended_data:
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
class EFHWAnalysis(ResonanceAnalysis):
|
||||
"""
|
||||
|
@ -152,7 +153,7 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
filename = None
|
||||
crossing = self._get_crossing()
|
||||
data = [d.impedance().real for d in self.app.data.s11]
|
||||
maximums = sorted(self.find_maximums(data, threshold=500))
|
||||
maximums = sorted(at.maxima(data, threshold=500))
|
||||
results_header = self.layout.indexOf(self.results_label)
|
||||
logger.debug("Results start at %d, out of %d",
|
||||
results_header, self.layout.rowCount())
|
||||
|
@ -160,47 +161,22 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
for _ in range(results_header, self.layout.rowCount()):
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
extended_data = {}
|
||||
both = []
|
||||
tolerance = 2
|
||||
for i, (low, _, high) in itertools.product(maximums, crossing):
|
||||
if low - tolerance <= i <= high + tolerance:
|
||||
both.append(i)
|
||||
continue
|
||||
if low > 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.data.s11[both[i]].freq))
|
||||
self.app.markers[i].frequencyInput.setText(
|
||||
str(self.app.data.s11[both[i]].freq))
|
||||
|
||||
else:
|
||||
logger.info("TO DO: find near data")
|
||||
for _, lowest, _ in crossing:
|
||||
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
|
||||
logger.info("TO DO: find near data")
|
||||
for lowest in crossing:
|
||||
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
|
||||
fields = [("freq", format_frequency_short),
|
||||
("r", format_resistence_neg), ("lambda", lambda x: round(x, 2))]
|
||||
|
||||
|
|
|
@ -31,80 +31,86 @@ logger = logging.getLogger(__name__)
|
|||
class SimplePeakSearchAnalysis(Analysis):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self._widget = QtWidgets.QWidget()
|
||||
outer_layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(outer_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.label['peak_freq'] = QtWidgets.QLabel()
|
||||
self.label['peak_db'] = QtWidgets.QLabel()
|
||||
|
||||
self.rbtn_data_s21_gain.setChecked(True)
|
||||
self.button = {
|
||||
'vswr': QtWidgets.QRadioButton("VSWR"),
|
||||
'resistance': QtWidgets.QRadioButton("Resistance"),
|
||||
'reactance': QtWidgets.QRadioButton("Reactance"),
|
||||
'gain': QtWidgets.QRadioButton("S21 Gain"),
|
||||
'peak_h': QtWidgets.QRadioButton("Highest value"),
|
||||
'peak_l': QtWidgets.QRadioButton("Lowest value"),
|
||||
'move_marker': QtWidgets.QCheckBox()
|
||||
}
|
||||
|
||||
self.rbtn_peak_group = QtWidgets.QButtonGroup()
|
||||
self.rbtn_peak_positive = QtWidgets.QRadioButton("Highest value")
|
||||
self.rbtn_peak_negative = QtWidgets.QRadioButton("Lowest value")
|
||||
self.rbtn_peak_group.addButton(self.rbtn_peak_positive)
|
||||
self.rbtn_peak_group.addButton(self.rbtn_peak_negative)
|
||||
self.button['gain'].setChecked(True)
|
||||
self.button['peak_h'].setChecked(True)
|
||||
|
||||
self.rbtn_peak_positive.setChecked(True)
|
||||
self.btn_group = {
|
||||
'data': QtWidgets.QButtonGroup(),
|
||||
'peak': QtWidgets.QButtonGroup(),
|
||||
}
|
||||
|
||||
self.checkbox_move_marker = QtWidgets.QCheckBox()
|
||||
for btn in ('vswr', 'resistance', 'reactance', 'gain'):
|
||||
self.btn_group['data'].addButton(self.button[btn])
|
||||
self.btn_group['peak'].addButton(self.button['peak_h'])
|
||||
self.btn_group['peak'].addButton(self.button['peak_l'])
|
||||
|
||||
outer_layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
|
||||
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(QHLine())
|
||||
outer_layout.addRow("Peak type", self.rbtn_peak_positive)
|
||||
outer_layout.addRow("", self.rbtn_peak_negative)
|
||||
outer_layout.addRow(QHLine())
|
||||
outer_layout.addRow("Move marker to peak", self.checkbox_move_marker)
|
||||
outer_layout.addRow(QHLine())
|
||||
layout = self.layout
|
||||
layout.addRow(self.label['titel'])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
|
||||
layout.addRow("Data source", self.button['vswr'])
|
||||
layout.addRow("", self.button['resistance'])
|
||||
layout.addRow("", self.button['reactance'])
|
||||
layout.addRow("", self.button['gain'])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow("Peak type", self.button['peak_h'])
|
||||
layout.addRow("", self.button['peak_l'])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow("Move marker to peak", self.button['move_marker'])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow(self.label['result'])
|
||||
layout.addRow("Peak frequency:", self.label['peak_freq'])
|
||||
layout.addRow("Peak value:", self.label['peak_db'])
|
||||
|
||||
outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
|
||||
|
||||
self.peak_frequency = QtWidgets.QLabel()
|
||||
self.peak_value = QtWidgets.QLabel()
|
||||
|
||||
outer_layout.addRow("Peak frequency:", self.peak_frequency)
|
||||
outer_layout.addRow("Peak value:", self.peak_value)
|
||||
self.set_titel('Simple peak search')
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s11:
|
||||
return
|
||||
|
||||
s11 = self.app.data.s11
|
||||
s21 = self.app.data.s21
|
||||
|
||||
if self.rbtn_data_vswr.isChecked():
|
||||
if not s21:
|
||||
self.button['gain'].setEnabled(False)
|
||||
if self.button['gain'].isChecked():
|
||||
self.button['vswr'].setChecked(True)
|
||||
else:
|
||||
self.button['gain'].setEnabled(True)
|
||||
|
||||
if self.button['vswr'].isChecked():
|
||||
suffix = ""
|
||||
data = [d.vswr for d in s11]
|
||||
elif self.rbtn_data_resistance.isChecked():
|
||||
elif self.button['resistance'].isChecked():
|
||||
suffix = " \N{OHM SIGN}"
|
||||
data = [d.impedance().real for d in s11]
|
||||
elif self.rbtn_data_reactance.isChecked():
|
||||
elif self.button['reactance'].isChecked():
|
||||
suffix = " \N{OHM SIGN}"
|
||||
data = [d.impedance().imag for d in s11]
|
||||
elif self.rbtn_data_s21_gain.isChecked():
|
||||
elif self.button['gain'].isChecked():
|
||||
suffix = " dB"
|
||||
data = [d.gain for d in s21]
|
||||
else:
|
||||
logger.warning("Searching for peaks on unknown data")
|
||||
return
|
||||
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
if self.rbtn_peak_positive.isChecked():
|
||||
if self.button['peak_h'].isChecked():
|
||||
idx_peak = np.argmax(data)
|
||||
elif self.rbtn_peak_negative.isChecked():
|
||||
elif self.button['peak_l'].isChecked():
|
||||
idx_peak = np.argmin(data)
|
||||
else:
|
||||
# Both is not yet in
|
||||
|
@ -113,12 +119,9 @@ class SimplePeakSearchAnalysis(Analysis):
|
|||
" but neither looking at positive nor negative?")
|
||||
return
|
||||
|
||||
self.peak_frequency.setText(
|
||||
format_frequency(self.app.data.s11[idx_peak].freq))
|
||||
self.peak_value.setText(str(round(data[idx_peak], 3)) + suffix)
|
||||
self.label['peak_freq'].setText(
|
||||
format_frequency(s11[idx_peak].freq))
|
||||
self.label['peak_db'].setText(f"{round(data[idx_peak], 3)}{suffix}")
|
||||
|
||||
if self.checkbox_move_marker.isChecked() and self.app.markers:
|
||||
self.app.markers[0].setFrequency(
|
||||
str(self.app.data.s11[idx_peak].freq))
|
||||
self.app.markers[0].frequencyInput.setText(
|
||||
format_frequency(self.app.data.s11[idx_peak].freq))
|
||||
if self.button['move_marker'].isChecked() and self.app.markers:
|
||||
self.app.markers[0].setFrequency(f"{s11[idx_peak].freq}")
|
||||
|
|
|
@ -23,7 +23,7 @@ from typing import Callable, List, Tuple
|
|||
import numpy as np
|
||||
import scipy
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
|
||||
|
||||
def zero_crossings(data: List[float]) -> List[int]:
|
||||
|
@ -146,3 +146,18 @@ def dip_cut_offs(gains: List[float], peak_gain: float,
|
|||
attn: float = 3.0) -> Tuple[int, int]:
|
||||
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
|
||||
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
|
||||
|
||||
|
||||
def calculate_rolloff(s21: List[Datapoint],
|
||||
idx_1: int, idx_2: int) -> Tuple[float, float]:
|
||||
if idx_1 == idx_2:
|
||||
return (math.nan, math.nan)
|
||||
freq_1 = s21[idx_1].freq
|
||||
freq_2 = s21[idx_2].freq
|
||||
gain1 = s21[idx_1].gain
|
||||
gain2 = s21[idx_2].gain
|
||||
factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1
|
||||
attn = abs(gain1 - gain2)
|
||||
octave_attn = attn / (math.log10(factor) / math.log10(2))
|
||||
decade_attn = attn / math.log10(factor)
|
||||
return (octave_attn, decade_attn)
|
||||
|
|
|
@ -433,7 +433,6 @@ class FrequencyChart(Chart):
|
|||
m = self.getActiveMarker()
|
||||
if m is not None:
|
||||
m.setFrequency(str(f))
|
||||
m.frequencyInput.setText(str(f))
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
self.dim.width = (
|
||||
|
|
|
@ -141,7 +141,6 @@ class SquareChart(Chart):
|
|||
minimum_position = positions.index(min(positions))
|
||||
if m := self.getActiveMarker():
|
||||
m.setFrequency(str(round(target[minimum_position].freq)))
|
||||
m.frequencyInput.setText(str(round(target[minimum_position].freq)))
|
||||
|
||||
def getXPosition(self, d: Datapoint) -> int:
|
||||
return int(self.width() / 2 + d.re * self.dim.width / 2)
|
||||
|
|
|
@ -212,6 +212,7 @@ class Marker(QtCore.QObject, Value):
|
|||
|
||||
def setFrequency(self, frequency):
|
||||
self.freq = parse_frequency(frequency)
|
||||
self.frequencyInput.setText(frequency)
|
||||
self.updated.emit(self)
|
||||
|
||||
def setFieldSelection(self, fields):
|
||||
|
|
Ładowanie…
Reference in New Issue