mirror of
https://github.com/nqrduck/nqrduck-autotm.git
synced 2024-11-09 19:50:00 +00:00
Fixed calibration and bug with phase plotting.
This commit is contained in:
parent
9ee2f54a97
commit
e2a7bc997f
3 changed files with 76 additions and 146 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue