Fixed calibration and bug with phase plotting.

This commit is contained in:
jupfi 2023-08-18 11:02:08 +02:00
parent 9ee2f54a97
commit e2a7bc997f
3 changed files with 76 additions and 146 deletions

View file

@ -32,9 +32,7 @@ class AutoTMController(ModuleController):
self.module.model.serial = QtSerialPort.QSerialPort(
device, baudRate=self.BAUDRATE, readyRead=self.on_ready_read
)
self.module.model.serial.open(
QtSerialPort.QSerialPort.OpenModeFlag.ReadWrite
)
self.module.model.serial.open(QtSerialPort.QSerialPort.OpenModeFlag.ReadWrite)
logger.debug("Connected to device %s", device)
except Exception as e:
@ -77,9 +75,9 @@ class AutoTMController(ModuleController):
return
if start_frequency < MIN_FREQUENCY or stop_frequency > MAX_FREQUENCY:
error = (
"Could not start frequency sweep. Start and stop frequency must be between %s and %s MHz"
% (MIN_FREQUENCY / 1e6, MAX_FREQUENCY / 1e6)
error = "Could not start frequency sweep. Start and stop frequency must be between %s and %s MHz" % (
MIN_FREQUENCY / 1e6,
MAX_FREQUENCY / 1e6,
)
logger.error(error)
self.module.view.add_info_text(error)
@ -111,10 +109,7 @@ class AutoTMController(ModuleController):
# 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
# then we have the data for the return loss and the phase at a certain frequency
if (
text.startswith("f")
and self.module.view.frequency_sweep_spinner.isVisible()
):
if text.startswith("f") and self.module.view.frequency_sweep_spinner.isVisible():
text = text[1:].split("r")
frequency = float(text[0])
return_loss, phase = map(float, text[1].split("p"))
@ -122,38 +117,24 @@ class AutoTMController(ModuleController):
# If the text starts with 'r' and no calibration is active we know that the data is a measurement
elif text.startswith("r") and self.module.model.active_calibration == None:
logger.debug("Measurement finished")
self.module.model.measurement = S11Data(
self.module.model.data_points.copy()
)
self.module.model.measurement = S11Data(self.module.model.data_points.copy())
self.module.view.frequency_sweep_spinner.hide()
# If the text starts with 'r' and a short calibration is active we know that the data is a short calibration
elif (
text.startswith("r") and self.module.model.active_calibration == "short"
):
elif text.startswith("r") and self.module.model.active_calibration == "short":
logger.debug("Short calibration finished")
self.module.model.short_calibration = S11Data(
self.module.model.data_points.copy()
)
self.module.model.short_calibration = S11Data(self.module.model.data_points.copy())
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
# If the text starts with 'r' and an open calibration is active we know that the data is an open calibration
elif (
text.startswith("r") and self.module.model.active_calibration == "open"
):
elif text.startswith("r") and self.module.model.active_calibration == "open":
logger.debug("Open calibration finished")
self.module.model.open_calibration = S11Data(
self.module.model.data_points.copy()
)
self.module.model.open_calibration = S11Data(self.module.model.data_points.copy())
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
# If the text starts with 'r' and a load calibration is active we know that the data is a load calibration
elif (
text.startswith("r") and self.module.model.active_calibration == "load"
):
elif text.startswith("r") and self.module.model.active_calibration == "load":
logger.debug("Load calibration finished")
self.module.model.load_calibration = S11Data(
self.module.model.data_points.copy()
)
self.module.model.load_calibration = S11Data(self.module.model.data_points.copy())
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
# If the text starts with 'i' we know that the data is an info message
@ -187,9 +168,7 @@ class AutoTMController(ModuleController):
logger.debug("Starting next voltage sweep: %s", command)
serial.write(command.encode("utf-8"))
def on_short_calibration(
self, start_frequency: float, stop_frequency: float
) -> None:
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.
"""
@ -197,9 +176,7 @@ class AutoTMController(ModuleController):
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:
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.
"""
@ -207,9 +184,7 @@ class AutoTMController(ModuleController):
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:
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.
"""
@ -221,9 +196,8 @@ class AutoTMController(ModuleController):
"""This method is called when the calculate calibration button is pressed.
It calculates the calibration from the short, open and calibration data points.
@TODO: Make calibration useful. Right now the calibration does not work for the probe coils. It completly messes up the S11 data.
For 50 Ohm reference loads the calibration makes the S11 data usable - one then gets a flat line at -50 dB.
The problem is probably two things:
@TODO: Improvements to the calibrations can be made the following ways:
1. The ideal values for open, short and load should be measured with a VNA and then be loaded for the calibration.
The ideal values are probably not -1, 1 and 0 but will also show frequency dependent behaviour.
2 The AD8302 chip only returns the absolute value of the phase. One would probably need to calculate the phase with various algorithms found in the literature.
@ -232,19 +206,13 @@ class AutoTMController(ModuleController):
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(
"Could not calculate calibration. No short calibration data points available."
)
logger.error("Could not calculate calibration. No short calibration data points available.")
return
if self.module.model.open_calibration == None:
logger.error(
"Could not calculate calibration. No open calibration data points available."
)
logger.error("Could not calculate calibration. No open calibration data points available.")
return
if self.module.model.load_calibration == None:
logger.error(
"Could not calculate calibration. No load calibration data points available."
)
logger.error("Could not calculate calibration. No load calibration data points available.")
return
# Then we calculate the calibration
@ -256,42 +224,29 @@ class AutoTMController(ModuleController):
measured_gamma_open = self.module.model.open_calibration.gamma
measured_gamma_load = self.module.model.load_calibration.gamma
E_Ds = []
E_Ss = []
E_ts = []
for gamma_s, gamma_o, gamma_l in zip(
measured_gamma_short, measured_gamma_open, measured_gamma_load
):
e_00s = []
e_11s = []
delta_es = []
for gamma_s, gamma_o, gamma_l in zip(measured_gamma_short, measured_gamma_open, measured_gamma_load):
# This is the solution from
# 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]
# ])
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])
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_00, e11, delta_e = np.linalg.lstsq(A, B, rcond=None)[0]
E_D = gamma_l
E_ = (2 * gamma_l - (gamma_s + gamma_o)) / (gamma_s - gamma_o)
E_S = (2 * (gamma_o + gamma_l) * (gamma_s + gamma_l)) / (gamma_s - gamma_o)
e_00s.append(e_00)
e_11s.append(e11)
delta_es.append(delta_e)
E_Ds.append(E_D)
E_Ss.append(E_S)
E_ts.append(E_)
# e_00 = gamma_l # Because here the reflection coefficient should be 0
# e11 = (gamma_o + gamma_o - 2 * e_00) / (gamma_o - gamma_s)
# delta_e = -gamma_o + gamma_o* e11 + e_00
# e_00s.append(e_00)
# e11s.append(e11)
# delta_es.append(delta_e)
self.module.model.calibration = (E_Ds, E_Ss, E_ts)
self.module.model.calibration = (e_00s, e_11s, delta_es)
def export_calibration(self, filename: str) -> None:
"""This method is called when the export calibration button is pressed.
@ -303,21 +258,15 @@ class AutoTMController(ModuleController):
logger.debug("Exporting calibration")
# First we check if the short and open calibration data points are available
if self.module.model.short_calibration == None:
logger.error(
"Could not export calibration. No short calibration data points available."
)
logger.error("Could not export calibration. No short calibration data points available.")
return
if self.module.model.open_calibration == None:
logger.error(
"Could not export calibration. No open calibration data points available."
)
logger.error("Could not export calibration. No open calibration data points available.")
return
if self.module.model.load_calibration == None:
logger.error(
"Could not export calibration. No load calibration data points available."
)
logger.error("Could not export calibration. No load calibration data points available.")
return
# Then we export the different calibrations as a json file
@ -368,9 +317,7 @@ class AutoTMController(ModuleController):
return
if tuning_voltage < 0 or matching_voltage < 0:
error = (
"Could not set voltages. Tuning and matching voltage must be positive"
)
error = "Could not set voltages. Tuning and matching voltage must be positive"
logger.error(error)
self.module.view.add_info_text(error)
return
@ -424,12 +371,7 @@ class AutoTMController(ModuleController):
self.module.view.add_info_text(error)
return
if (
start_frequency < 0
or stop_frequency < 0
or frequency_step < 0
or voltage_resolution < 0
):
if start_frequency < 0 or stop_frequency < 0 or frequency_step < 0 or voltage_resolution < 0:
error = "Could not generate LUT. Start frequency, stop frequency, frequency step and voltage resolution must be positive"
logger.error(error)
self.module.view.add_info_text(error)
@ -456,9 +398,7 @@ class AutoTMController(ModuleController):
)
# We create the lookup table
LUT = LookupTable(
start_frequency, stop_frequency, frequency_step, voltage_resolution
)
LUT = LookupTable(start_frequency, stop_frequency, frequency_step, voltage_resolution)
LUT.started_frequency = start_frequency
self.module.model.LUT = LUT

View file

@ -12,7 +12,7 @@ class S11Data:
# Conversion factors - the data is generally sent and received in mV
# These values are used to convert the data to dB and degrees
CENTER_POINT_MAGNITUDE = 900 # mV
CENTER_POINT_PHASE = 1800 # mV
CENTER_POINT_PHASE = 900 # mV
MAGNITUDE_SLOPE = 30 # dB/mV
PHASE_SLOPE = 10 # deg/mV

View file

@ -1,6 +1,7 @@
import logging
from datetime import datetime
import time
import cmath
from pathlib import Path
from PyQt6.QtGui import QMovie
from PyQt6.QtSerialPort import QSerialPort
@ -18,6 +19,7 @@ from PyQt6.QtWidgets import (
QTableWidgetItem,
)
from PyQt6.QtCore import pyqtSlot, Qt
from PyQt6.QtTest import QTest
from nqrduck.module.module_view import ModuleView
from nqrduck.contrib.mplwidget import MplWidget
from .widget import Ui_Form
@ -107,6 +109,8 @@ class AutoTMView(ModuleView):
ax.set_ylim(-100, 0)
self._ui_form.S11Plot.canvas.draw()
self.phase_ax = self._ui_form.S11Plot.canvas.ax.twinx()
def on_calibration_button_clicked(self) -> None:
"""This method is called when the calibration button is clicked.
It opens the calibration window.
@ -164,59 +168,43 @@ class AutoTMView(ModuleView):
phase = data.phase_deg
gamma = data.gamma
# Plot complex reflection coefficient
""" import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([g.real for g in gamma], [g.imag for g in gamma])
ax.set_aspect('equal')
ax.grid(True)
ax.set_title("Complex reflection coefficient")
ax.set_xlabel("Real")
ax.set_ylabel("Imaginary")
plt.show()
"""
self._ui_form.S11Plot.canvas.ax.clear()
magnitude_ax = self._ui_form.S11Plot.canvas.ax
magnitude_ax.clear()
phase_ax = self._ui_form.S11Plot.canvas.ax.twinx()
phase_ax.clear()
# @ TODO: implement proper calibration
self.phase_ax.clear()
logger.debug("Shape of phase: %s", phase.shape)
# Calibration for visualization happens here.
if self.module.model.calibration is not None:
# Calibration test:
import cmath
calibration = self.module.model.calibration
E_D = calibration[0]
E_S = calibration[1]
E_t = calibration[2]
e_00 = calibration[0]
e11 = calibration[1]
delta_e = calibration[2]
# gamma_corr = [(data_point - e_00[i]) / (data_point * e11[i] - delta_e[i]) for i, data_point in enumerate(gamma)]
gamma_corr = [
(data_point - E_D[i]) / (E_S[i] * (data_point - E_D[i]) + E_t[i]) for i, data_point in enumerate(gamma)
(data_point - e_00[i]) / (data_point * e11[i] - delta_e[i]) for i, data_point in enumerate(gamma)
]
""" fig, ax = plt.subplots()
ax.plot([g.real for g in gamma_corr], [g.imag for g in gamma_corr])
ax.set_aspect('equal')
ax.grid(True)
ax.set_title("Complex reflection coefficient")
ax.set_xlabel("Real")
ax.set_ylabel("Imaginary")
plt.show() """
return_loss_db_corr = [-20 * cmath.log10(abs(g + 1e-12)) for g in gamma_corr]
magnitude_ax.plot(frequency, return_loss_db_corr, color="red")
phase_ax.set_ylabel("|Phase (deg)|")
phase_ax.plot(frequency, phase, color="orange", linestyle="--")
phase_ax.set_ylim(-180, 180)
phase_ax.invert_yaxis()
else:
magnitude_ax.plot(frequency, return_loss_db, color="blue")
self.phase_ax.set_ylabel("|Phase (deg)|")
self.phase_ax.plot(frequency, phase, color="orange", linestyle="--")
self.phase_ax.set_ylim(-180, 180)
self.phase_ax.invert_yaxis()
magnitude_ax.set_xlabel("Frequency (MHz)")
magnitude_ax.set_ylabel("S11 (dB)")
magnitude_ax.set_title("S11")
magnitude_ax.grid(True)
magnitude_ax.plot(frequency, return_loss_db, color="blue")
# make the y axis go down instead of up
magnitude_ax.invert_yaxis()
@ -297,7 +285,7 @@ class AutoTMView(ModuleView):
# Create table widget
self.table_widget = QTableWidget()
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(["Frequency (MHz)", "Tuning Voltage", "Matching Voltage"])
self.table_widget.setHorizontalHeaderLabels(["Frequency (MHz)", "Matching Voltage", "Tuning Voltage"])
LUT = self.module.model.LUT
for row, frequency in enumerate(LUT.data.keys()):
self.table_widget.insertRow(row)
@ -320,11 +308,13 @@ class AutoTMView(ModuleView):
One can then view the matching on a seperate VNA.
"""
for frequency in self.module.model.LUT.data.keys():
tuning_voltage = str(self.module.model.LUT.data[frequency][0])
matching_voltage = str(self.module.model.LUT.data[frequency][1])
tuning_voltage = str(self.module.model.LUT.data[frequency][1])
matching_voltage = str(self.module.model.LUT.data[frequency][0])
self.module.controller.set_voltages(tuning_voltage, matching_voltage)
# Evil
time.sleep(0.5)
# Wait for 0.5 seconds
QTest.qWait(500)
QApplication.processEvents()
class CalibrationWindow(QWidget):
def __init__(self, module, parent=None):
@ -358,7 +348,7 @@ class AutoTMView(ModuleView):
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()))
lambda: self.module.controller.on_short_calibration(start_edit.text(), stop_edit.text())
)
# Short plot widget
self.short_plot = MplWidget()
@ -370,7 +360,7 @@ class AutoTMView(ModuleView):
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()))
lambda: self.module.controller.on_open_calibration(start_edit.text(), stop_edit.text())
)
# Open plot widget
self.open_plot = MplWidget()
@ -382,7 +372,7 @@ class AutoTMView(ModuleView):
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()))
lambda: self.module.controller.on_load_calibration(start_edit.text(), stop_edit.text())
)
# Load plot widget
self.load_plot = MplWidget()