Feature/v0.5.4 pre (#548)

* Simplified VSWR analysis
* Split history VSWRAnalysis.py to ResonanceAnalysis.py
* simplified BandPassAnalysis
pull/553/head
Holger Müller 2022-09-18 18:03:52 +02:00 zatwierdzone przez GitHub
rodzic 6630568ed9
commit 879d5ddea3
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
16 zmienionych plików z 713 dodań i 782 usunięć

1
.gitignore vendored
Wyświetl plik

@ -1,6 +1,7 @@
/venv/ /venv/
/env/ /env/
.idea/ .idea/
.tox/
.vscode/ .vscode/
/build/ /build/
/dist/ /dist/

Wyświetl plik

@ -1,6 +1,9 @@
Changelog Changelog
========= =========
0.5.4
-----
0.5.3 0.5.3
----- -----

Wyświetl plik

@ -18,15 +18,18 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import math import math
from typing import Dict
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_frequency from NanoVNASaver.Formatting import format_frequency
from NanoVNASaver.Analysis import Analysis
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CUTOFF_VALS = (3.0, 6.0, 10.0, 20.0, 60.0)
class BandPassAnalysis(Analysis): class BandPassAnalysis(Analysis):
def __init__(self, app): def __init__(self, app):
@ -41,344 +44,149 @@ class BandPassAnalysis(Analysis):
QtWidgets.QLabel( QtWidgets.QLabel(
f"Please place {self.app.markers[0].name}" f"Please place {self.app.markers[0].name}"
f" in the filter passband.")) f" in the filter passband."))
self.result_label = QtWidgets.QLabel() self.label = {
self.lower_cutoff_label = QtWidgets.QLabel() label: QtWidgets.QLabel() for label in
self.lower_six_db_label = QtWidgets.QLabel() ('result', 'octave_l', 'octave_r', 'decade_l', 'decade_r',
self.lower_sixty_db_label = QtWidgets.QLabel() 'freq_center', 'span_3.0dB', 'span_6.0dB', 'q_factor')
self.lower_db_per_octave_label = QtWidgets.QLabel() }
self.lower_db_per_decade_label = QtWidgets.QLabel() for attn in CUTOFF_VALS:
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
self.upper_cutoff_label = QtWidgets.QLabel() self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
self.upper_six_db_label = QtWidgets.QLabel()
self.upper_sixty_db_label = QtWidgets.QLabel()
self.upper_db_per_octave_label = QtWidgets.QLabel()
self.upper_db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow("Result:", self.label['result'])
layout.addRow(QtWidgets.QLabel("")) layout.addRow(QtWidgets.QLabel(""))
self.center_frequency_label = QtWidgets.QLabel() layout.addRow("Center frequency:", self.label['freq_center'])
self.span_label = QtWidgets.QLabel() layout.addRow("Bandwidth (-3 dB):", self.label['span_3.0dB'])
self.six_db_span_label = QtWidgets.QLabel() layout.addRow("Quality factor:", self.label['q_factor'])
self.quality_label = QtWidgets.QLabel() layout.addRow("Bandwidth (-6 dB):", self.label['span_6.0dB'])
layout.addRow("Center frequency:", self.center_frequency_label)
layout.addRow("Bandwidth (-3 dB):", self.span_label)
layout.addRow("Quality factor:", self.quality_label)
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
layout.addRow(QtWidgets.QLabel("")) layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:")) layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.lower_cutoff_label) layout.addRow("Cutoff frequency:", self.label['3.0dB_l'])
layout.addRow("-6 dB point:", self.lower_six_db_label) layout.addRow("-6 dB point:", self.label['6.0dB_l'])
layout.addRow("-60 dB point:", self.lower_sixty_db_label) layout.addRow("-60 dB point:", self.label['60.0dB_l'])
layout.addRow("Roll-off:", self.lower_db_per_octave_label) layout.addRow("Roll-off:", self.label['octave_l'])
layout.addRow("Roll-off:", self.lower_db_per_decade_label) layout.addRow("Roll-off:", self.label['decade_l'])
layout.addRow(QtWidgets.QLabel("")) layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:")) layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.upper_cutoff_label) layout.addRow("Cutoff frequency:", self.label['3.0dB_r'])
layout.addRow("-6 dB point:", self.upper_six_db_label) layout.addRow("-6 dB point:", self.label['6.0dB_r'])
layout.addRow("-60 dB point:", self.upper_sixty_db_label) layout.addRow("-60 dB point:", self.label['60.0dB_r'])
layout.addRow("Roll-off:", self.upper_db_per_octave_label) layout.addRow("Roll-off:", self.label['octave_r'])
layout.addRow("Roll-off:", self.upper_db_per_decade_label) layout.addRow("Roll-off:", self.label['decade_r'])
def reset(self): def reset(self):
self.result_label.clear() for label in self.label.values():
self.center_frequency_label.clear() label.clear()
self.span_label.clear()
self.quality_label.clear()
self.six_db_span_label.clear()
self.upper_cutoff_label.clear()
self.upper_six_db_label.clear()
self.upper_sixty_db_label.clear()
self.upper_db_per_octave_label.clear()
self.upper_db_per_decade_label.clear()
self.lower_cutoff_label.clear()
self.lower_six_db_label.clear()
self.lower_sixty_db_label.clear()
self.lower_db_per_octave_label.clear()
self.lower_db_per_decade_label.clear()
def runAnalysis(self): def runAnalysis(self):
self.reset() if not self.app.data.s21:
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") logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.") self.label['result'].setText("No data to analyse.")
return return
if pass_band_location < 0: self.reset()
logger.debug("No location for %s", self.app.markers[0].name) s21 = self.app.data.s21
self.result_label.setText( gains = [d.gain for d in s21]
f"Please place {self.app.markers[0].name} in the passband.")
marker = self.app.markers[0]
if marker.location <= 0 or marker.location >= len(s21) - 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.")
return return
pass_band_db = self.app.data.s21[pass_band_location].gain # find center of passband based on marker pos
if (peak := at.center_from_idx(gains, marker.location)) < 0:
logger.debug("Initial passband gain: %d", pass_band_db) self.label['result'].setText("Bandpass center not found")
initial_lower_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found a cutoff location
initial_lower_cutoff_location = i
break
if initial_lower_cutoff_location < 0:
self.result_label.setText("Lower cutoff location not found.")
return return
peak_db = gains[peak]
logger.debug("Bandpass center pos: %d(%fdB)", peak, peak_db)
initial_lower_cutoff_frequency = ( # find passband bounderies
self.app.data.s21[initial_lower_cutoff_location].freq) cutoff_pos = {}
for attn in CUTOFF_VALS:
cutoff_pos[f"{attn:.1f}dB_l"] = at.cut_off_left(
gains, peak, peak_db, attn)
cutoff_pos[f"{attn:.1f}dB_r"] = at.cut_off_right(
gains, peak, peak_db, attn)
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("Found initial lower cutoff frequency at %d", self.derive_60dB(cutoff_pos, cutoff_freq)
initial_lower_cutoff_frequency)
initial_upper_cutoff_location = -1 result = {
for i in range(pass_band_location, len(self.app.data.s21), 1): 'span_3.0dB': cutoff_freq['3.0dB_r'] - cutoff_freq['3.0dB_l'],
if (pass_band_db - self.app.data.s21[i].gain) > 3: 'span_6.0dB': cutoff_freq['6.0dB_r'] - cutoff_freq['6.0dB_l'],
# We found a cutoff location 'freq_center': int(
initial_upper_cutoff_location = i math.sqrt(cutoff_freq['3.0dB_l'] * cutoff_freq['3.0dB_r'])),
break }
result['q_factor'] = result['freq_center'] / result['span_3.0dB']
if initial_upper_cutoff_location < 0: result['octave_l'], result['decade_l'] = self.calculateRolloff(
self.result_label.setText("Upper cutoff location not found.") cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"])
return result['octave_r'], result['decade_r'] = self.calculateRolloff(
cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"])
initial_upper_cutoff_frequency = ( for label, val in cutoff_freq.items():
self.app.data.s21[initial_upper_cutoff_location].freq) self.label[label].setText(
f"{format_frequency(val)}"
f" ({cutoff_gain[label]:.1f} dB)")
for label in ('freq_center', 'span_3.0dB', 'span_6.0dB'):
self.label[label].setText(format_frequency(result[label]))
self.label['q_factor'].setText(f"{result['q_factor']:.2f}")
logger.debug("Found initial upper cutoff frequency at %d", for label in ('octave_l', 'decade_l', 'octave_r', 'decade_r'):
initial_upper_cutoff_frequency) self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
peak_location = -1 self.app.markers[0].setFrequency(f"{result['freq_center']}")
peak_db = self.app.data.s21[initial_lower_cutoff_location].gain self.app.markers[0].frequencyInput.setText(f"{result['freq_center']}")
for i in range(initial_lower_cutoff_location, self.app.markers[1].setFrequency(f"{cutoff_freq['3.0dB_l']}")
initial_upper_cutoff_location, 1): self.app.markers[1].frequencyInput.setText(f"{cutoff_freq['3.0dB_l']}")
db = self.app.data.s21[i].gain self.app.markers[2].setFrequency(f"{cutoff_freq['3.0dB_r']}")
if db > peak_db: self.app.markers[2].frequencyInput.setText(f"{cutoff_freq['3.0dB_r']}")
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, if cutoff_gain['3.0dB_l'] < -4 or cutoff_gain['3.0dB_r'] < -4:
self.app.data.s11[peak_location].freq) logger.warning(
"Data points insufficient for true -3 dB points."
lower_cutoff_location = -1 "Cutoff gains: %fdB, %fdB", cutoff_gain['3.0dB_l'], cutoff_gain['3.0dB_r'])
pass_band_db = peak_db self.label['result'].setText(
for i in range(peak_location, -1, -1): f"Analysis complete ({len(s21)} points)\n"
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = (
self.app.data.s21[lower_cutoff_location].freq)
lower_cutoff_gain = (
self.app.data.s21[lower_cutoff_location].gain - pass_band_db)
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
lower_cutoff_gain)
logger.debug("Found true lower cutoff frequency at %d",
lower_cutoff_frequency)
self.lower_cutoff_label.setText(
f"{format_frequency(lower_cutoff_frequency)}"
f" ({round(lower_cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(lower_cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
upper_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data.s21[upper_cutoff_location].freq
upper_cutoff_gain = (
self.app.data.s21[upper_cutoff_location].gain - pass_band_db)
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
logger.debug("Found true upper cutoff frequency at %d",
upper_cutoff_frequency)
self.upper_cutoff_label.setText(
f"{format_frequency(upper_cutoff_frequency)}"
f" ({round(upper_cutoff_gain, 1)} dB)")
self.app.markers[2].setFrequency(str(upper_cutoff_frequency))
self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency))
span = upper_cutoff_frequency - lower_cutoff_frequency
center_frequency = math.sqrt(
lower_cutoff_frequency * upper_cutoff_frequency)
q = center_frequency / span
self.span_label.setText(format_frequency(span))
self.center_frequency_label.setText(
format_frequency(center_frequency))
self.quality_label.setText(str(round(q, 2)))
self.app.markers[0].setFrequency(str(round(center_frequency)))
self.app.markers[0].frequencyInput.setText(
str(round(center_frequency)))
# Lower roll-off
lower_six_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
if lower_six_db_location < 0:
self.result_label.setText("Lower 6 dB location not found.")
return
lower_six_db_cutoff_frequency = (
self.app.data.s21[lower_six_db_location].freq)
self.lower_six_db_label.setText(
format_frequency(lower_six_db_cutoff_frequency))
ten_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_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.lower_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.lower_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.lower_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.lower_db_per_octave_label.setText(
str(round(octave_attenuation, 3)) + " dB / octave")
self.lower_db_per_decade_label.setText(
str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.lower_db_per_octave_label.setText("Not calculated")
self.lower_db_per_decade_label.setText("Not calculated")
# Upper roll-off
upper_six_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
if upper_six_db_location < 0:
self.result_label.setText("Upper 6 dB location not found.")
return
upper_six_db_cutoff_frequency = (
self.app.data.s21[upper_six_db_location].freq)
self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency))
six_db_span = (
upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency)
self.six_db_span_label.setText(
format_frequency(six_db_span))
ten_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, len(self.app.data.s21), 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:
sixty_db_cutoff_frequency = (
self.app.data.s21[sixty_db_location].freq)
self.upper_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.upper_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.upper_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.upper_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.upper_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.upper_db_per_octave_label.setText("Not calculated")
self.upper_db_per_decade_label.setText("Not calculated")
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
self.result_label.setText(
f"Analysis complete ({len(self.app.data.s11)} points)\n"
f"Insufficient data for analysis. Increase segment count.") f"Insufficient data for analysis. Increase segment count.")
else: return
self.result_label.setText( self.label['result'].setText(
f"Analysis complete ({len(self.app.data.s11)} points)") f"Analysis complete ({len(s21)} points)")
def derive_60dB(self,
cutoff_pos: Dict[str, int],
cutoff_freq: Dict[str, float]):
"""derive 60dB cutoff if needed an possible
Args:
cutoff_pos (Dict[str, int])
cutoff_freq (Dict[str, float])
"""
if (math.isnan(cutoff_freq['60.0dB_l']) and
cutoff_pos['20.0dB_l'] != -1 and cutoff_pos['10.0dB_l'] != -1):
cutoff_freq['60.0dB_l'] = (
cutoff_freq["10.0dB_l"] *
10 ** (5 * (math.log10(cutoff_pos['20.0dB_l']) -
math.log10(cutoff_pos['10.0dB_l']))))
if (math.isnan(cutoff_freq['60.0dB_r']) and
cutoff_pos['20.0dB_r'] != -1 and cutoff_pos['10.0dB_r'] != -1):
cutoff_freq['60.0dB_r'] = (
cutoff_freq["10.0dB_r"] *
10 ** (5 * (math.log10(cutoff_pos['20.0dB_r']) -
math.log10(cutoff_pos['10.0dB_r'])
)))

Wyświetl plik

@ -21,7 +21,7 @@ import math
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_frequency from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -100,34 +100,28 @@ class BandStopAnalysis(Analysis):
def runAnalysis(self): def runAnalysis(self):
self.reset() self.reset()
if not self.app.data.s21:
if len(self.app.data.s21) == 0:
logger.debug("No data to analyse") logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.") self.result_label.setText("No data to analyse.")
return return
peak_location = -1 s21 = self.app.data.s21
peak_db = self.app.data.s21[0].gain
for i in range(len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
peak_location, peak = max(enumerate(s21), key=lambda i: i[1].gain)
logger.debug("Found peak of %f at %d", logger.debug("Found peak of %f at %d",
peak_db, self.app.data.s11[peak_location].freq) peak.gain, peak.freq)
pass_band_db = peak.gain
lower_cutoff_location = -1 lower_cutoff_location = -1
pass_band_db = peak_db for i in range(len(s21)):
for i in range(len(self.app.data.s21)): if (pass_band_db - s21[i].gain) > 3:
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location # We found the cutoff location
lower_cutoff_location = i lower_cutoff_location = i
break break
lower_cutoff_frequency = self.app.data.s21[lower_cutoff_location].freq lower_cutoff_frequency = s21[lower_cutoff_location].freq
lower_cutoff_gain = ( lower_cutoff_gain = (
self.app.data.s21[lower_cutoff_location].gain - pass_band_db) s21[lower_cutoff_location].gain - pass_band_db)
if lower_cutoff_gain < -4: if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB" logger.debug("Lower cutoff frequency found at %f dB"
@ -145,16 +139,16 @@ class BandStopAnalysis(Analysis):
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency)) self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
upper_cutoff_location = -1 upper_cutoff_location = -1
for i in range(len(self.app.data.s21)-1, -1, -1): for i in range(len(s21)-1, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3: if (pass_band_db - s21[i].gain) > 3:
# We found the cutoff location # We found the cutoff location
upper_cutoff_location = i upper_cutoff_location = i
break break
upper_cutoff_frequency = ( upper_cutoff_frequency = (
self.app.data.s21[upper_cutoff_location].freq) s21[upper_cutoff_location].freq)
upper_cutoff_gain = ( upper_cutoff_gain = (
self.app.data.s21[upper_cutoff_location].gain - pass_band_db) s21[upper_cutoff_location].gain - pass_band_db)
if upper_cutoff_gain < -4: if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB" logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.", " - insufficient data points for true -3 dB point.",
@ -186,8 +180,8 @@ class BandStopAnalysis(Analysis):
# Lower roll-off # Lower roll-off
lower_six_db_location = -1 lower_six_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)): for i in range(lower_cutoff_location, len(s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 6: if (pass_band_db - s21[i].gain) > 6:
# We found 6dB location # We found 6dB location
lower_six_db_location = i lower_six_db_location = i
break break
@ -196,39 +190,39 @@ class BandStopAnalysis(Analysis):
self.result_label.setText("Lower 6 dB location not found.") self.result_label.setText("Lower 6 dB location not found.")
return return
lower_six_db_cutoff_frequency = ( lower_six_db_cutoff_frequency = (
self.app.data.s21[lower_six_db_location].freq) s21[lower_six_db_location].freq)
self.lower_six_db_label.setText( self.lower_six_db_label.setText(
format_frequency(lower_six_db_cutoff_frequency)) format_frequency(lower_six_db_cutoff_frequency))
ten_db_location = -1 ten_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)): for i in range(lower_cutoff_location, len(s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 10: if (pass_band_db - s21[i].gain) > 10:
# We found 6dB location # We found 6dB location
ten_db_location = i ten_db_location = i
break break
twenty_db_location = -1 twenty_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)): for i in range(lower_cutoff_location, len(s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 20: if (pass_band_db - s21[i].gain) > 20:
# We found 6dB location # We found 6dB location
twenty_db_location = i twenty_db_location = i
break break
sixty_db_location = -1 sixty_db_location = -1
for i in range(lower_six_db_location, len(self.app.data.s21)): for i in range(lower_six_db_location, len(s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 60: if (pass_band_db - s21[i].gain) > 60:
# We found 60dB location! Wow. # We found 60dB location! Wow.
sixty_db_location = i sixty_db_location = i
break break
if sixty_db_location > 0: if sixty_db_location > 0:
sixty_db_cutoff_frequency = ( sixty_db_cutoff_frequency = (
self.app.data.s21[sixty_db_location].freq) s21[sixty_db_location].freq)
self.lower_sixty_db_label.setText( self.lower_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency)) format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1: elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq ten = s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq twenty = s21[twenty_db_location].freq
sixty_db_frequency = ten * \ sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten))) 10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.lower_sixty_db_label.setText( self.lower_sixty_db_label.setText(
@ -253,7 +247,7 @@ class BandStopAnalysis(Analysis):
upper_six_db_location = -1 upper_six_db_location = -1
for i in range(upper_cutoff_location, -1, -1): for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 6: if (pass_band_db - s21[i].gain) > 6:
# We found 6dB location # We found 6dB location
upper_six_db_location = i upper_six_db_location = i
break break
@ -262,7 +256,7 @@ class BandStopAnalysis(Analysis):
self.result_label.setText("Upper 6 dB location not found.") self.result_label.setText("Upper 6 dB location not found.")
return return
upper_six_db_cutoff_frequency = ( upper_six_db_cutoff_frequency = (
self.app.data.s21[upper_six_db_location].freq) s21[upper_six_db_location].freq)
self.upper_six_db_label.setText( self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency)) format_frequency(upper_six_db_cutoff_frequency))
@ -274,33 +268,33 @@ class BandStopAnalysis(Analysis):
ten_db_location = -1 ten_db_location = -1
for i in range(upper_cutoff_location, -1, -1): for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 10: if (pass_band_db - s21[i].gain) > 10:
# We found 6dB location # We found 6dB location
ten_db_location = i ten_db_location = i
break break
twenty_db_location = -1 twenty_db_location = -1
for i in range(upper_cutoff_location, -1, -1): for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 20: if (pass_band_db - s21[i].gain) > 20:
# We found 6dB location # We found 6dB location
twenty_db_location = i twenty_db_location = i
break break
sixty_db_location = -1 sixty_db_location = -1
for i in range(upper_six_db_location, -1, -1): for i in range(upper_six_db_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 60: if (pass_band_db - s21[i].gain) > 60:
# We found 60dB location! Wow. # We found 60dB location! Wow.
sixty_db_location = i sixty_db_location = i
break break
if sixty_db_location > 0: if sixty_db_location > 0:
sixty_db_cutoff_frequency = ( sixty_db_cutoff_frequency = (
self.app.data.s21[sixty_db_location].freq) s21[sixty_db_location].freq)
self.upper_sixty_db_label.setText( self.upper_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency)) format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1: elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq ten = s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq twenty = s21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** ( sixty_db_frequency = ten * 10 ** (
5 * (math.log10(twenty) - math.log10(ten))) 5 * (math.log10(twenty) - math.log10(ten)))
self.upper_sixty_db_label.setText( self.upper_sixty_db_label.setText(

Wyświetl plik

@ -18,13 +18,22 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import math import math
from typing import Tuple
import numpy as np import numpy as np
import scipy
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from scipy import signal
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
class Analysis: class Analysis:
_widget = None _widget = None
@ -109,7 +118,7 @@ class Analysis:
:param data: list of values :param data: list of values
:param threshold: :param threshold:
""" """
peaks, _ = signal.find_peaks( peaks, _ = scipy.signal.find_peaks(
data, width=2, distance=3, prominence=1) data, width=2, distance=3, prominence=1)
# my_data = np.array(data) # my_data = np.array(data)
@ -130,21 +139,18 @@ class Analysis:
def reset(self): def reset(self):
pass pass
def calculateRolloff(self, location1, location2): def calculateRolloff(self, idx_1: int, idx_2: int) -> Tuple[float, float]:
if location1 == location2: if idx_1 == idx_2:
return 0, 0 return (math.nan, math.nan)
frequency1 = self.app.data.s21[location1].freq s21 = self.app.data.s21
frequency2 = self.app.data.s21[location2].freq freq_1 = s21[idx_1].freq
gain1 = self.app.data.s21[location1].gain freq_2 = s21[idx_2].freq
gain2 = self.app.data.s21[location2].gain gain1 = s21[idx_1].gain
frequency_factor = frequency2 / frequency1 gain2 = s21[idx_2].gain
if frequency_factor < 1: factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1
frequency_factor = 1 / frequency_factor attn = abs(gain1 - gain2)
attenuation = abs(gain1 - gain2) logger.debug("Measured points: %d Hz and %d Hz\n%fdB over %f factor",
logger.debug("Measured points: %d Hz and %d Hz", freq_1, freq_2, attn, factor)
frequency1, frequency2) octave_attn = attn / (math.log10(factor) / math.log10(2))
logger.debug("%f dB over %f factor", attenuation, frequency_factor) decade_attn = attn / math.log10(factor)
octave_attenuation = attenuation / \ return (octave_attn, decade_attn)
(math.log10(frequency_factor) / math.log10(2))
decade_attenuation = attenuation / math.log10(frequency_factor)
return octave_attenuation, decade_attenuation

Wyświetl plik

@ -21,7 +21,7 @@ import math
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_frequency from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

Wyświetl plik

@ -21,7 +21,7 @@ import math
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_frequency from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

Wyświetl plik

@ -19,10 +19,10 @@
import logging import logging
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from scipy import signal import scipy
import numpy as np import numpy as np
from NanoVNASaver.Analysis import Analysis from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_vswr from NanoVNASaver.Formatting import format_vswr
from NanoVNASaver.Formatting import format_gain from NanoVNASaver.Formatting import format_gain
from NanoVNASaver.Formatting import format_resistance from NanoVNASaver.Formatting import format_resistance
@ -112,17 +112,17 @@ class PeakSearchAnalysis(Analysis):
return return
if self.rbtn_peak_positive.isChecked(): if self.rbtn_peak_positive.isChecked():
peaks, _ = signal.find_peaks( peaks, _ = scipy.signal.find_peaks(
data, width=3, distance=3, prominence=1) data, width=3, distance=3, prominence=1)
elif self.rbtn_peak_negative.isChecked(): elif self.rbtn_peak_negative.isChecked():
sign = -1 sign = -1
data = [x * sign for x in data] data = [x * sign for x in data]
peaks, _ = signal.find_peaks( peaks, _ = scipy.signal.find_peaks(
data, width=3, distance=3, prominence=1) data, width=3, distance=3, prominence=1)
# elif self.rbtn_peak_both.isChecked(): # elif self.rbtn_peak_both.isChecked():
# peaks_max, _ = signal.find_peaks( # peaks_max, _ = scipy.signal.find_peaks(
# data, width=3, distance=3, prominence=1) # data, width=3, distance=3, prominence=1)
# peaks_min, _ = signal.find_peaks( # peaks_min, _ = scipy.signal.find_peaks(
# np.array(data)*-1, width=3, distance=3, prominence=1) # np.array(data)*-1, width=3, distance=3, prominence=1)
# peaks = np.concatenate((peaks_max, peaks_min)) # peaks = np.concatenate((peaks_max, peaks_min))
else: else:
@ -136,7 +136,7 @@ class PeakSearchAnalysis(Analysis):
for i, p in np.ndenumerate(peaks): for i, p in np.ndenumerate(peaks):
logger.debug("Peak %i at %d", i, p) logger.debug("Peak %i at %d", i, p)
prominences = signal.peak_prominences(data, peaks)[0] prominences = scipy.signal.peak_prominences(data, peaks)[0]
logger.debug("%d prominences", len(prominences)) logger.debug("%d prominences", len(prominences))
# Find the peaks with the most extreme values # Find the peaks with the most extreme values

Wyświetl plik

@ -0,0 +1,316 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import csv
import itertools
import logging
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis.Base import Analysis, QHLine
from NanoVNASaver.Formatting import (
format_frequency, format_complex_imp,
format_frequency_short, format_resistance)
from NanoVNASaver.RFTools import reflection_coefficient
logger = logging.getLogger(__name__)
def format_resistence_neg(x):
return format_resistance(x, allow_negative=True)
def vswr_transformed(z, ratio=49) -> float:
refl = reflection_coefficient(z / ratio)
mag = abs(refl)
return 1 if mag == 1 else (1 + mag) / (1 - mag)
class ResonanceAnalysis(Analysis):
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("<b>Settings</b>"))
self.layout.addRow("Description", self.input_description)
self.layout.addRow(QHLine())
self.layout.addRow(QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
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,
}
my_data["vswr_49"] = vswr_transformed(
my_data["impedance"], 49)
my_data["vswr_4"] = 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 = [d.phase for d in self.app.data.s11]
return sorted(self.find_crossing_zero(data))
def runAnalysis(self):
self.reset()
filename = (
os.path.join("/tmp/", f"{self.input_description.text()}.csv")
if self.input_description.text()
else 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 _ 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:
self.layout.addRow(QtWidgets.QLabel(
"No resonance found"))
class EFHWAnalysis(ResonanceAnalysis):
"""
find only resonance when HI impedance
"""
old_data = []
def reset(self):
logger.debug("reset")
def runAnalysis(self):
self.reset()
if description := self.input_description.text():
filename = os.path.join("/tmp/", f"{description}.csv")
else:
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))
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d",
results_header, self.layout.rowCount())
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
fields = [("freq", format_frequency_short),
("r", format_resistence_neg), ("lambda", lambda x: round(x, 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(sorted(extended_data.keys())):
s11_idx = self.app.data.s11[index]
self.layout.addRow(
f"{format_frequency_short(s11_idx.freq)}",
QtWidgets.QLabel(
f" ({diff[i]['freq']})"
f" {format_complex_imp(s11_idx.impedance())}"
f" ({diff[i]['r']}) {diff[i]['lambda']} m"))
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)
def compare(self, old, new, fields=None):
"""
Compare data to help changes
NB
must be same sweep
( same index must be same frequence )
:param old:
:param new:
"""
fields = fields or [("freq", str), ]
def no_compare():
return {k: "-" for k, _ in fields}
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))
if i_max == i_tot:
logger.debug("may be the same antenna ... analyzing")
else:
logger.warning("resonances changed from %s to %s",
len(old_idx), len(new_idx))
logger.debug("Trying to compare only first %s resonances", i_max)
split = 0
max_delta_f = 1000000 # 1M
for i, k in enumerate(new_idx):
my_diff = {}
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("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
diff[i] = no_compare()
return diff

Wyświetl plik

@ -21,7 +21,7 @@ import logging
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
import numpy as np import numpy as np
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis from NanoVNASaver.Analysis.Base import Analysis, QHLine
from NanoVNASaver.Formatting import format_frequency from NanoVNASaver.Formatting import format_frequency
@ -62,12 +62,12 @@ class SimplePeakSearchAnalysis(Analysis):
outer_layout.addRow("", self.rbtn_data_resistance) outer_layout.addRow("", self.rbtn_data_resistance)
outer_layout.addRow("", self.rbtn_data_reactance) outer_layout.addRow("", self.rbtn_data_reactance)
outer_layout.addRow("", self.rbtn_data_s21_gain) outer_layout.addRow("", self.rbtn_data_s21_gain)
outer_layout.addRow(PeakSearchAnalysis.QHLine()) outer_layout.addRow(QHLine())
outer_layout.addRow("Peak type", self.rbtn_peak_positive) outer_layout.addRow("Peak type", self.rbtn_peak_positive)
outer_layout.addRow("", self.rbtn_peak_negative) outer_layout.addRow("", self.rbtn_peak_negative)
outer_layout.addRow(PeakSearchAnalysis.QHLine()) outer_layout.addRow(QHLine())
outer_layout.addRow("Move marker to peak", self.checkbox_move_marker) outer_layout.addRow("Move marker to peak", self.checkbox_move_marker)
outer_layout.addRow(PeakSearchAnalysis.QHLine()) outer_layout.addRow(QHLine())
outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>")) outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
@ -78,26 +78,23 @@ class SimplePeakSearchAnalysis(Analysis):
outer_layout.addRow("Peak value:", self.peak_value) outer_layout.addRow("Peak value:", self.peak_value)
def runAnalysis(self): 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 self.rbtn_data_vswr.isChecked():
suffix = "" suffix = ""
data = [] data = [d.vswr for d in s11]
for d in self.app.data.s11:
data.append(d.vswr)
elif self.rbtn_data_resistance.isChecked(): elif self.rbtn_data_resistance.isChecked():
suffix = " \N{OHM SIGN}" suffix = " \N{OHM SIGN}"
data = [] data = [d.impedance().real for d in s11]
for d in self.app.data.s11:
data.append(d.impedance().real)
elif self.rbtn_data_reactance.isChecked(): elif self.rbtn_data_reactance.isChecked():
suffix = " \N{OHM SIGN}" suffix = " \N{OHM SIGN}"
data = [] data = [d.impedance().imag for d in s11]
for d in self.app.data.s11:
data.append(d.impedance().imag)
elif self.rbtn_data_s21_gain.isChecked(): elif self.rbtn_data_s21_gain.isChecked():
suffix = " dB" suffix = " dB"
data = [] data = [d.gain for d in s21]
for d in self.app.data.s21:
data.append(d.gain)
else: else:
logger.warning("Searching for peaks on unknown data") logger.warning("Searching for peaks on unknown data")
return return

Wyświetl plik

@ -16,40 +16,22 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import csv
import itertools
import logging import logging
from typing import List
import numpy as np
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Formatting import ( from NanoVNASaver.Analysis.Base import Analysis, QHLine
format_frequency, format_complex_imp, from NanoVNASaver.Formatting import format_frequency, format_vswr
format_frequency_short, format_resistance)
from NanoVNASaver.RFTools import reflection_coefficient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def round_2(x):
return round(x, 2)
def format_resistence_neg(x):
return format_resistance(x, allow_negative=True)
class VSWRAnalysis(Analysis): class VSWRAnalysis(Analysis):
max_dips_shown = 3 max_dips_shown = 3
vswr_limit_value = 1.5 vswr_limit_value = 1.5
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app): def __init__(self, app):
super().__init__(app) super().__init__(app)
@ -58,7 +40,7 @@ class VSWRAnalysis(Analysis):
self._widget.setLayout(self.layout) self._widget.setLayout(self.layout)
self.input_vswr_limit = QtWidgets.QDoubleSpinBox() self.input_vswr_limit = QtWidgets.QDoubleSpinBox()
self.input_vswr_limit.setValue(self.vswr_limit_value) self.input_vswr_limit.setValue(VSWRAnalysis.vswr_limit_value)
self.input_vswr_limit.setSingleStep(0.1) self.input_vswr_limit.setSingleStep(0.1)
self.input_vswr_limit.setMinimum(1) self.input_vswr_limit.setMinimum(1)
self.input_vswr_limit.setMaximum(25) self.input_vswr_limit.setMaximum(25)
@ -67,140 +49,24 @@ class VSWRAnalysis(Analysis):
self.checkbox_move_marker = QtWidgets.QCheckBox() self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>")) self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("VSWR limit", self.input_vswr_limit) self.layout.addRow("VSWR limit", self.input_vswr_limit)
self.layout.addRow(VSWRAnalysis.QHLine()) self.layout.addRow(QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>") self.results_label = QtWidgets.QLabel("<b>Results</b>")
self.layout.addRow(self.results_label) self.layout.addRow(self.results_label)
self.minimums: List[int] = []
def runAnalysis(self): def runAnalysis(self):
max_dips_shown = self.max_dips_shown if not self.app.data.s11:
data = [d.vswr for d in self.app.data.s11] return
s11 = self.app.data.s11
data = [d.vswr for d in s11]
threshold = self.input_vswr_limit.value() threshold = self.input_vswr_limit.value()
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())
for _ in range(results_header, self.layout.rowCount()): minima = sorted(at.minima(data, threshold),
self.layout.removeRow(self.layout.rowCount() - 1) key=lambda i: data[i])[:VSWRAnalysis.max_dips_shown]
if len(minimums) > max_dips_shown: self.minimums = minima
self.layout.addRow(
QtWidgets.QLabel(
f"<b>More than {str(max_dips_shown)} dips found."
" Lowest shown.</b>"))
dips = []
for m in minimums:
start, lowest, end = m
dips.append(data[lowest])
best_dips = []
for _ in range(max_dips_shown):
min_idx = np.argmin(dips)
best_dips.append(minimums[min_idx])
dips.remove(dips[min_idx])
minimums.remove(minimums[min_idx])
minimums = best_dips
self.minimums = minimums
if len(minimums) > 0:
for m in minimums:
start, lowest, end = m
if start != end:
logger.debug(
"Section from %d to %d, lowest at %d",
start, end, lowest)
self.layout.addRow("Start", QtWidgets.QLabel(
format_frequency(self.app.data.s11[start].freq)))
self.layout.addRow("Minimum", QtWidgets.QLabel(
f"{format_frequency(self.app.data.s11[lowest].freq)}"
f" ({round(data[lowest], 2)})"))
self.layout.addRow("End", QtWidgets.QLabel(
format_frequency(self.app.data.s11[end].freq)))
self.layout.addRow(
"Span", QtWidgets.QLabel(format_frequency(
(self.app.data.s11[end].freq -
self.app.data.s11[start].freq))))
else:
self.layout.addRow("Low spot", QtWidgets.QLabel(
format_frequency(self.app.data.s11[lowest].freq)))
self.layout.addWidget(PeakSearchAnalysis.QHLine())
self.layout.removeRow(self.layout.rowCount() - 1)
else:
self.layout.addRow(
QtWidgets.QLabel(
f"No areas found with VSWR below {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)
return 1 if mag == 1 else (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("<b>Settings</b>"))
self.layout.addRow("Description", self.input_description)
self.layout.addRow(VSWRAnalysis.QHLine())
self.layout.addRow(VSWRAnalysis.QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
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,
}
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 = [d.phase for d in self.app.data.s11]
return sorted(self.find_crossing_zero(data))
def runAnalysis(self):
self.reset()
# self.results_label = QtWidgets.QLabel("<b>Results</b>")
# max_dips_shown = self.max_dips_shown
filename = (
os.path.join("/tmp/", f"{self.input_description.text()}.csv")
if self.input_description.text()
else None)
crossing = self._get_crossing()
logger.debug("Found %d sections ",
len(crossing))
results_header = self.layout.indexOf(self.results_label) results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d", logger.debug("Results start at %d, out of %d",
@ -208,223 +74,24 @@ class ResonanceAnalysis(Analysis):
for _ in range(results_header, self.layout.rowCount()): for _ in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1) self.layout.removeRow(self.layout.rowCount() - 1)
# if len(crossing) > max_dips_shown: if not minima:
# self.layout.addRow(QtWidgets.QLabel(
# "<b>More than " + str(max_dips_shown) +
# " dips found. Lowest shown.</b>"))
# self.crossing = crossing[:max_dips_shown]
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(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( self.layout.addRow(QtWidgets.QLabel(
"No resonance found")) f"No areas found with VSWR below {format_vswr(threshold)}."))
return
for idx in minima:
class EFHWAnalysis(ResonanceAnalysis): rng = at.take_from_idx(data, idx, lambda i: i[1] < threshold)
""" begin, end = rng[0], rng[-1]
find only resonance when HI impedance self.layout.addRow("Start", QtWidgets.QLabel(
""" format_frequency(s11[begin].freq)))
old_data = [] self.layout.addRow("Minimum", QtWidgets.QLabel(
f"{format_frequency(s11[idx].freq)}"
def reset(self): f" ({round(s11[idx].vswr, 2)})"))
logger.debug("reset") self.layout.addRow("End", QtWidgets.QLabel(
format_frequency(s11[end].freq)))
def runAnalysis(self):
self.reset()
if description := self.input_description.text():
filename = os.path.join("/tmp/", f"{description}.csv")
else:
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))
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d",
results_header, self.layout.rowCount())
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
fields = [("freq", format_frequency_short),
("r", format_resistence_neg), ("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(sorted(extended_data.keys())):
s11_idx = self.app.data.s11[index]
self.layout.addRow( self.layout.addRow(
f"{format_frequency_short(s11_idx.freq)}", "Span", QtWidgets.QLabel(format_frequency(
QtWidgets.QLabel( (s11[end].freq - s11[begin].freq))))
f" ({diff[i]['freq']})" self.layout.addWidget(QHLine())
f" {format_complex_imp(s11_idx.impedance())}"
f" ({diff[i]['r']}) {diff[i]['lambda']} m"))
if filename and extended_data: self.layout.removeRow(self.layout.rowCount() - 1)
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)
def compare(self, old, new, fields=None):
"""
Compare data to help changes
NB
must be same sweep
( same index must be same frequence )
:param old:
:param new:
"""
fields = fields or [("freq", str), ]
def no_compare():
return {k: "-" for k, _ in fields}
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))
if i_max == i_tot:
logger.debug("may be the same antenna ... analyzing")
else:
logger.warning("resonances changed from %s to %s",
len(old_idx), len(new_idx))
logger.debug("Trying to compare only first %s resonances", i_max)
split = 0
max_delta_f = 1000000 # 1M
for i, k in enumerate(new_idx):
my_diff = {}
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("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
diff[i] = no_compare()
return diff

Wyświetl plik

@ -1,9 +0,0 @@
from .Analysis import Analysis
from .BandPassAnalysis import BandPassAnalysis
from .BandStopAnalysis import BandStopAnalysis
from .HighPassAnalysis import HighPassAnalysis
from .LowPassAnalysis import LowPassAnalysis
from .PeakSearchAnalysis import PeakSearchAnalysis
from .SimplePeakSearchAnalysis import SimplePeakSearchAnalysis
from .VSWRAnalysis import VSWRAnalysis
from .AntennaAnalysis import MagLoopAnalysis

Wyświetl plik

@ -0,0 +1,141 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Callable, List, Tuple
import itertools as it
import numpy as np
import scipy
from NanoVNASaver.RFTools import Datapoint
def zero_crossings(data: List[float]) -> List[int]:
"""find zero crossings
Args:
data (List[float]): data list execute
Returns:
List[int]: sorted indices of zero crossing points
"""
if not data:
return []
np_data = np.array(data)
# start with real zeros (ignore first and last element)
real_zeros = [n for n in np.where(np_data == 0.0)[0] if
n not in {0, np_data.size - 1}]
# now multipy elements to find change in signess
crossings = [
n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
for n in np.where((np_data[:-1] * np_data[1:]) < 0.0)[0]
]
return sorted(real_zeros + crossings)
def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
"""maxima
Args:
data (List[float]): data list to execute
Returns:
List[int]: indices of maxima
"""
peaks, _ = scipy.signal.find_peaks(
data, width=2, distance=3, prominence=1)
return [
i for i in peaks if data[i] > threshold
] if threshold else peaks
def minima(data: List[float], threshold: float = 0.0) -> List[int]:
"""minima
Args:
data (List[float]): data list to execute
Returns:
List[int]: indices of minima
"""
bottoms, _ = scipy.signal.find_peaks(
-np.array(data), width=2, distance=3, prominence=1)
return [
i for i in bottoms if data[i] < threshold
] if threshold else bottoms
def take_from_idx(data: List[float],
idx: int,
predicate: Callable) -> List[int]:
"""take_from_center
Args:
data (List[float]): data list to execute
idx (int): index of a start position
predicate (Callable): predicate on which elements to take
from center. (e.g. lambda i: i[1] < threshold)
Returns:
List[int]: indices of element matching predicate left
and right from index
"""
lower = list(reversed(
[i for i, _ in
it.takewhile(predicate,
reversed(list(enumerate(data[:idx]))))]))
upper = [i for i, _ in
it.takewhile(predicate,
enumerate(data[idx:], idx))]
return lower + upper
def center_from_idx(gains: List[float],
idx: int, delta: float = 3.0) -> int:
"""find maximum from index postion of gains in a attn dB gain span
Args:
gains (List[float]): gain values
idx (int): start position to search from
delta (float=3.0): max gain delta from start
Returns:
int: position of highest gain from start in range (-1 if no data)
"""
peak_db = gains[idx]
rng = take_from_idx(gains, idx,
lambda i: abs(peak_db - i[1]) < delta)
return max(rng, key=lambda i: gains[i]) if rng else -1
def cut_off_left(gains: List[float], idx: int,
peak_gain: float, attn: float = 3.0) -> int:
return next(
(i for i in range(idx, -1, -1) if
(peak_gain - gains[i]) > attn),
-1)
def cut_off_right(gains: List[float], idx: int,
peak_gain: float, attn: float = 3.0) -> int:
return next(
(i for i in range(idx, len(gains)) if
(peak_gain - gains[i]) > attn),
-1)

Wyświetl plik

@ -82,6 +82,8 @@ class Value:
def __str__(self) -> str: def __str__(self) -> str:
fmt = self.fmt fmt = self.fmt
if math.isnan(self._value):
return (f"NaN{fmt.space_str}{self._unit}")
if (fmt.assume_infinity and if (fmt.assume_infinity and
abs(self._value) >= 10 ** ((fmt.max_offset + 1) * 3)): abs(self._value) >= 10 ** ((fmt.max_offset + 1) * 3)):
return (("-" if self._value < 0 else "") + return (("-" if self._value < 0 else "") +

Wyświetl plik

@ -20,13 +20,16 @@ import logging
from PyQt5 import QtWidgets, QtCore from PyQt5 import QtWidgets, QtCore
from NanoVNASaver.Analysis import ( from NanoVNASaver.Analysis.Base import Analysis
Analysis, LowPassAnalysis, HighPassAnalysis, BandPassAnalysis, from NanoVNASaver.Analysis.AntennaAnalysis import MagLoopAnalysis
BandStopAnalysis, VSWRAnalysis, SimplePeakSearchAnalysis, from NanoVNASaver.Analysis.ResonanceAnalysis import EFHWAnalysis, ResonanceAnalysis
MagLoopAnalysis) from NanoVNASaver.Analysis.LowPassAnalysis import LowPassAnalysis
from NanoVNASaver.Analysis.VSWRAnalysis import ResonanceAnalysis from NanoVNASaver.Analysis.HighPassAnalysis import HighPassAnalysis
from NanoVNASaver.Analysis.VSWRAnalysis import EFHWAnalysis from NanoVNASaver.Analysis.BandPassAnalysis import BandPassAnalysis
from NanoVNASaver.Analysis import PeakSearchAnalysis from NanoVNASaver.Analysis.BandStopAnalysis import BandStopAnalysis
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import SimplePeakSearchAnalysis
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
from NanoVNASaver.Analysis.PeakSearchAnalysis import PeakSearchAnalysis
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

Wyświetl plik

@ -17,6 +17,8 @@ points, and generally display and analyze the resulting data.
Latest Changes Latest Changes
-------------- --------------
### Changes in 0.5.4-pre
### Changes in 0.5.3 ### Changes in 0.5.3
- Python 3.10 compatability fixes - Python 3.10 compatability fixes