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/
/env/
.idea/
.tox/
.vscode/
/build/
/dist/

Wyświetl plik

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

Wyświetl plik

@ -18,15 +18,18 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from typing import Dict
from PyQt5 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_frequency
from NanoVNASaver.Analysis import Analysis
logger = logging.getLogger(__name__)
CUTOFF_VALS = (3.0, 6.0, 10.0, 20.0, 60.0)
class BandPassAnalysis(Analysis):
def __init__(self, app):
@ -41,344 +44,149 @@ class BandPassAnalysis(Analysis):
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name}"
f" in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.lower_cutoff_label = QtWidgets.QLabel()
self.lower_six_db_label = QtWidgets.QLabel()
self.lower_sixty_db_label = QtWidgets.QLabel()
self.lower_db_per_octave_label = QtWidgets.QLabel()
self.lower_db_per_decade_label = QtWidgets.QLabel()
self.upper_cutoff_label = 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)
self.label = {
label: QtWidgets.QLabel() for label in
('result', 'octave_l', 'octave_r', 'decade_l', 'decade_r',
'freq_center', 'span_3.0dB', 'span_6.0dB', 'q_factor')
}
for attn in CUTOFF_VALS:
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
layout.addRow("Result:", self.label['result'])
layout.addRow(QtWidgets.QLabel(""))
self.center_frequency_label = QtWidgets.QLabel()
self.span_label = QtWidgets.QLabel()
self.six_db_span_label = QtWidgets.QLabel()
self.quality_label = QtWidgets.QLabel()
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("Center frequency:", self.label['freq_center'])
layout.addRow("Bandwidth (-3 dB):", self.label['span_3.0dB'])
layout.addRow("Quality factor:", self.label['q_factor'])
layout.addRow("Bandwidth (-6 dB):", self.label['span_6.0dB'])
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.lower_cutoff_label)
layout.addRow("-6 dB point:", self.lower_six_db_label)
layout.addRow("-60 dB point:", self.lower_sixty_db_label)
layout.addRow("Roll-off:", self.lower_db_per_octave_label)
layout.addRow("Roll-off:", self.lower_db_per_decade_label)
layout.addRow("Cutoff frequency:", self.label['3.0dB_l'])
layout.addRow("-6 dB point:", self.label['6.0dB_l'])
layout.addRow("-60 dB point:", self.label['60.0dB_l'])
layout.addRow("Roll-off:", self.label['octave_l'])
layout.addRow("Roll-off:", self.label['decade_l'])
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.upper_cutoff_label)
layout.addRow("-6 dB point:", self.upper_six_db_label)
layout.addRow("-60 dB point:", self.upper_sixty_db_label)
layout.addRow("Roll-off:", self.upper_db_per_octave_label)
layout.addRow("Roll-off:", self.upper_db_per_decade_label)
layout.addRow("Cutoff frequency:", self.label['3.0dB_r'])
layout.addRow("-6 dB point:", self.label['6.0dB_r'])
layout.addRow("-60 dB point:", self.label['60.0dB_r'])
layout.addRow("Roll-off:", self.label['octave_r'])
layout.addRow("Roll-off:", self.label['decade_r'])
def reset(self):
self.result_label.clear()
self.center_frequency_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()
for label in self.label.values():
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:
if not self.app.data.s21:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
self.label['result'].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]
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
pass_band_db = self.app.data.s21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
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.")
# 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")
return
peak_db = gains[peak]
logger.debug("Bandpass center pos: %d(%fdB)", peak, peak_db)
initial_lower_cutoff_frequency = (
self.app.data.s21[initial_lower_cutoff_location].freq)
# find passband bounderies
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",
initial_lower_cutoff_frequency)
self.derive_60dB(cutoff_pos, cutoff_freq)
initial_upper_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found a cutoff location
initial_upper_cutoff_location = i
break
result = {
'span_3.0dB': cutoff_freq['3.0dB_r'] - cutoff_freq['3.0dB_l'],
'span_6.0dB': cutoff_freq['6.0dB_r'] - cutoff_freq['6.0dB_l'],
'freq_center': int(
math.sqrt(cutoff_freq['3.0dB_l'] * cutoff_freq['3.0dB_r'])),
}
result['q_factor'] = result['freq_center'] / result['span_3.0dB']
if initial_upper_cutoff_location < 0:
self.result_label.setText("Upper cutoff location not found.")
return
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"])
initial_upper_cutoff_frequency = (
self.app.data.s21[initial_upper_cutoff_location].freq)
for label, val in cutoff_freq.items():
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",
initial_upper_cutoff_frequency)
for label in ('octave_l', 'decade_l', 'octave_r', 'decade_r'):
self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
peak_location = -1
peak_db = self.app.data.s21[initial_lower_cutoff_location].gain
for i in range(initial_lower_cutoff_location,
initial_upper_cutoff_location, 1):
db = self.app.data.s21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
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']}")
logger.debug("Found peak of %f at %d", peak_db,
self.app.data.s11[peak_location].freq)
lower_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
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"
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(
f"Analysis complete ({len(s21)} points)\n"
f"Insufficient data for analysis. Increase segment count.")
else:
self.result_label.setText(
f"Analysis complete ({len(self.app.data.s11)} points)")
return
self.label['result'].setText(
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 NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
@ -100,34 +100,28 @@ class BandStopAnalysis(Analysis):
def runAnalysis(self):
self.reset()
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
peak_location = -1
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
s21 = self.app.data.s21
peak_location, peak = max(enumerate(s21), key=lambda i: i[1].gain)
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
pass_band_db = peak_db
for i in range(len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
for i in range(len(s21)):
if (pass_band_db - 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_frequency = s21[lower_cutoff_location].freq
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:
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))
upper_cutoff_location = -1
for i in range(len(self.app.data.s21)-1, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
for i in range(len(s21)-1, -1, -1):
if (pass_band_db - 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)
s21[upper_cutoff_location].freq)
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:
logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
@ -186,8 +180,8 @@ class BandStopAnalysis(Analysis):
# Lower roll-off
lower_six_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
for i in range(lower_cutoff_location, len(s21)):
if (pass_band_db - s21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
@ -196,39 +190,39 @@ class BandStopAnalysis(Analysis):
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)
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, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
for i in range(lower_cutoff_location, len(s21)):
if (pass_band_db - s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
for i in range(lower_cutoff_location, len(s21)):
if (pass_band_db - 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, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 60:
for i in range(lower_six_db_location, len(s21)):
if (pass_band_db - 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)
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
ten = s21[ten_db_location].freq
twenty = s21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.lower_sixty_db_label.setText(
@ -253,7 +247,7 @@ class BandStopAnalysis(Analysis):
upper_six_db_location = -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
upper_six_db_location = i
break
@ -262,7 +256,7 @@ class BandStopAnalysis(Analysis):
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)
s21[upper_six_db_location].freq)
self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency))
@ -274,33 +268,33 @@ class BandStopAnalysis(Analysis):
ten_db_location = -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
ten_db_location = i
break
twenty_db_location = -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
twenty_db_location = i
break
sixty_db_location = -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.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = (
self.app.data.s21[sixty_db_location].freq)
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
ten = s21[ten_db_location].freq
twenty = s21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (
5 * (math.log10(twenty) - math.log10(ten)))
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/>.
import logging
import math
from typing import Tuple
import numpy as np
import scipy
from PyQt5 import QtWidgets
from scipy import signal
logger = logging.getLogger(__name__)
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
class Analysis:
_widget = None
@ -109,7 +118,7 @@ class Analysis:
:param data: list of values
:param threshold:
"""
peaks, _ = signal.find_peaks(
peaks, _ = scipy.signal.find_peaks(
data, width=2, distance=3, prominence=1)
# my_data = np.array(data)
@ -130,21 +139,18 @@ class Analysis:
def reset(self):
pass
def calculateRolloff(self, location1, location2):
if location1 == location2:
return 0, 0
frequency1 = self.app.data.s21[location1].freq
frequency2 = self.app.data.s21[location2].freq
gain1 = self.app.data.s21[location1].gain
gain2 = self.app.data.s21[location2].gain
frequency_factor = frequency2 / frequency1
if frequency_factor < 1:
frequency_factor = 1 / frequency_factor
attenuation = abs(gain1 - gain2)
logger.debug("Measured points: %d Hz and %d Hz",
frequency1, frequency2)
logger.debug("%f dB over %f factor", attenuation, frequency_factor)
octave_attenuation = attenuation / \
(math.log10(frequency_factor) / math.log10(2))
decade_attenuation = attenuation / math.log10(frequency_factor)
return octave_attenuation, decade_attenuation
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)

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -19,10 +19,10 @@
import logging
from PyQt5 import QtWidgets
from scipy import signal
import scipy
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_gain
from NanoVNASaver.Formatting import format_resistance
@ -112,17 +112,17 @@ class PeakSearchAnalysis(Analysis):
return
if self.rbtn_peak_positive.isChecked():
peaks, _ = signal.find_peaks(
peaks, _ = scipy.signal.find_peaks(
data, width=3, distance=3, prominence=1)
elif self.rbtn_peak_negative.isChecked():
sign = -1
data = [x * sign for x in data]
peaks, _ = signal.find_peaks(
peaks, _ = scipy.signal.find_peaks(
data, width=3, distance=3, prominence=1)
# elif self.rbtn_peak_both.isChecked():
# peaks_max, _ = signal.find_peaks(
# peaks_max, _ = scipy.signal.find_peaks(
# 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)
# peaks = np.concatenate((peaks_max, peaks_min))
else:
@ -136,7 +136,7 @@ class PeakSearchAnalysis(Analysis):
for i, p in np.ndenumerate(peaks):
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))
# 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
import numpy as np
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Analysis.Base import Analysis, QHLine
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_reactance)
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("", 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(PeakSearchAnalysis.QHLine())
outer_layout.addRow(QHLine())
outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
@ -78,26 +78,23 @@ class SimplePeakSearchAnalysis(Analysis):
outer_layout.addRow("Peak value:", self.peak_value)
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():
suffix = ""
data = []
for d in self.app.data.s11:
data.append(d.vswr)
data = [d.vswr for d in s11]
elif self.rbtn_data_resistance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data.s11:
data.append(d.impedance().real)
data = [d.impedance().real for d in s11]
elif self.rbtn_data_reactance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data.s11:
data.append(d.impedance().imag)
data = [d.impedance().imag for d in s11]
elif self.rbtn_data_s21_gain.isChecked():
suffix = " dB"
data = []
for d in self.app.data.s21:
data.append(d.gain)
data = [d.gain for d in s21]
else:
logger.warning("Searching for peaks on unknown data")
return

Wyświetl plik

@ -16,40 +16,22 @@
#
# 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 typing import List
import numpy as np
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Formatting import (
format_frequency, format_complex_imp,
format_frequency_short, format_resistance)
from NanoVNASaver.RFTools import reflection_coefficient
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis, QHLine
from NanoVNASaver.Formatting import format_frequency, format_vswr
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):
max_dips_shown = 3
vswr_limit_value = 1.5
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app):
super().__init__(app)
@ -58,7 +40,7 @@ class VSWRAnalysis(Analysis):
self._widget.setLayout(self.layout)
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.setMinimum(1)
self.input_vswr_limit.setMaximum(25)
@ -67,140 +49,24 @@ class VSWRAnalysis(Analysis):
self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
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.layout.addRow(self.results_label)
self.minimums: List[int] = []
def runAnalysis(self):
max_dips_shown = self.max_dips_shown
data = [d.vswr for d in self.app.data.s11]
if not self.app.data.s11:
return
s11 = self.app.data.s11
data = [d.vswr for d in s11]
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()):
self.layout.removeRow(self.layout.rowCount() - 1)
if len(minimums) > max_dips_shown:
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))
minima = sorted(at.minima(data, threshold),
key=lambda i: data[i])[:VSWRAnalysis.max_dips_shown]
self.minimums = minima
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d",
@ -208,223 +74,24 @@ class ResonanceAnalysis(Analysis):
for _ in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1)
# if len(crossing) > max_dips_shown:
# self.layout.addRow(QtWidgets.QLabel(
# "<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:
if not minima:
self.layout.addRow(QtWidgets.QLabel(
"No resonance found"))
f"No areas found with VSWR below {format_vswr(threshold)}."))
return
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", 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]
for idx in minima:
rng = at.take_from_idx(data, idx, lambda i: i[1] < threshold)
begin, end = rng[0], rng[-1]
self.layout.addRow("Start", QtWidgets.QLabel(
format_frequency(s11[begin].freq)))
self.layout.addRow("Minimum", QtWidgets.QLabel(
f"{format_frequency(s11[idx].freq)}"
f" ({round(s11[idx].vswr, 2)})"))
self.layout.addRow("End", QtWidgets.QLabel(
format_frequency(s11[end].freq)))
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"))
"Span", QtWidgets.QLabel(format_frequency(
(s11[end].freq - s11[begin].freq))))
self.layout.addWidget(QHLine())
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
self.layout.removeRow(self.layout.rowCount() - 1)

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:
fmt = self.fmt
if math.isnan(self._value):
return (f"NaN{fmt.space_str}{self._unit}")
if (fmt.assume_infinity and
abs(self._value) >= 10 ** ((fmt.max_offset + 1) * 3)):
return (("-" if self._value < 0 else "") +

Wyświetl plik

@ -20,13 +20,16 @@ import logging
from PyQt5 import QtWidgets, QtCore
from NanoVNASaver.Analysis import (
Analysis, LowPassAnalysis, HighPassAnalysis, BandPassAnalysis,
BandStopAnalysis, VSWRAnalysis, SimplePeakSearchAnalysis,
MagLoopAnalysis)
from NanoVNASaver.Analysis.VSWRAnalysis import ResonanceAnalysis
from NanoVNASaver.Analysis.VSWRAnalysis import EFHWAnalysis
from NanoVNASaver.Analysis import PeakSearchAnalysis
from NanoVNASaver.Analysis.Base import Analysis
from NanoVNASaver.Analysis.AntennaAnalysis import MagLoopAnalysis
from NanoVNASaver.Analysis.ResonanceAnalysis import EFHWAnalysis, ResonanceAnalysis
from NanoVNASaver.Analysis.LowPassAnalysis import LowPassAnalysis
from NanoVNASaver.Analysis.HighPassAnalysis import HighPassAnalysis
from NanoVNASaver.Analysis.BandPassAnalysis import BandPassAnalysis
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__)

Wyświetl plik

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