nanovna-saver/src/NanoVNASaver/Marker/Widget.py

387 wiersze
14 KiB
Python

# NanoVNASaver
2020-06-25 17:52:30 +00:00
#
# A python program to view and export Touchstone data from a NanoVNA
2020-06-25 17:52:30 +00:00
# Copyright (C) 2019, 2020 Rune B. Broberg
2021-06-30 05:21:14 +00:00
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
2019-08-29 13:10:35 +00:00
#
# 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
2023-03-12 07:02:58 +00:00
from PyQt6 import QtGui, QtWidgets, QtCore
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QColorConstants
2019-12-07 22:22:54 +00:00
from NanoVNASaver import RFTools
2020-02-14 18:34:24 +00:00
from NanoVNASaver.Formatting import (
format_capacitance,
format_complex_adm,
2020-02-14 18:34:24 +00:00
format_complex_imp,
2020-06-21 18:54:23 +00:00
format_frequency_space,
2020-02-14 18:34:24 +00:00
format_gain,
format_group_delay,
format_inductance,
format_magnitude,
format_phase,
format_q_factor,
format_resistance,
format_vswr,
2020-07-25 18:35:16 +00:00
format_wavelength,
2020-06-21 17:54:00 +00:00
parse_frequency,
2020-02-14 18:34:24 +00:00
)
2019-12-17 19:13:28 +00:00
from NanoVNASaver.Inputs import MarkerFrequencyInputWidget as FrequencyInput
2020-02-14 18:34:24 +00:00
from NanoVNASaver.Marker.Values import TYPES, Value, default_label_ids
COLORS = (
2023-03-12 07:02:58 +00:00
QtGui.QColor(QColorConstants.DarkGray),
2020-02-14 18:34:24 +00:00
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),
2020-02-14 18:34:24 +00:00
)
class MarkerLabel(QtWidgets.QLabel):
def __init__(self, name):
super().__init__("")
self.name = name
2020-02-14 18:34:24 +00:00
class Marker(QtCore.QObject, Value):
_instances = 0
coloredText = True
location = -1
returnloss_is_positive = False
updated = pyqtSignal(object)
2020-02-14 18:34:24 +00:00
active_labels = []
2020-02-14 18:34:24 +00:00
@classmethod
def count(cls):
return cls._instances
2020-02-14 18:34:24 +00:00
def __init__(self, name: str = "", qsettings: QtCore.QSettings = None):
super().__init__()
2020-02-14 18:34:24 +00:00
self.qsettings = qsettings
self.name = name
self.color = QtGui.QColor()
self.index = 0
2020-02-14 18:34:24 +00:00
if self.qsettings:
Marker._instances += 1
Marker.active_labels = self.qsettings.value(
2023-03-08 08:40:39 +00:00
"MarkerFields", defaultValue=default_label_ids()
)
2020-02-14 18:34:24 +00:00
self.index = Marker._instances
if not self.name:
self.name = f"Marker {Marker._instances}"
self.frequencyInput = FrequencyInput()
self.frequencyInput.setMinimumHeight(20)
2023-03-12 07:02:58 +00:00
self.frequencyInput.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
2020-07-27 15:05:11 +00:00
self.frequencyInput.editingFinished.connect(
lambda: self.setFrequency(
2023-03-08 08:40:39 +00:00
parse_frequency(self.frequencyInput.text())
)
)
2020-02-14 18:34:24 +00:00
###############################################################
2019-12-17 19:13:28 +00:00
# Data display labels
2020-02-14 18:34:24 +00:00
###############################################################
2022-09-15 18:08:23 +00:00
self.label = {
label.label_id: MarkerLabel(label.name) for label in TYPES
}
2023-03-08 08:40:39 +00:00
self.label["actualfreq"].setMinimumWidth(100)
self.label["returnloss"].setMinimumWidth(80)
2019-11-27 21:05:24 +00:00
###############################################################
# Marker control layout
2019-11-27 21:05:24 +00:00
###############################################################
self.btnColorPicker = QtWidgets.QPushButton("")
self.btnColorPicker.setMinimumHeight(20)
self.btnColorPicker.setFixedWidth(20)
2019-11-27 21:05:24 +00:00
self.btnColorPicker.clicked.connect(
2023-03-08 08:40:39 +00:00
lambda: self.setColor(
QtWidgets.QColorDialog.getColor(
2023-03-12 07:02:58 +00:00
self.color,
options=QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel,
2023-03-08 08:40:39 +00:00
)
)
2019-11-27 21:05:24 +00:00
)
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
###############################################################
2019-11-27 21:05:24 +00:00
self.group_box = QtWidgets.QGroupBox(self.name)
self.group_box.setMaximumWidth(340)
box_layout = QtWidgets.QHBoxLayout(self.group_box)
2020-02-14 18:34:24 +00:00
try:
self.setColor(
self.qsettings.value(
2023-03-08 08:40:39 +00:00
f"Marker{self.count()}Color", COLORS[self.count()]
)
)
2020-02-14 18:34:24 +00:00
except AttributeError: # happens when qsettings == None
self.setColor(COLORS[1])
except IndexError:
self.setColor(COLORS[0])
line = QtWidgets.QFrame()
2023-03-12 07:02:58 +00:00
line.setFrameShape(QtWidgets.QFrame.Shape.VLine)
2020-02-14 18:34:24 +00:00
# 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()
2020-02-14 18:34:24 +00:00
def __del__(self):
if self.qsettings:
Marker._instances -= 1
def _add_active_labels(self, label_id, form):
if label_id in self.label:
2023-03-08 08:40:39 +00:00
form.addRow(f"{self.label[label_id].name}:", self.label[label_id])
2020-02-14 18:34:24 +00:00
self.label[label_id].show()
2019-11-27 21:05:24 +00:00
def _size_str(self) -> str:
return str(self.group_box.font().pointSize())
2020-02-14 18:34:24 +00:00
def update_settings(self):
self.qsettings.setValue(f"Marker{self.index}Color", self.color)
2019-11-27 21:05:24 +00:00
2019-11-03 18:27:49 +00:00
def setScale(self, scale):
self.group_box.setMaximumWidth(int(340 * scale))
2023-03-08 08:40:39 +00:00
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
self.label["returnloss"].setMinimumWidth(int(80 * scale))
2019-11-03 18:27:49 +00:00
if self.coloredText:
2019-11-27 21:05:24 +00:00
self.group_box.setStyleSheet(
f"QGroupBox {{ color: {self.color.name()}; "
2019-11-27 21:05:24 +00:00
f"font-size: {self._size_str()}}};"
)
2019-11-03 18:27:49 +00:00
else:
2019-11-27 21:05:24 +00:00
self.group_box.setStyleSheet(
f"QGroupBox {{ font-size: {self._size_str()}}};"
)
2019-11-03 18:27:49 +00:00
def buildForm(self):
while self.left_form.count() > 0:
old_row = self.left_form.takeRow(0)
old_row.fieldItem.widget().hide()
old_row.labelItem.widget().hide()
while self.right_form.count() > 0:
old_row = self.right_form.takeRow(0)
old_row.fieldItem.widget().hide()
old_row.labelItem.widget().hide()
2020-02-14 18:34:24 +00:00
if len(self.active_labels) <= 3:
for label_id in self.active_labels:
self._add_active_labels(label_id, self.left_form)
else:
2022-09-15 18:08:23 +00:00
left_half = math.ceil(len(self.active_labels) / 2)
2020-02-14 18:34:24 +00:00
right_half = len(self.active_labels)
for i in range(left_half):
2020-02-14 18:34:24 +00:00
label_id = self.active_labels[i]
self._add_active_labels(label_id, self.left_form)
for i in range(left_half, right_half):
2020-02-14 18:34:24 +00:00
label_id = self.active_labels[i]
self._add_active_labels(label_id, self.right_form)
def setFrequency(self, frequency):
2020-06-21 17:54:00 +00:00
self.freq = parse_frequency(frequency)
2022-09-20 17:52:34 +00:00
self.frequencyInput.setText(frequency)
self.updated.emit(self)
def setFieldSelection(self, fields):
2020-02-14 18:34:24 +00:00
self.active_labels = fields[:]
self.buildForm()
def setColor(self, color):
if color.isValid():
self.color = color
p = self.btnColorPicker.palette()
2023-03-13 11:08:33 +00:00
p.setColor(QtGui.QPalette.ColorRole.ButtonText, self.color)
self.btnColorPicker.setPalette(p)
if self.coloredText:
self.group_box.setStyleSheet(
f"QGroupBox {{ color: {color.name()}; "
2019-11-27 21:05:24 +00:00
f"font-size: {self._size_str()}}};"
)
else:
2019-11-27 21:05:24 +00:00
self.group_box.setStyleSheet(
f"QGroupBox {{ font-size: {self._size_str()}}};"
)
def setColoredText(self, colored_text):
self.coloredText = colored_text
self.setColor(self.color)
def getRow(self):
2019-09-23 10:53:40 +00:00
return QtWidgets.QLabel(self.name), self.layout
def findLocation(self, data: list[RFTools.Datapoint]):
self.location = -1
2019-11-27 21:05:24 +00:00
self.frequencyInput.nextFrequency = -1
self.frequencyInput.previousFrequency = -1
datasize = len(data)
if datasize == 0:
# Set the frequency before loading any data
return
2019-10-04 10:51:20 +00:00
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
2019-10-04 10:51:20 +00:00
2019-11-27 21:05:24 +00:00
# We are outside the bounds of the data, so we can't put in a marker
2023-03-08 08:40:39 +00:00
if (
self.freq + lower_stepsize / 2 < min_freq
or self.freq - upper_stepsize / 2 > max_freq
):
2019-10-04 10:51:20 +00:00
return
min_distance = max_freq
for i, item in enumerate(data):
2020-02-14 18:34:24 +00:00
if abs(item.freq - self.freq) <= min_distance:
min_distance = abs(item.freq - self.freq)
2019-10-04 10:51:20 +00:00
else:
# We have now started moving away from the nearest point
2022-09-15 18:08:23 +00:00
self.location = i - 1
if i < datasize:
self.frequencyInput.nextFrequency = item.freq
if i >= 2:
2022-09-15 18:08:23 +00:00
self.frequencyInput.previousFrequency = data[i - 2].freq
2019-10-04 10:51:20 +00:00
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
2019-10-04 10:51:20 +00:00
2020-07-19 14:38:03 +00:00
def get_data_layout(self) -> QtWidgets.QGroupBox:
return self.group_box
2019-11-03 18:27:49 +00:00
def resetLabels(self):
2020-02-14 18:34:24 +00:00
for v in self.label.values():
v.setText("")
2023-03-08 08:40:39 +00:00
def updateLabels(
self, s11: list[RFTools.Datapoint], s21: list[RFTools.Datapoint]
2023-03-08 08:40:39 +00:00
):
2021-06-22 20:07:36 +00:00
if not s11:
2020-07-24 10:18:31 +00:00
return
if self.location == -1: # initial position
2020-07-28 20:07:35 +00:00
try:
2022-09-15 18:08:23 +00:00
location = (self.index - 1) / (
2023-03-08 08:40:39 +00:00
(self._instances - 1) * (len(s11) - 1)
)
2020-07-28 20:07:35 +00:00
self.location = int(location)
except ZeroDivisionError:
2020-07-24 10:18:31 +00:00
self.location = 0
try:
2021-06-22 20:07:36 +00:00
_s11 = s11[self.location]
except IndexError:
self.location = 0
return
2020-02-14 18:34:24 +00:00
2021-06-22 20:07:36 +00:00
self.frequencyInput.setText(_s11.freq)
self.store(self.location, s11, s21)
2019-12-07 22:22:54 +00:00
2021-06-22 20:07:36 +00:00
imp = _s11.impedance()
cap_str = format_capacitance(
2023-03-08 08:40:39 +00:00
RFTools.impedance_to_capacitance(imp, _s11.freq)
)
2021-06-22 20:07:36 +00:00
ind_str = format_inductance(
2023-03-08 08:40:39 +00:00
RFTools.impedance_to_inductance(imp, _s11.freq)
)
2019-12-07 22:22:54 +00:00
imp_p = RFTools.serial_to_parallel(imp)
2021-06-22 20:07:36 +00:00
cap_p_str = format_capacitance(
2023-03-08 08:40:39 +00:00
RFTools.impedance_to_capacitance(imp_p, _s11.freq)
)
2021-06-22 20:07:36 +00:00
ind_p_str = format_inductance(
2023-03-08 08:40:39 +00:00
RFTools.impedance_to_inductance(imp_p, _s11.freq)
)
2019-12-07 22:22:54 +00:00
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
2023-03-08 08:40:39 +00:00
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"{str(round(abs(_s11.z), 2))}{format_phase(_s11.phase)}"
)
2023-03-08 08:40:39 +00:00
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))
2021-06-22 20:07:36 +00:00
if len(s21) == len(s11):
_s21 = s21[self.location]
2023-03-08 08:40:39 +00:00
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"{str(round(abs(_s21.z), 2))}{format_phase(_s21.phase)}"
)
2023-03-08 08:40:39 +00:00
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)
)