refactored BandStopAnalysis

pull/550/head
Holger Müller 2022-09-19 19:21:22 +02:00
rodzic a73028e2c3
commit 24a4ca0ffa
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 2FDB49E81EAE6622
8 zmienionych plików z 246 dodań i 374 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -72,7 +72,8 @@ class Value:
self.fmt = fmt
if isinstance(value, str):
self._value = Decimal(math.nan)
self.parse(value)
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 "") +

Wyświetl plik

@ -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))

Wyświetl plik

@ -143,7 +143,8 @@ class TestRFToolsDatapoint(unittest.TestCase):
self.dp50 = Datapoint(100000, 1, 0)
self.dp75 = Datapoint(100000, 0.2, 0)
self.dp_im50 = Datapoint(100000, 0, 1)
self.dp_ill = Datapoint(100000, 1.1, 0)
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)

Wyświetl plik

@ -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))), '')