# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 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
from typing import ClassVar
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Signal
from PySide6.QtGui import QColorConstants
from .. import RFTools
from ..Controls.SweepControl import FrequencyInputWidget
from ..Formatting import (
format_capacitance,
format_complex_adm,
format_complex_imp,
format_frequency_space,
format_gain,
format_group_delay,
format_inductance,
format_magnitude,
format_phase,
format_q_factor,
format_resistance,
format_vswr,
format_wavelength,
parse_frequency,
)
from .Values import TYPES, Value, default_label_ids
COLORS = (
QtGui.QColor(QColorConstants.DarkGray),
QtGui.QColor(255, 0, 0),
QtGui.QColor(0, 255, 0),
QtGui.QColor(0, 0, 255),
QtGui.QColor(0, 255, 255),
QtGui.QColor(255, 0, 255),
QtGui.QColor(255, 255, 0),
)
[docs]
class MarkerLabel(QtWidgets.QLabel):
def __init__(self, name):
super().__init__("")
self.name = name
[docs]
class Marker(QtCore.QObject, Value):
_instances = 0
colored_text = True
location = -1
returnloss_is_positive = False
updated = Signal(object)
active_labels: ClassVar[list[str]] = []
[docs]
@classmethod
def count(cls):
return cls._instances
def __init__(self, name: str = "", qsettings: QtCore.QSettings = None):
super().__init__()
self.qsettings = qsettings
self.name = name
self.color = QtGui.QColor()
self.index = 0
if self.qsettings:
Marker._instances += 1
Marker.active_labels = self.qsettings.value(
"MarkerFields", defaultValue=default_label_ids()
)
self.index = Marker._instances
if not self.name:
self.name = f"Marker {Marker._instances}"
self.frequencyInput = FrequencyInputWidget()
self.frequencyInput.setMinimumHeight(20)
self.frequencyInput.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
self.frequencyInput.editingFinished.connect(
lambda: self.setFrequency(
self.frequencyInput.get_freq()
)
)
###############################################################
# Data display labels
###############################################################
self.label = {
label.label_id: MarkerLabel(label.name) for label in TYPES
}
self.label["actualfreq"].setMinimumWidth(100)
self.label["returnloss"].setMinimumWidth(80)
###############################################################
# Marker control layout
###############################################################
self.btnColorPicker = QtWidgets.QPushButton("█")
self.btnColorPicker.setMinimumHeight(20)
self.btnColorPicker.setFixedWidth(20)
self.btnColorPicker.clicked.connect(
lambda: self.setColor(
QtWidgets.QColorDialog.getColor(
self.color,
options=QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel,
)
)
)
self.isMouseControlledRadioButton = QtWidgets.QRadioButton()
self.layout = QtWidgets.QHBoxLayout()
self.layout.addWidget(self.frequencyInput)
self.layout.addWidget(self.btnColorPicker)
self.layout.addWidget(self.isMouseControlledRadioButton)
###############################################################
# Data display layout
###############################################################
self.group_box = QtWidgets.QGroupBox(self.name)
self.group_box.setMaximumWidth(340)
box_layout = QtWidgets.QHBoxLayout(self.group_box)
try:
self.setColor(
self.qsettings.value(
f"Marker{self.count()}Color", COLORS[self.count()]
)
)
except AttributeError: # happens when qsettings == None
self.setColor(COLORS[1])
except IndexError:
self.setColor(COLORS[0])
line = QtWidgets.QFrame()
line.setFrameShape(QtWidgets.QFrame.Shape.VLine)
# line only if more then 3 selected
self.left_form = QtWidgets.QFormLayout()
self.left_form.setVerticalSpacing(0)
self.right_form = QtWidgets.QFormLayout()
self.right_form.setVerticalSpacing(0)
box_layout.addLayout(self.left_form)
box_layout.addWidget(line)
box_layout.addLayout(self.right_form)
self.buildForm()
def __del__(self):
if self.qsettings:
Marker._instances -= 1
def _add_active_labels(self, label_id, form):
if label_id in self.label:
form.addRow(f"{self.label[label_id].name}:", self.label[label_id])
self.label[label_id].show()
def _size_str(self) -> str:
return str(self.group_box.font().pointSize())
[docs]
def update_settings(self):
self.qsettings.setValue(f"Marker{self.index}Color", self.color)
[docs]
def setScale(self, scale):
self.group_box.setMaximumWidth(int(340 * scale))
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
self.label["returnloss"].setMinimumWidth(int(80 * scale))
if self.colored_text:
self.group_box.setStyleSheet(
f"QGroupBox {{ color: {self.color.name()}; "
f"font-size: {self._size_str()}}};"
)
else:
self.group_box.setStyleSheet(
f"QGroupBox {{ font-size: {self._size_str()}}};"
)
[docs]
def setFrequency(self, frequency):
self.freq = parse_frequency(frequency)
self.frequencyInput.setText(frequency)
self.updated.emit(self)
[docs]
def setFieldSelection(self, fields):
self.active_labels = fields[:]
self.buildForm()
[docs]
def setColor(self, color):
if color.isValid():
self.color = color
p = self.btnColorPicker.palette()
p.setColor(QtGui.QPalette.ColorRole.ButtonText, self.color)
self.btnColorPicker.setPalette(p)
if self.colored_text:
self.group_box.setStyleSheet(
f"QGroupBox {{ color: {color.name()}; "
f"font-size: {self._size_str()}}};"
)
else:
self.group_box.setStyleSheet(
f"QGroupBox {{ font-size: {self._size_str()}}};"
)
[docs]
def setColoredText(self, colored_text):
self.colored_text = colored_text
self.setColor(self.color)
[docs]
def getRow(self):
return QtWidgets.QLabel(self.name), self.layout
[docs]
def findLocation(self, data: list[RFTools.Datapoint]):
self.location = -1
self.frequencyInput.nextFrequency = -1
self.frequencyInput.previousFrequency = -1
datasize = len(data)
if datasize == 0:
# Set the frequency before loading any data
return
min_freq = data[0].freq
max_freq = data[-1].freq
lower_stepsize = data[1].freq - data[0].freq
upper_stepsize = data[-1].freq - data[-2].freq
# We are outside the bounds of the data, so we can't put in a marker
if (
self.freq + lower_stepsize / 2 < min_freq
or self.freq - upper_stepsize / 2 > max_freq
):
return
min_distance = max_freq
for i, item in enumerate(data):
if abs(item.freq - self.freq) <= min_distance:
min_distance = abs(item.freq - self.freq)
else:
# We have now started moving away from the nearest point
self.location = i - 1
if i < datasize:
self.frequencyInput.nextFrequency = item.freq
if i >= 2:
self.frequencyInput.previousFrequency = data[i - 2].freq
return
# If we still didn't find a best spot, it was the last value
self.location = datasize - 1
self.frequencyInput.previousFrequency = data[-2].freq
[docs]
def get_data_layout(self) -> QtWidgets.QGroupBox:
return self.group_box
[docs]
def resetLabels(self):
for v in self.label.values():
v.setText("")
[docs]
def updateLabels(
self, s11: list[RFTools.Datapoint], s21: list[RFTools.Datapoint]
):
if not s11:
return
if self.location == -1: # initial position
try:
location = (self.index - 1) / (
(self._instances - 1) * (len(s11) - 1)
)
self.location = int(location)
except ZeroDivisionError:
self.location = 0
try:
_s11 = s11[self.location]
except IndexError:
self.location = 0
return
self.frequencyInput.setText(_s11.freq)
self.store(self.location, s11, s21)
imp = _s11.impedance()
cap_str = format_capacitance(
RFTools.impedance_to_capacitance(imp, _s11.freq)
)
ind_str = format_inductance(
RFTools.impedance_to_inductance(imp, _s11.freq)
)
imp_p = RFTools.serial_to_parallel(imp)
cap_p_str = format_capacitance(
RFTools.impedance_to_capacitance(imp_p, _s11.freq)
)
ind_p_str = format_inductance(
RFTools.impedance_to_inductance(imp_p, _s11.freq)
)
x_str = cap_str if imp.imag < 0 else ind_str
x_p_str = cap_p_str if imp_p.imag < 0 else ind_p_str
self.label["actualfreq"].setText(format_frequency_space(_s11.freq))
self.label["lambda"].setText(format_wavelength(_s11.wavelength))
self.label["admittance"].setText(format_complex_adm(imp))
self.label["impedance"].setText(format_complex_imp(imp))
self.label["parc"].setText(cap_p_str)
self.label["parl"].setText(ind_p_str)
self.label["parlc"].setText(x_p_str)
self.label["parr"].setText(format_resistance(imp_p.real))
self.label["returnloss"].setText(
format_gain(_s11.gain, self.returnloss_is_positive)
)
self.label["s11groupdelay"].setText(
format_group_delay(RFTools.groupDelay(s11, self.location))
)
self.label["s11mag"].setText(format_magnitude(abs(_s11.z)))
self.label["s11phase"].setText(format_phase(_s11.phase))
self.label["s11polar"].setText(
f"{round(abs(_s11.z), 2)!s}∠{format_phase(_s11.phase)}"
)
self.label["s11q"].setText(format_q_factor(_s11.qFactor()))
self.label["s11z"].setText(format_resistance(abs(imp)))
self.label["serc"].setText(cap_str)
self.label["serl"].setText(ind_str)
self.label["serlc"].setText(x_str)
self.label["serr"].setText(format_resistance(imp.real))
self.label["vswr"].setText(format_vswr(_s11.vswr))
if len(s21) == len(s11):
_s21 = s21[self.location]
self.label["s21gain"].setText(format_gain(_s21.gain))
self.label["s21groupdelay"].setText(
format_group_delay(RFTools.groupDelay(s21, self.location) / 2)
)
self.label["s21mag"].setText(format_magnitude(abs(_s21.z)))
self.label["s21phase"].setText(format_phase(_s21.phase))
self.label["s21polar"].setText(
f"{round(abs(_s21.z), 2)!s}∠{format_phase(_s21.phase)}"
)
self.label["s21magshunt"].setText(
format_magnitude(abs(_s21.shuntImpedance()))
)
self.label["s21magseries"].setText(
format_magnitude(abs(_s21.seriesImpedance()))
)
self.label["s21realimagshunt"].setText(
format_complex_imp(_s21.shuntImpedance(), allow_negative=True)
)
self.label["s21realimagseries"].setText(
format_complex_imp(_s21.seriesImpedance(), allow_negative=True)
)