kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
refactored BandStopAnalysis
rodzic
a73028e2c3
commit
24a4ca0ffa
|
@ -18,7 +18,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
|
@ -37,22 +37,22 @@ class BandPassAnalysis(Analysis):
|
|||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout.addRow(QtWidgets.QLabel("Band pass filter analysis"))
|
||||
layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."))
|
||||
self.label = {
|
||||
label: QtWidgets.QLabel() for label in
|
||||
('result', 'octave_l', 'octave_r', 'decade_l', 'decade_r',
|
||||
('titel', '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 = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout.addRow(self.label['titel'])
|
||||
layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."))
|
||||
layout.addRow("Result:", self.label['result'])
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
|
@ -77,6 +77,11 @@ class BandPassAnalysis(Analysis):
|
|||
layout.addRow("Roll-off:", self.label['octave_r'])
|
||||
layout.addRow("Roll-off:", self.label['decade_r'])
|
||||
|
||||
self.set_titel("Band pass filter analysis")
|
||||
|
||||
def set_titel(self, name):
|
||||
self.label['titel'].setText(name)
|
||||
|
||||
def reset(self):
|
||||
for label in self.label.values():
|
||||
label.clear()
|
||||
|
@ -91,28 +96,13 @@ class BandPassAnalysis(Analysis):
|
|||
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
|
||||
|
||||
# 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")
|
||||
if (peak := self.find_center(gains)) < 0:
|
||||
return
|
||||
peak_db = gains[peak]
|
||||
logger.debug("Bandpass center pos: %d(%fdB)", peak, peak_db)
|
||||
logger.debug("Filter center pos: %d(%fdB)", peak, peak_db)
|
||||
|
||||
# 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_pos = self.find_bounderies(gains, peak, peak_db)
|
||||
cutoff_freq = {
|
||||
att: s21[val].freq if val >= 0 else math.nan
|
||||
for att, val in cutoff_pos.items()
|
||||
|
@ -129,8 +119,8 @@ class BandPassAnalysis(Analysis):
|
|||
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'])),
|
||||
'freq_center':
|
||||
math.sqrt(cutoff_freq['3.0dB_l'] * cutoff_freq['3.0dB_r']),
|
||||
}
|
||||
result['q_factor'] = result['freq_center'] / result['span_3.0dB']
|
||||
|
||||
|
@ -190,3 +180,29 @@ class BandPassAnalysis(Analysis):
|
|||
10 ** (5 * (math.log10(cutoff_pos['20.0dB_r']) -
|
||||
math.log10(cutoff_pos['10.0dB_r'])
|
||||
)))
|
||||
|
||||
def find_center(self, gains: List[float]) -> int:
|
||||
marker = self.app.markers[0]
|
||||
if marker.location <= 0 or marker.location >= len(gains) - 1:
|
||||
logger.debug("No valid location for %s (%s)",
|
||||
marker.name, marker.location)
|
||||
self.label['result'].setText(
|
||||
f"Please place {marker.name} in the passband.")
|
||||
return -1
|
||||
|
||||
# find center of passband based on marker pos
|
||||
if (peak := at.center_from_idx(gains, marker.location)) < 0:
|
||||
self.label['result'].setText("Bandpass center not found")
|
||||
return -1
|
||||
return peak
|
||||
|
||||
def find_bounderies(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
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)
|
||||
return cutoff_pos
|
||||
|
|
|
@ -17,12 +17,11 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from NanoVNASaver.Analysis.BandPassAnalysis import BandPassAnalysis
|
||||
from NanoVNASaver.Formatting import format_frequency
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.BandPassAnalysis import (
|
||||
BandPassAnalysis, CUTOFF_VALS)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -30,251 +29,16 @@ logger = logging.getLogger(__name__)
|
|||
class BandStopAnalysis(BandPassAnalysis):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.set_titel("Band stop filter analysis")
|
||||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
def find_center(self, gains: List[float]) -> int:
|
||||
return max(enumerate(gains), key=lambda i: i[1])[0]
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout.addRow(QtWidgets.QLabel("Band stop filter analysis"))
|
||||
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)
|
||||
|
||||
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(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(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)
|
||||
|
||||
def reset(self):
|
||||
self.result_label.clear()
|
||||
self.span_label.clear()
|
||||
self.quality_label.clear()
|
||||
self.six_db_span_label.clear()
|
||||
|
||||
self.upper_cutoff_label.clear()
|
||||
self.upper_six_db_label.clear()
|
||||
self.upper_sixty_db_label.clear()
|
||||
self.upper_db_per_octave_label.clear()
|
||||
self.upper_db_per_decade_label.clear()
|
||||
|
||||
self.lower_cutoff_label.clear()
|
||||
self.lower_six_db_label.clear()
|
||||
self.lower_sixty_db_label.clear()
|
||||
self.lower_db_per_octave_label.clear()
|
||||
self.lower_db_per_decade_label.clear()
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s21:
|
||||
logger.debug("No data to analyse")
|
||||
self.result_label.setText("No data to analyse.")
|
||||
return
|
||||
|
||||
self.reset()
|
||||
s21 = self.app.data.s21
|
||||
gains = [d.gain for d in s21]
|
||||
|
||||
peak_location, pass_band_db = max(enumerate(gains), key=lambda i: i[1])
|
||||
logger.debug("Found peak of %f at %d",
|
||||
pass_band_db, s21[peak_location].freq)
|
||||
|
||||
lower_cutoff_location = next(
|
||||
(i for i in range(len(s21)) if (pass_band_db - s21[i].gain) > 3), -1)
|
||||
|
||||
lower_cutoff_frequency = s21[lower_cutoff_location].freq
|
||||
lower_cutoff_gain = (
|
||||
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 = next((i for i in range(
|
||||
len(s21) - 1, -1, -1) if (pass_band_db - s21[i].gain) > 3), -1)
|
||||
|
||||
upper_cutoff_frequency = (
|
||||
s21[upper_cutoff_location].freq)
|
||||
upper_cutoff_gain = (
|
||||
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_six_db_location = next((i for i in range(
|
||||
lower_cutoff_location, len(s21)) if (pass_band_db - s21[i].gain) > 6), -1)
|
||||
|
||||
if lower_six_db_location < 0:
|
||||
self.result_label.setText("Lower 6 dB location not found.")
|
||||
return
|
||||
lower_six_db_cutoff_frequency = (
|
||||
s21[lower_six_db_location].freq)
|
||||
self.lower_six_db_label.setText(
|
||||
format_frequency(lower_six_db_cutoff_frequency))
|
||||
|
||||
ten_db_location = next((i for i in range(lower_cutoff_location, len(
|
||||
s21)) if (pass_band_db - s21[i].gain) > 10), -1)
|
||||
|
||||
twenty_db_location = next((i for i in range(
|
||||
lower_cutoff_location, len(s21)) if (pass_band_db - s21[i].gain) > 20), -1)
|
||||
|
||||
sixty_db_location = next((i for i in range(
|
||||
lower_six_db_location, len(s21)) if (pass_band_db - s21[i].gain) > 60), -1)
|
||||
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = (
|
||||
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 = 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(
|
||||
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(
|
||||
f"{round(octave_attenuation, 3)} dB / octave")
|
||||
self.lower_db_per_decade_label.setText(
|
||||
f"{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_six_db_location = next((i for i in range(
|
||||
upper_cutoff_location, -1, -1) if (pass_band_db - s21[i].gain) > 6), -1)
|
||||
|
||||
if upper_six_db_location < 0:
|
||||
self.result_label.setText("Upper 6 dB location not found.")
|
||||
return
|
||||
upper_six_db_cutoff_frequency = (
|
||||
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 = next((i for i in range(
|
||||
upper_cutoff_location, -1, -1) if (pass_band_db - s21[i].gain) > 10), -1)
|
||||
|
||||
twenty_db_location = next((i for i in range(
|
||||
upper_cutoff_location, -1, -1) if (pass_band_db - s21[i].gain) > 20), -1)
|
||||
|
||||
sixty_db_location = next((i for i in range(
|
||||
upper_six_db_location, -1, -1) if (pass_band_db - s21[i].gain) > 60), -1)
|
||||
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = (
|
||||
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 = 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(
|
||||
f"{format_frequency(sixty_db_frequency)} (derived)")
|
||||
else:
|
||||
self.upper_sixty_db_label.setText("Not calculated")
|
||||
|
||||
if (ten_db_location > 0 and
|
||||
twenty_db_location > 0 and
|
||||
ten_db_location != twenty_db_location):
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(
|
||||
ten_db_location, twenty_db_location)
|
||||
self.upper_db_per_octave_label.setText(
|
||||
f"{round(octave_attenuation, 3)} dB / octave")
|
||||
self.upper_db_per_decade_label.setText(
|
||||
f"{round(decade_attenuation, 3)} dB / decade")
|
||||
else:
|
||||
self.upper_db_per_octave_label.setText("Not calculated")
|
||||
self.upper_db_per_decade_label.setText("Not calculated")
|
||||
|
||||
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
|
||||
self.result_label.setText(
|
||||
f"Analysis complete ({len(self.app.data.s11)} points)\n"
|
||||
f"Insufficient data for analysis. Increase segment count.")
|
||||
return
|
||||
self.result_label.setText(
|
||||
f"Analysis complete ({len(self.app.data.s11)} points)")
|
||||
def find_bounderies(self,
|
||||
gains: List[float],
|
||||
_: int, peak_db: float) -> Dict[str, int]:
|
||||
cutoff_pos = {}
|
||||
for attn in CUTOFF_VALS:
|
||||
cutoff_pos[f"{attn:.1f}dB_l"], cutoff_pos[f"{attn:.1f}dB_r"] = (
|
||||
at.dip_cut_offs(gains, peak_db, attn))
|
||||
return cutoff_pos
|
||||
|
|
|
@ -16,13 +16,14 @@
|
|||
#
|
||||
# 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 math
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import scipy
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
|
||||
|
||||
def zero_crossings(data: List[float]) -> List[int]:
|
||||
|
@ -59,8 +60,8 @@ def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
|
|||
Returns:
|
||||
List[int]: indices of maxima
|
||||
"""
|
||||
peaks, _ = scipy.signal.find_peaks(
|
||||
data, width=2, distance=3, prominence=1)
|
||||
peaks = scipy.signal.find_peaks(
|
||||
data, width=2, distance=3, prominence=1)[0].tolist()
|
||||
return [
|
||||
i for i in peaks if data[i] > threshold
|
||||
] if threshold else peaks
|
||||
|
@ -75,8 +76,8 @@ def minima(data: List[float], threshold: float = 0.0) -> List[int]:
|
|||
Returns:
|
||||
List[int]: indices of minima
|
||||
"""
|
||||
bottoms, _ = scipy.signal.find_peaks(
|
||||
-np.array(data), width=2, distance=3, prominence=1)
|
||||
bottoms = scipy.signal.find_peaks(
|
||||
-np.array(data), width=2, distance=3, prominence=1)[0].tolist()
|
||||
return [
|
||||
i for i in bottoms if data[i] < threshold
|
||||
] if threshold else bottoms
|
||||
|
@ -139,3 +140,9 @@ def cut_off_right(gains: List[float], idx: int,
|
|||
(i for i in range(idx, len(gains)) if
|
||||
(peak_gain - gains[i]) > attn),
|
||||
-1)
|
||||
|
||||
|
||||
def dip_cut_offs(gains: List[float], peak_gain: float,
|
||||
attn: float = 3.0) -> Tuple[int, int]:
|
||||
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
|
||||
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
|
||||
|
|
|
@ -127,8 +127,9 @@ class RealImaginaryChart(FrequencyChart):
|
|||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
if not self.data and not self.reference:
|
||||
return
|
||||
|
||||
pen = QtGui.QPen(Chart.color.sweep)
|
||||
pen.setWidth(self.dim.point)
|
||||
line_pen = QtGui.QPen(Chart.color.sweep)
|
||||
|
@ -142,71 +143,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
if self.bands.enabled:
|
||||
self.drawBands(qp, self.fstart, self.fstop)
|
||||
|
||||
# Find scaling
|
||||
if self.fixedValues:
|
||||
min_real = self.minDisplayReal
|
||||
max_real = self.maxDisplayReal
|
||||
min_imag = self.minDisplayImag
|
||||
max_imag = self.maxDisplayImag
|
||||
else:
|
||||
min_real = 1000
|
||||
min_imag = 1000
|
||||
max_real = 0
|
||||
max_imag = -1000
|
||||
for d in self.data:
|
||||
imp = self.impedance(d)
|
||||
re, im = imp.real, imp.imag
|
||||
if math.isinf(re): # Avoid infinite scales
|
||||
continue
|
||||
max_real = max(max_real, re)
|
||||
min_real = min(min_real, re)
|
||||
max_imag = max(max_imag, im)
|
||||
min_imag = min(min_imag, im)
|
||||
# Also check min/max for the reference sweep
|
||||
for d in self.reference:
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
imp = self.impedance(d)
|
||||
re, im = imp.real, imp.imag
|
||||
if math.isinf(re): # Avoid infinite scales
|
||||
continue
|
||||
max_real = max(max_real, re)
|
||||
min_real = min(min_real, re)
|
||||
max_imag = max(max_imag, im)
|
||||
min_imag = min(min_imag, im)
|
||||
# Always have at least 8 numbered horizontal lines
|
||||
max_real = math.ceil(max_real)
|
||||
min_real = math.floor(min_real)
|
||||
max_imag = math.ceil(max_imag)
|
||||
min_imag = math.floor(min_imag)
|
||||
|
||||
if max_imag - min_imag < 8:
|
||||
missing = 8 - (max_imag - min_imag)
|
||||
max_imag += math.ceil(missing / 2)
|
||||
min_imag -= math.floor(missing / 2)
|
||||
|
||||
if 0 > max_imag > -2:
|
||||
max_imag = 0
|
||||
if 0 < min_imag < 2:
|
||||
min_imag = 0
|
||||
|
||||
if (max_imag - min_imag) > 8 and min_imag < 0 < max_imag:
|
||||
# We should show a "0" line for the reactive part
|
||||
span = max_imag - min_imag
|
||||
step_size = span / 8
|
||||
if max_imag < step_size:
|
||||
# The 0 line is the first step after the top.
|
||||
# Scale accordingly.
|
||||
max_imag = -min_imag / 7
|
||||
elif -min_imag < step_size:
|
||||
# The 0 line is the last step before the bottom.
|
||||
# Scale accordingly.
|
||||
min_imag = -max_imag / 7
|
||||
else:
|
||||
# Scale max_imag to be a whole factor of min_imag
|
||||
num_min = math.floor(min_imag / step_size * -1)
|
||||
num_max = 8 - num_min
|
||||
max_imag = num_max * (min_imag / num_min) * -1
|
||||
min_real, max_real, min_imag, max_imag = self.find_scaling()
|
||||
|
||||
self.max_real = max_real
|
||||
self.max_imag = max_imag
|
||||
|
@ -214,24 +151,9 @@ class RealImaginaryChart(FrequencyChart):
|
|||
self.span_real = (max_real - min_real) or 0.01
|
||||
self.span_imag = (max_imag - min_imag) or 0.01
|
||||
|
||||
# We want one horizontal tick per 50 pixels, at most
|
||||
horizontal_ticks = self.dim.height // 50
|
||||
self.drawHorizontalTicks(qp)
|
||||
|
||||
fmt = Format(max_nr_digits=3)
|
||||
for i in range(horizontal_ticks):
|
||||
y = self.topMargin + i * self.dim.height // horizontal_ticks
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width + 5, y)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
re = max_real - i * self.span_real / horizontal_ticks
|
||||
im = max_imag - i * self.span_imag / horizontal_ticks
|
||||
qp.drawText(3, y + 4, f"{Value(re, fmt=fmt)}")
|
||||
qp.drawText(
|
||||
self.leftMargin + self.dim.width + 8,
|
||||
y + 4,
|
||||
f"{Value(im, fmt=fmt)}")
|
||||
|
||||
qp.drawText(3, self.dim.height + self.topMargin,
|
||||
str(Value(min_real, fmt=fmt)))
|
||||
qp.drawText(self.leftMargin + self.dim.width + 8,
|
||||
|
@ -242,7 +164,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
primary_pen = pen
|
||||
secondary_pen = QtGui.QPen(Chart.color.sweep_secondary)
|
||||
if len(self.data) > 0:
|
||||
if self.data:
|
||||
c = QtGui.QColor(Chart.color.sweep)
|
||||
c.setAlpha(255)
|
||||
pen = QtGui.QPen(c)
|
||||
|
@ -307,7 +229,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
line_pen.setColor(Chart.color.reference)
|
||||
secondary_pen.setColor(Chart.color.reference_secondary)
|
||||
qp.setPen(primary_pen)
|
||||
if len(self.reference) > 0:
|
||||
if self.reference:
|
||||
c = QtGui.QColor(Chart.color.reference)
|
||||
c.setAlpha(255)
|
||||
pen = QtGui.QPen(c)
|
||||
|
@ -379,6 +301,97 @@ class RealImaginaryChart(FrequencyChart):
|
|||
self.drawMarker(x, y_im, qp, m.color,
|
||||
self.markers.index(m) + 1)
|
||||
|
||||
def drawHorizontalTicks(self, qp):
|
||||
# We want one horizontal tick per 50 pixels, at most
|
||||
fmt = Format(max_nr_digits=3)
|
||||
horizontal_ticks = self.dim.height // 50
|
||||
for i in range(horizontal_ticks):
|
||||
y = self.topMargin + i * self.dim.height // horizontal_ticks
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width + 5, y)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
re = self.max_real - i * self.span_real / horizontal_ticks
|
||||
im = self.max_imag - i * self.span_imag / horizontal_ticks
|
||||
qp.drawText(3, y + 4, f"{Value(re, fmt=fmt)}")
|
||||
qp.drawText(
|
||||
self.leftMargin + self.dim.width + 8,
|
||||
y + 4,
|
||||
f"{Value(im, fmt=fmt)}")
|
||||
|
||||
def find_scaling(self):
|
||||
# Find scaling
|
||||
if self.fixedValues:
|
||||
min_real = self.minDisplayReal
|
||||
max_real = self.maxDisplayReal
|
||||
min_imag = self.minDisplayImag
|
||||
max_imag = self.maxDisplayImag
|
||||
return min_real, max_real, min_imag, max_imag
|
||||
|
||||
min_real = 1000
|
||||
min_imag = 1000
|
||||
max_real = 0
|
||||
max_imag = -1000
|
||||
for d in self.data:
|
||||
imp = self.impedance(d)
|
||||
re, im = imp.real, imp.imag
|
||||
if math.isinf(re): # Avoid infinite scales
|
||||
continue
|
||||
max_real = max(max_real, re)
|
||||
min_real = min(min_real, re)
|
||||
max_imag = max(max_imag, im)
|
||||
min_imag = min(min_imag, im)
|
||||
# Also check min/max for the reference sweep
|
||||
for d in self.reference:
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
imp = self.impedance(d)
|
||||
re, im = imp.real, imp.imag
|
||||
if math.isinf(re): # Avoid infinite scales
|
||||
continue
|
||||
max_real = max(max_real, re)
|
||||
min_real = min(min_real, re)
|
||||
max_imag = max(max_imag, im)
|
||||
min_imag = min(min_imag, im)
|
||||
# Always have at least 8 numbered horizontal lines
|
||||
max_real = math.ceil(max_real)
|
||||
min_real = math.floor(min_real)
|
||||
max_imag = math.ceil(max_imag)
|
||||
min_imag = math.floor(min_imag)
|
||||
|
||||
min_imag, max_imag = self.imag_scaling_constraints(min_imag, max_imag)
|
||||
return min_real, max_real, min_imag, max_imag
|
||||
|
||||
def imag_scaling_constraints(self, min_imag, max_imag):
|
||||
if max_imag - min_imag < 8:
|
||||
missing = 8 - (max_imag - min_imag)
|
||||
max_imag += math.ceil(missing / 2)
|
||||
min_imag -= math.floor(missing / 2)
|
||||
|
||||
if 0 > max_imag > -2:
|
||||
max_imag = 0
|
||||
if 0 < min_imag < 2:
|
||||
min_imag = 0
|
||||
|
||||
if (max_imag - min_imag) > 8 and min_imag < 0 < max_imag:
|
||||
# We should show a "0" line for the reactive part
|
||||
span = max_imag - min_imag
|
||||
step_size = span / 8
|
||||
if max_imag < step_size:
|
||||
# The 0 line is the first step after the top.
|
||||
# Scale accordingly.
|
||||
max_imag = -min_imag / 7
|
||||
elif -min_imag < step_size:
|
||||
# The 0 line is the last step before the bottom.
|
||||
# Scale accordingly.
|
||||
min_imag = -max_imag / 7
|
||||
else:
|
||||
# Scale max_imag to be a whole factor of min_imag
|
||||
num_min = math.floor(min_imag / step_size * -1)
|
||||
num_max = 8 - num_min
|
||||
max_imag = num_max * (min_imag / num_min) * -1
|
||||
return min_imag, max_imag
|
||||
|
||||
def getImYPosition(self, d: Datapoint) -> int:
|
||||
im = self.impedance(d).imag
|
||||
return int(self.topMargin + (self.max_imag - im) / self.span_imag
|
||||
|
|
|
@ -72,6 +72,7 @@ class Value:
|
|||
self.fmt = fmt
|
||||
if isinstance(value, str):
|
||||
self._value = Decimal(math.nan)
|
||||
if value.lower() != 'nan':
|
||||
self.parse(value)
|
||||
else:
|
||||
self._value = Decimal(value, context=Value.CTX)
|
||||
|
@ -83,7 +84,7 @@ class Value:
|
|||
def __str__(self) -> str:
|
||||
fmt = self.fmt
|
||||
if math.isnan(self._value):
|
||||
return (f"NaN{fmt.space_str}{self._unit}")
|
||||
return (f"-{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 "") +
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# 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 math
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
# Import targets to be tested
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
|
||||
SINEWAVE = [math.sin(x/45 * math.pi) for x in range(360)]
|
||||
|
||||
|
||||
class AnalyticsTools(unittest.TestCase):
|
||||
|
||||
def test_zero_crossings(self):
|
||||
self.assertEqual(at.zero_crossings(SINEWAVE),
|
||||
[45, 90, 135, 180, 225, 270, 315])
|
||||
self.assertEqual(at.zero_crossings([]), [])
|
||||
|
||||
def test_maxima(self):
|
||||
self.assertEqual(at.maxima(SINEWAVE), [112, 202, 292])
|
||||
self.assertEqual(at.maxima(SINEWAVE, 0.9999), [])
|
||||
self.assertEqual(at.maxima(-np.array(SINEWAVE)), [67, 157, 247])
|
||||
|
||||
def test_minima(self):
|
||||
self.assertEqual(at.minima(SINEWAVE), [67, 157, 247])
|
||||
self.assertEqual(at.minima(SINEWAVE, -0.9999), [])
|
||||
self.assertEqual(at.minima(-np.array(SINEWAVE)), [112, 202, 292])
|
||||
|
||||
def test_take_from_idx(self):
|
||||
self.assertEqual(
|
||||
at.take_from_idx(SINEWAVE, 109, lambda i: i[1] > 0.9),
|
||||
[107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118])
|
||||
|
||||
def test_center_from_idx(self):
|
||||
self.assertEqual(at.center_from_idx(SINEWAVE, 200), 22)
|
||||
self.assertEqual(at.center_from_idx(SINEWAVE, 200, .5), 202)
|
||||
|
||||
def test_cut_off_left(self):
|
||||
self.assertEqual(at.cut_off_left(SINEWAVE, 210, 1, 0.4), 189)
|
||||
|
||||
def test_cut_off_right(self):
|
||||
self.assertEqual(at.cut_off_right(SINEWAVE, 210, 1, 0.4), 216)
|
||||
|
||||
def test_dip_cut_offs(self):
|
||||
self.assertEqual(at.dip_cut_offs(SINEWAVE, .8, .9), (47, 358))
|
||||
self.assertEqual(at.dip_cut_offs(SINEWAVE[:90], .8, .9), (47, 88))
|
|
@ -144,6 +144,7 @@ class TestRFToolsDatapoint(unittest.TestCase):
|
|||
self.dp75 = Datapoint(100000, 0.2, 0)
|
||||
self.dp_im50 = Datapoint(100000, 0, 1)
|
||||
self.dp_ill = Datapoint(100000, 1.1, 0)
|
||||
self.dp_div0 = Datapoint(100000, 0.0, 1.0)
|
||||
|
||||
def test_properties(self):
|
||||
self.assertEqual(self.dp.z, complex(0.1091, 0.3118))
|
||||
|
@ -166,3 +167,10 @@ class TestRFToolsDatapoint(unittest.TestCase):
|
|||
self.assertAlmostEqual(self.dp.qFactor(), 0.6999837)
|
||||
self.assertAlmostEqual(self.dp.capacitiveEquivalent(), -4.54761539e-08)
|
||||
self.assertAlmostEqual(self.dp.inductiveEquivalent(), 5.57001e-05)
|
||||
self.assertAlmostEqual(self.dp.shuntImpedance(),
|
||||
complex(-6.18740998e-04, 8.749362528))
|
||||
self.assertAlmostEqual(self.dp.seriesImpedance(),
|
||||
complex(-2.02067318e-2, -285.7351012))
|
||||
self.assertAlmostEqual(self.dp0.shuntImpedance(), 0)
|
||||
self.assertAlmostEqual(self.dp0.seriesImpedance(), math.inf)
|
||||
self.assertAlmostEqual(self.dp50.shuntImpedance(), math.inf)
|
||||
|
|
|
@ -81,7 +81,7 @@ class TestTSIToolsValue(unittest.TestCase):
|
|||
self.assertEqual(str(Value(1e24)), "1.00000Y")
|
||||
self.assertEqual(str(Value(1e27)), "\N{INFINITY}")
|
||||
self.assertEqual(str(Value(-1e27)), "-\N{INFINITY}")
|
||||
self.assertEqual(str(Value(nan)), "NaN")
|
||||
self.assertEqual(str(Value(nan)), "-")
|
||||
self.assertEqual(float(Value(1e27)), 1e27)
|
||||
self.assertEqual(
|
||||
str(Value(11, fmt=Format(printable_max=10))), '')
|
||||
|
|
Ładowanie…
Reference in New Issue