kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Feature/v0.5.4 pre (#548)
* Simplified VSWR analysis * Split history VSWRAnalysis.py to ResonanceAnalysis.py * simplified BandPassAnalysispull/553/head
rodzic
6630568ed9
commit
879d5ddea3
|
@ -1,6 +1,7 @@
|
|||
/venv/
|
||||
/env/
|
||||
.idea/
|
||||
.tox/
|
||||
.vscode/
|
||||
/build/
|
||||
/dist/
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
0.5.4
|
||||
-----
|
||||
|
||||
0.5.3
|
||||
-----
|
||||
|
||||
|
|
|
@ -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'])
|
||||
)))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
|
@ -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__)
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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 "") +
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue