Basic implementation of calibration.

This commit is contained in:
jupfi 2023-08-08 17:09:28 +02:00
parent acb328eea2
commit e2eafb4e6d
6 changed files with 394 additions and 26 deletions

View file

@ -1,4 +1,5 @@
import logging
import numpy as np
from serial.tools.list_ports import comports
from PyQt6 import QtSerialPort
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
@ -34,9 +35,16 @@ class AutoTMController(ModuleController):
def start_frequency_sweep(self, start_frequency : float, stop_frequency : float) -> None:
""" This starts a frequency sweep on the device in the specified range."""
logger.debug("Starting frequency sweep from %s to %s", start_frequency, stop_frequency)
# We create the frequency sweep spinner dialog
self.module.model.clear_data_points()
self.module.view.create_frequency_sweep_spinner_dialog()
# Print the command 'f <start> <stop>' to the serial connection
command = "f %s %s" % (start_frequency, stop_frequency)
self.module.model.serial.write(command.encode('utf-8'))
try:
command = "f %s %s" % (start_frequency, stop_frequency)
self.module.model.serial.write(command.encode('utf-8'))
except AttributeError:
logger.error("Could not start frequency sweep. No device connected.")
def on_ready_read(self) -> None:
"""This method is called when data is received from the serial connection. """
@ -44,11 +52,108 @@ class AutoTMController(ModuleController):
while serial.canReadLine():
text = serial.readLine().data().decode()
text = text.rstrip('\r\n')
logger.debug("Received data: %s", text)
if text.startswith("f"):
# logger.debug("Received data: %s", text)
# If the text starts with 'f' and the frequency sweep spinner is visible we know that the data is a data point
if text.startswith("f") and self.module.view.frequency_sweep_spinner.isVisible():
text = text[1:].split("r")
frequency = float(text[0])
return_loss = float(text[1])
self.module.model.add_data_point(frequency, return_loss)
return_loss, phase = map(float, text[1].split("p"))
self.module.model.add_data_point(frequency, return_loss, phase)
elif text.startswith("r") and self.module.model.active_calibration == None:
logger.debug("Measurement finished")
self.module.view.plot_data()
self.module.view.frequency_sweep_spinner.hide()
elif text.startswith("r") and self.module.model.active_calibration == "short":
logger.debug("Short calibration finished")
self.module.model.short_calibration = self.module.model.data_points.copy()
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
elif text.startswith("r") and self.module.model.active_calibration == "open":
logger.debug("Open calibration finished")
self.module.model.open_calibration = self.module.model.data_points.copy()
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
elif text.startswith("r") and self.module.model.active_calibration == "load":
logger.debug("Load calibration finished")
self.module.model.load_calibration = self.module.model.data_points.copy()
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
else:
self.module.view.add_info_text(text)
def on_short_calibration(self, start_frequency : float, stop_frequency : float) -> None:
"""This method is called when the short calibration button is pressed.
It starts a frequency sweep in the specified range and then starts a short calibration.
"""
logger.debug("Starting short calibration")
self.module.model.init_short_calibration()
self.start_frequency_sweep(start_frequency, stop_frequency)
def on_open_calibration(self, start_frequency : float, stop_frequency : float) -> None:
"""This method is called when the open calibration button is pressed.
It starts a frequency sweep in the specified range and then starts an open calibration.
"""
logger.debug("Starting open calibration")
self.module.model.init_open_calibration()
self.start_frequency_sweep(start_frequency, stop_frequency)
def on_load_calibration(self, start_frequency : float, stop_frequency : float) -> None:
"""This method is called when the load calibration button is pressed.
It starts a frequency sweep in the specified range and then loads a calibration.
"""
logger.debug("Starting load calibration")
self.module.model.init_load_calibration()
self.start_frequency_sweep(start_frequency, stop_frequency)
def calculate_calibration(self) -> None:
"""This method is called when the calculate calibration button is pressed.
It calculates the calibration from the short, open and calibration data points.
"""
logger.debug("Calculating calibration")
# First we check if the short and open calibration data points are available
if self.module.model.short_calibration == None:
logger.error("No short calibration data points available")
return
if self.module.model.open_calibration == None:
logger.error("No open calibration data points available")
return
if self.module.model.load_calibration == None:
logger.error("No load calibration data points available")
return
# Then we check if the short, open and load calibration data points have the same length
if len(self.module.model.short_calibration) != len(self.module.model.open_calibration) or len(self.module.model.short_calibration) != len(self.module.model.load_calibration):
logger.error("The short, open and load calibration data points do not have the same length")
return
# Then we calculate the calibration
ideal_gamma_short = -1
ideal_gamma_open = 1
ideal_gamma_load = 0
short_calibration = [10 **(-returnloss_s[1] /6 / 24 / 20) for returnloss_s in self.module.model.short_calibration]
open_calibration = [10 **(-returnloss_o[1] / 6 / 24 / 20) for returnloss_o in self.module.model.open_calibration]
load_calibration = [10 **(-returnloss_l[1] / 6 / 24 / 20) for returnloss_l in self.module.model.load_calibration]
e_00s = []
e11s = []
delta_es = []
for gamma_s, gamma_o, gamma_l in zip(short_calibration, open_calibration, load_calibration):
A = np.array([
[1, ideal_gamma_short * gamma_s, -ideal_gamma_short],
[1, ideal_gamma_open * gamma_o, -ideal_gamma_open],
[1, ideal_gamma_load * gamma_l, -ideal_gamma_load]
])
B = np.array([gamma_s, gamma_o, gamma_l])
# Solve the system
e_00, e11, delta_e = np.linalg.lstsq(A, B, rcond=None)[0]
e_00s.append(e_00)
e11s.append(e11)
delta_es.append(delta_e)
self.module.model.calibration = (e_00s, e11s, delta_es)

View file

@ -1,22 +1,29 @@
import serial
import logging
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtSerialPort import QSerialPort
from nqrduck.module.module_model import ModuleModel
logger = logging.getLogger(__name__)
class AutoTMModel(ModuleModel):
available_devices_changed = pyqtSignal(list)
serial_changed = pyqtSignal(QSerialPort)
data_points_changed = pyqtSignal(list)
short_calibration_finished = pyqtSignal(list)
open_calibration_finished = pyqtSignal(list)
load_calibration_finished = pyqtSignal(list)
def __init__(self, module) -> None:
super().__init__(module)
self.data_points = []
self.active_calibration = None
@property
def available_devices(self):
return self._available_devices
@available_devices.setter
def available_devices(self, value):
self._available_devices = value
@ -25,18 +32,80 @@ class AutoTMModel(ModuleModel):
@property
def serial(self):
return self._serial
@serial.setter
def serial(self, value):
self._serial = value
self.serial_changed.emit(value)
def add_data_point(self, frequency : float, return_loss : float) -> None:
"""Add a data point to the model. """
self.data_points.append((frequency, return_loss))
def add_data_point(self, frequency: float, return_loss: float, phase : float) -> None:
"""Add a data point to the model."""
self.data_points.append((frequency, return_loss, phase))
self.data_points_changed.emit(self.data_points)
def clear_data_points(self) -> None:
"""Clear all data points from the model. """
"""Clear all data points from the model."""
self.data_points.clear()
self.data_points_changed.emit(self.data_points)
@property
def active_calibration(self):
return self._active_calibration
@active_calibration.setter
def active_calibration(self, value):
self._active_calibration = value
@property
def short_calibration(self):
return self._short_calibration
@short_calibration.setter
def short_calibration(self, value):
logger.debug("Setting short calibration")
self._short_calibration = value
self.short_calibration_finished.emit(value)
def init_short_calibration(self):
"""This method is called when a frequency sweep has been started for a short calibration in this way the module knows that the next data points are for a short calibration."""
self.active_calibration = "short"
self.clear_data_points()
@property
def open_calibration(self):
return self._open_calibration
@open_calibration.setter
def open_calibration(self, value):
logger.debug("Setting open calibration")
self._open_calibration = value
self.open_calibration_finished.emit(value)
def init_open_calibration(self):
"""This method is called when a frequency sweep has been started for an open calibration in this way the module knows that the next data points are for an open calibration."""
self.active_calibration = "open"
self.clear_data_points()
@property
def load_calibration(self):
return self._load_calibration
@load_calibration.setter
def load_calibration(self, value):
logger.debug("Setting load calibration")
self._load_calibration = value
self.load_calibration_finished.emit(value)
def init_load_calibration(self):
"""This method is called when a frequency sweep has been started for a load calibration in this way the module knows that the next data points are for a load calibration."""
self.active_calibration = "load"
self.clear_data_points()
@property
def calibration(self):
return self._calibration
@calibration.setter
def calibration(self, value):
logger.debug("Setting calibration")
self._calibration = value

View file

@ -163,7 +163,7 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<widget class="QPushButton" name="calibrationButton">
<property name="text">
<string>Calibrate</string>
</property>

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,9 +1,12 @@
import logging
from datetime import datetime
from pathlib import Path
from PyQt6.QtGui import QMovie
from PyQt6.QtSerialPort import QSerialPort
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QApplication
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QApplication, QHBoxLayout, QLineEdit, QPushButton, QDialog
from PyQt6.QtCore import pyqtSlot, Qt
from nqrduck.module.module_view import ModuleView
from nqrduck.contrib.mplwidget import MplWidget
from .widget import Ui_Form
logger = logging.getLogger(__name__)
@ -39,8 +42,8 @@ class AutoTMView(ModuleView):
float(self._ui_form.stopEdit.text())
))
# Connect the data points changed signal to the on_data_points_changed slot
self.module.model.data_points_changed.connect(self.on_data_points_changed)
# On clicking of the calibration button call the on_calibration_button_clicked method
self._ui_form.calibrationButton.clicked.connect(self.on_calibration_button_clicked)
# Add a vertical layout to the info box
self._ui_form.scrollAreaWidgetContents.setLayout(QVBoxLayout())
@ -68,6 +71,14 @@ class AutoTMView(ModuleView):
ax.set_ylim(-100, 0)
self._ui_form.S11Plot.canvas.draw()
def on_calibration_button_clicked(self) -> None:
"""This method is called when the calibration button is clicked.
It opens the calibration window.
"""
logger.debug("Calibration button clicked")
self.calibration_window = self.CalibrationWindow(self.module)
self.calibration_window.show()
@pyqtSlot(list)
def on_available_devices_changed(self, available_devices : list) -> None:
"""Update the available devices list in the view. """
@ -104,15 +115,32 @@ class AutoTMView(ModuleView):
self._ui_form.connectionLabel.setText("Disconnected")
logger.debug("Updated serial connection label")
@pyqtSlot(list)
def on_data_points_changed(self, data_points : list) -> None:
def plot_data(self) -> None:
"""Update the S11 plot with the current data points.
Args:
data_points (list): List of data points to plot.
"""
x = [data_point[0] for data_point in data_points]
y = [data_point[1] for data_point in data_points]
x = [data_point[0] for data_point in self.module.model.data_points]
y = [(data_point[1] - 900) / 30 for data_point in self.module.model.data_points]
phase = [(data_point[2] - 900) / 10 for data_point in self.module.model.data_points]
# Calibration test:
#calibration = self.module.model.calibration
#e_00 = calibration[0]
#e11 = calibration[1]
#delta_e = calibration[2]
#y_corr = [(data_point - e_00[i]) / (data_point * e11[i] - delta_e[i]) for i, data_point in enumerate(y)]
#import numpy as np
#y = [data_point[1] for data_point in self.module.model.data_points]
#open_calibration = [data_point[1] for data_point in self.module.model.open_calibration]
#load_calibration = [data_point[1] for data_point in self.module.model.load_calibration]
#short_calibration = [data_point[1] for data_point in self.module.model.short_calibration]
#y_corr = np.array(y) - np.array(load_calibration)
#y_corr = y_corr - np.mean(y_corr)
ax = self._ui_form.S11Plot.canvas.ax
ax.clear()
ax.set_xlabel("Frequency (MHz)")
@ -120,6 +148,7 @@ class AutoTMView(ModuleView):
ax.set_title("S11")
ax.grid(True)
ax.plot(x, y)
ax.plot(x, phase)
# make the y axis go down instead of up
ax.invert_yaxis()
self._ui_form.S11Plot.canvas.draw()
@ -139,4 +168,169 @@ class AutoTMView(ModuleView):
text_label = QLabel(text)
text_label.setStyleSheet("font-size: 25px;")
self._ui_form.scrollAreaWidgetContents.layout().addWidget(text_label)
self._ui_form.scrollArea.verticalScrollBar().setValue(self._ui_form.scrollArea.verticalScrollBar().maximum())
self._ui_form.scrollArea.verticalScrollBar().setValue(self._ui_form.scrollArea.verticalScrollBar().maximum())
def create_frequency_sweep_spinner_dialog(self) -> None:
"""Creates a frequency sweep spinner dialog. """
self.frequency_sweep_spinner = self.FrequencySweepSpinner()
self.frequency_sweep_spinner.show()
class FrequencySweepSpinner(QDialog):
"""This class implements a spinner dialog that is shown during a frequency sweep."""
def __init__(self):
super().__init__()
self.setWindowTitle("Frequency sweep")
self.setModal(True)
self.setWindowFlag(Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
path = Path(__file__).parent
self.spinner_movie = QMovie(str(path / "resources/duck_kick.gif"))
self.spinner_label = QLabel(self)
self.spinner_label.setMovie(self.spinner_movie)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.spinner_label)
self.spinner_movie.start()
class CalibrationWindow(QWidget):
def __init__(self, module, parent=None):
super().__init__()
self.module = module
self.setParent(parent)
self.setWindowTitle("Calibration")
# Add vertical main layout
main_layout = QVBoxLayout()
# Add horizontal layout for the frequency range
frequency_layout = QHBoxLayout()
main_layout.addLayout(frequency_layout)
frequency_label = QLabel("Frequency range")
frequency_layout.addWidget(frequency_label)
start_edit = QLineEdit()
start_edit.setPlaceholderText("Start")
frequency_layout.addWidget(start_edit)
stop_edit = QLineEdit()
stop_edit.setPlaceholderText("Stop")
frequency_layout.addWidget(stop_edit)
unit_label = QLabel("MHz")
frequency_layout.addWidget(unit_label)
# Add horizontal layout for the calibration type
type_layout = QHBoxLayout()
main_layout.addLayout(type_layout)
# Add vertical layout for short calibration
short_layout = QVBoxLayout()
short_button = QPushButton("Short")
short_button.clicked.connect(lambda: self.module.controller.on_short_calibration(
float(start_edit.text()),
float(stop_edit.text())
))
# Short plot widget
self.short_plot = MplWidget()
short_layout.addWidget(self.short_plot)
short_layout.addWidget(short_button)
type_layout.addLayout(short_layout)
# Add vertical layout for open calibration
open_layout = QVBoxLayout()
open_button = QPushButton("Open")
open_button.clicked.connect(lambda: self.module.controller.on_open_calibration(
float(start_edit.text()),
float(stop_edit.text())
))
# Open plot widget
self.open_plot = MplWidget()
open_layout.addWidget(self.open_plot)
open_layout.addWidget(open_button)
type_layout.addLayout(open_layout)
# Add vertical layout for load calibration
load_layout = QVBoxLayout()
load_button = QPushButton("Load")
load_button.clicked.connect(lambda: self.module.controller.on_load_calibration(
float(start_edit.text()),
float(stop_edit.text())
))
# Load plot widget
self.load_plot = MplWidget()
load_layout.addWidget(self.load_plot)
load_layout.addWidget(load_button)
type_layout.addLayout(load_layout)
# Add vertical layout for save calibration
data_layout = QVBoxLayout()
# Export button
export_button = QPushButton("Export")
export_button.clicked.connect(self.on_export_button_clicked)
data_layout.addWidget(export_button)
# Import button
import_button = QPushButton("Import")
import_button.clicked.connect(self.on_import_button_clicked)
data_layout.addWidget(import_button)
# Apply button
apply_button = QPushButton("Apply calibration")
apply_button.clicked.connect(self.on_apply_button_clicked)
data_layout.addWidget(apply_button)
main_layout.addLayout(data_layout)
self.setLayout(main_layout)
# Connect the calibration finished signals to the on_calibration_finished slot
self.module.model.short_calibration_finished.connect(self.on_short_calibration_finished)
self.module.model.open_calibration_finished.connect(self.on_open_calibration_finished)
self.module.model.load_calibration_finished.connect(self.on_load_calibration_finished)
def on_short_calibration_finished(self, short_calibration : list) -> None:
self.on_calibration_finished("short", self.short_plot, short_calibration)
def on_open_calibration_finished(self, open_calibration : list) -> None:
self.on_calibration_finished("open", self.open_plot, open_calibration)
def on_load_calibration_finished(self, load_calibration : list) -> None:
self.on_calibration_finished("load", self.load_plot, load_calibration)
def on_calibration_finished(self, type : str, widget: MplWidget, data :list) -> None:
"""This method is called when a calibration has finished.
It plots the calibration data on the given widget.
"""
x = [data_point[0] for data_point in data]
magnitude = [data_point[1] for data_point in data]
phase = [data_point[2] for data_point in data]
ax = widget.canvas.ax
ax.clear()
ax.set_xlabel("Frequency (MHz)")
ax.set_ylabel("S11 (dB)")
ax.set_title("S11")
ax.grid(True)
ax.plot(x, magnitude, label="Magnitude")
ax.plot(x, phase, label="Phase")
ax.legend()
# make the y axis go down instead of up
ax.invert_yaxis()
widget.canvas.draw()
widget.canvas.flush_events()
def on_export_button_clicked(self) -> None:
"""This method is called when the export button is clicked. """
pass
def on_import_button_clicked(self) -> None:
"""This method is called when the import button is clicked. """
pass
def on_apply_button_clicked(self) -> None:
"""This method is called when the apply button is clicked. """
self.module.controller.calculate_calibration()
# Close the calibration window
self.close()

View file

@ -94,9 +94,9 @@ class Ui_Form(object):
self.startButton = QtWidgets.QPushButton(parent=Form)
self.startButton.setObjectName("startButton")
self.verticalLayout_2.addWidget(self.startButton)
self.pushButton = QtWidgets.QPushButton(parent=Form)
self.pushButton.setObjectName("pushButton")
self.verticalLayout_2.addWidget(self.pushButton)
self.calibrationButton = QtWidgets.QPushButton(parent=Form)
self.calibrationButton.setObjectName("calibrationButton")
self.verticalLayout_2.addWidget(self.calibrationButton)
self.pushButton_3 = QtWidgets.QPushButton(parent=Form)
self.pushButton_3.setObjectName("pushButton_3")
self.verticalLayout_2.addWidget(self.pushButton_3)
@ -149,7 +149,7 @@ class Ui_Form(object):
self.label_7.setText(_translate("Form", "Stop Frequency:"))
self.label_5.setText(_translate("Form", "Start Frequency:"))
self.startButton.setText(_translate("Form", "Start Sweep"))
self.pushButton.setText(_translate("Form", "Calibrate"))
self.calibrationButton.setText(_translate("Form", "Calibrate"))
self.pushButton_3.setText(_translate("Form", "T&M Settings"))
self.titleinfoLabel.setText(_translate("Form", "Info Box:"))
from nqrduck.contrib.mplwidget import MplWidget