Implemented basic voltage sweep.

This commit is contained in:
jupfi 2023-08-16 16:23:23 +02:00
parent 224417687b
commit dcf8bdeb85
5 changed files with 551 additions and 195 deletions

View file

@ -5,15 +5,16 @@ from serial.tools.list_ports import comports
from PyQt6 import QtSerialPort
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from nqrduck.module.module_controller import ModuleController
from .model import S11Data
from .model import S11Data, LookupTable
logger = logging.getLogger(__name__)
class AutoTMController(ModuleController):
BAUDRATE = 115200
def find_devices(self) -> None:
"""Scan for available serial devices and add them to the model as available devices. """
"""Scan for available serial devices and add them to the model as available devices."""
logger.debug("Scanning for available serial devices")
ports = comports()
self.module.model.available_devices = [port.device for port in ports]
@ -21,34 +22,36 @@ class AutoTMController(ModuleController):
for device in self.module.model.available_devices:
logger.debug("Found device: %s", device)
def connect(self, device : str) -> None:
"""Connect to the specified device.
def connect(self, device: str) -> None:
"""Connect to the specified device.
Args:
device (str): The device port to connect to."""
logger.debug("Connecting to device %s", device)
try:
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 = QtSerialPort.QSerialPort(
device, baudRate=self.BAUDRATE, readyRead=self.on_ready_read
)
self.module.model.serial.open(
QtSerialPort.QSerialPort.OpenModeFlag.ReadWrite
)
logger.debug("Connected to device %s", device)
except Exception as e:
logger.error("Could not connect to device %s: %s", device, e)
def start_frequency_sweep(self, start_frequency : str, stop_frequency : str) -> None:
""" This starts a frequency sweep on the device in the specified range.
def start_frequency_sweep(self, start_frequency: str, stop_frequency: str) -> None:
"""This starts a frequency sweep on the device in the specified range.
The minimum start and stop frequency are specific to the AD4351 based frequency generator.
Args:
start_frequency (str): The start frequency in MHz.
stop_frequency (str): The stop frequency in MHz.
"""
FREQUENCY_STEP = 50000 # Hz
MIN_FREQUENCY = 35e6 # Hz
MAX_FREQUENCY = 300e6 # Hz
FREQUENCY_STEP = 50000 # Hz
MIN_FREQUENCY = 35e6 # Hz
MAX_FREQUENCY = 300e6 # Hz
try:
start_frequence = start_frequency.replace(",", ".")
@ -66,42 +69,52 @@ class AutoTMController(ModuleController):
logger.error(error)
self.module.view.add_info_text(error)
return
if start_frequency < 0 or stop_frequency < 0:
error = "Could not start frequency sweep. Start and stop frequency must be positive"
logger.error(error)
self.module.view.add_info_text(error)
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)
return
logger.debug("Starting frequency sweep from %s to %s with step size %s", start_frequency, stop_frequency, FREQUENCY_STEP)
logger.debug(
"Starting frequency sweep from %s to %s with step size %s",
start_frequency,
stop_frequency,
FREQUENCY_STEP,
)
# 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>f<stop>' to the serial connection
# Print the command 'f<start>f<stop>f<step>' to the serial connection
try:
command = "f%sf%sf%s" % (start_frequency, stop_frequency, FREQUENCY_STEP)
self.module.model.serial.write(command.encode('utf-8'))
self.module.model.serial.write(command.encode("utf-8"))
except AttributeError:
logger.error("Could not start frequency sweep. No device connected.")
self.module.view.frequency_sweep_spinner.hide()
def on_ready_read(self) -> None:
"""This method is called when data is received from the serial connection. """
"""This method is called when data is received from the serial connection."""
serial = self.module.model.serial
while serial.canReadLine():
text = serial.readLine().data().decode()
text = text.rstrip('\r\n')
text = text.rstrip("\r\n")
# 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"))
@ -109,24 +122,38 @@ 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
@ -137,8 +164,32 @@ class AutoTMController(ModuleController):
elif text.startswith("e"):
text = "ATM Error: " + text[1:]
self.module.view.add_info_text(text)
# If the text starts with 'v' we know that the data is a voltage sweep result
elif text.startswith("v"):
text = text[1:]
text = text.split("t")
matching_voltage = float(text[0])
tuning_voltage = float(text[1])
# Now we add the datapoint to the current LUT
LUT = self.module.model.LUT
logger.debug(
"Received voltage sweep result: %s %s",
matching_voltage,
tuning_voltage,
)
LUT.add_voltages(matching_voltage, tuning_voltage)
def on_short_calibration(self, start_frequency : float, stop_frequency : float) -> None:
# Start the next voltage sweep if there are more voltages to sweep
if LUT.is_incomplete():
next_frequency = LUT.get_next_frequency()
command = "s%s" % next_frequency
LUT.started_frequency = next_frequency
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:
"""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.
"""
@ -146,7 +197,9 @@ 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.
"""
@ -154,7 +207,9 @@ 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.
"""
@ -169,7 +224,7 @@ class AutoTMController(ModuleController):
@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:
1. The ideal values for open, short and load should be measured with a VNA and then be loaded for the calibration.
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.
Though Im not sure if these proposed algorithms would work for the AD8302 chip.
@ -177,15 +232,21 @@ 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
ideal_gamma_short = -1
ideal_gamma_open = 1
@ -198,8 +259,10 @@ class AutoTMController(ModuleController):
E_Ds = []
E_Ss = []
E_ts = []
for gamma_s, gamma_o, gamma_l in zip(measured_gamma_short, measured_gamma_open, measured_gamma_load):
# This is the solution from
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],
@ -212,7 +275,7 @@ class AutoTMController(ModuleController):
# 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_ = (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_Ds.append(E_D)
@ -240,22 +303,28 @@ 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
data = {
"short": self.module.model.short_calibration.to_json(),
"open": self.module.model.open_calibration.to_json(),
"load": self.module.model.load_calibration.to_json()
"load": self.module.model.load_calibration.to_json(),
}
with open(filename, "w") as f:
@ -277,16 +346,16 @@ class AutoTMController(ModuleController):
self.module.model.open_calibration = S11Data.from_json(data["open"])
self.module.model.load_calibration = S11Data.from_json(data["load"])
def set_voltages(self, tuning_voltage : str, matching_voltage : str) -> None:
def set_voltages(self, tuning_voltage: str, matching_voltage: str) -> None:
"""This method is called when the set voltages button is pressed.
It writes the specified tuning and matching voltage to the serial connection.
Args:
tuning_voltage (str): The tuning voltage in V.
matching_voltage (str): The matching voltage in V.
"""
logger.debug("Setting voltages")
MAX_VOLTAGE = 5 # V
MAX_VOLTAGE = 5 # V
try:
tuning_voltage = tuning_voltage.replace(",", ".")
matching_voltage = matching_voltage.replace(",", ".")
@ -297,22 +366,103 @@ class AutoTMController(ModuleController):
logger.error(error)
self.module.view.add_info_text(error)
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
if tuning_voltage > MAX_VOLTAGE or matching_voltage > MAX_VOLTAGE:
error = "Could not set voltages. Tuning and matching voltage must be between 0 and 5 V"
logger.error(error)
self.module.view.add_info_text(error)
return
logger.debug("Setting tuning voltage to %s V and matching voltage to %s V", tuning_voltage, matching_voltage)
logger.debug(
"Setting tuning voltage to %s V and matching voltage to %s V",
tuning_voltage,
matching_voltage,
)
try:
command = "v%sv%s" % (matching_voltage, tuning_voltage)
self.module.model.serial.write(command.encode('utf-8'))
self.module.model.serial.write(command.encode("utf-8"))
except AttributeError:
logger.error("Could not set voltages. No device connected.")
def generate_lut(
self,
start_frequency: str,
stop_frequency: str,
frequency_step: str,
voltage_resolution: str,
) -> None:
"""This method is called when the generate LUT button is pressed.
It generates a lookup table for the specified frequency range and voltage resolution.
Args:
start_frequency (str): The start frequency in Hz.
stop_frequency (str): The stop frequency in Hz.
frequency_step (str): The frequency step in Hz.
voltage_resolution (str): The voltage resolution in V.
"""
logger.debug("Generating LUT")
try:
start_frequency = start_frequency.replace(",", ".")
stop_frequency = stop_frequency.replace(",", ".")
frequency_step = frequency_step.replace(",", ".")
voltage_resolution = voltage_resolution.replace(",", ".")
start_frequency = float(start_frequency)
stop_frequency = float(stop_frequency)
frequency_step = float(frequency_step)
voltage_resolution = float(voltage_resolution)
except ValueError:
error = "Could not generate LUT. Start frequency, stop frequency, frequency step and voltage resolution must be floats"
logger.error(error)
self.module.view.add_info_text(error)
return
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)
return
if start_frequency > stop_frequency:
error = "Could not generate LUT. Start frequency must be smaller than stop frequency"
logger.error(error)
self.module.view.add_info_text(error)
return
if frequency_step > (stop_frequency - start_frequency):
error = "Could not generate LUT. Frequency step must be smaller than the frequency range"
logger.error(error)
self.module.view.add_info_text(error)
return
logger.debug(
"Generating LUT from %s MHz to %s MHz with a frequency step of %s MHz and a voltage resolution of %s V",
start_frequency,
stop_frequency,
frequency_step,
voltage_resolution,
)
# We create the lookup table
LUT = LookupTable(
start_frequency, stop_frequency, frequency_step, voltage_resolution
)
LUT.started_frequency = start_frequency
self.module.model.LUT = LUT
# We write the first command to the serial connection
command = "s%s" % (start_frequency)
self.module.model.serial.write(command.encode("utf-8"))

View file

@ -7,55 +7,57 @@ from nqrduck.module.module_model import ModuleModel
logger = logging.getLogger(__name__)
class S11Data:
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
MAGNITUDE_SLOPE = 30 # dB/mV
PHASE_SLOPE = 10 # deg/mV
CENTER_POINT_MAGNITUDE = 900 # mV
CENTER_POINT_PHASE = 1800 # mV
MAGNITUDE_SLOPE = 30 # dB/mV
PHASE_SLOPE = 10 # deg/mV
def __init__(self, data_points : list) -> None:
def __init__(self, data_points: list) -> None:
self.frequency = np.array([data_point[0] for data_point in data_points])
self.return_loss_mv = np.array([data_point[1] for data_point in data_points])
self.phase_mv = np.array([data_point[2] for data_point in data_points])
@property
def millivolts(self):
return self.frequency, self.return_loss_mv, self.phase_mv
@property
def return_loss_db(self):
return (self.return_loss_mv - self.CENTER_POINT_MAGNITUDE) / self.MAGNITUDE_SLOPE
return (
self.return_loss_mv - self.CENTER_POINT_MAGNITUDE
) / self.MAGNITUDE_SLOPE
@property
def phase_deg(self):
"""Returns the absolute value of the phase in degrees"""
return (self.phase_mv - self.CENTER_POINT_PHASE) / self.PHASE_SLOPE
@property
def phase_rad(self):
return self.phase_deg * cmath.pi / 180
@property
def gamma(self):
"""Complex reflection coefficient"""
if len(self.return_loss_db) != len(self.phase_rad):
raise ValueError("return_loss_db and phase_rad must be the same length")
return [cmath.rect(10 ** (-loss_db / 20), phase_rad) for loss_db, phase_rad in zip(self.return_loss_db, self.phase_rad)]
return [
cmath.rect(10 ** (-loss_db / 20), phase_rad)
for loss_db, phase_rad in zip(self.return_loss_db, self.phase_rad)
]
def to_json(self):
return {
"frequency": self.frequency.tolist(),
"return_loss_mv": self.return_loss_mv.tolist(),
"phase_mv": self.phase_mv.tolist()
"phase_mv": self.phase_mv.tolist(),
}
@classmethod
def from_json(cls, json):
f = json["frequency"]
@ -64,8 +66,74 @@ class S11Data:
data = [(f[i], rl[i], p[i]) for i in range(len(f))]
return cls(data)
class AutoTMModel(ModuleModel):
class LookupTable:
"""This class is used to store a lookup table for tuning and matching of electrical probeheads."""
data = dict()
def __init__(
self,
start_frequency: float,
stop_frequency: float,
frequency_step: float,
voltage_resolution: float,
) -> None:
self.start_frequency = start_frequency
self.stop_frequency = stop_frequency
self.frequency_step = frequency_step
self.voltage_resolution = voltage_resolution
# This is the frequency at which the tuning and matching process was started
self.started_frequency = None
self.init_voltages()
def init_voltages(self) -> None:
"""Initialize the lookup table with default values."""
for frequency in np.arange(
self.start_frequency, self.stop_frequency, self.frequency_step
):
self.started_frequency = frequency
self.add_voltages(None, None)
def is_incomplete(self) -> bool:
"""This method returns True if the lookup table is incomplete,
i.e. if there are frequencies for which no the tuning or matching voltage is none.
Returns:
bool: True if the lookup table is incomplete, False otherwise.
"""
return any(
[
tuning_voltage is None or matching_voltage is None
for tuning_voltage, matching_voltage in self.data.values()
]
)
def get_next_frequency(self) -> float:
"""This method returns the next frequency for which the tuning and matching voltage is not yet set.
Returns:
float: The next frequency for which the tuning and matching voltage is not yet set.
"""
for frequency, (tuning_voltage, matching_voltage) in self.data.items():
if tuning_voltage is None or matching_voltage is None:
return frequency
return None
def add_voltages(self, tuning_voltage: float, matching_voltage: float) -> None:
"""Add a tuning and matching voltage for the last started frequency to the lookup table.
Args:
tuning_voltage (float): The tuning voltage for the given frequency.
matching_voltage (float): The matching voltage for the given frequency."""
self.data[self.started_frequency] = (tuning_voltage, matching_voltage)
class AutoTMModel(ModuleModel):
available_devices_changed = pyqtSignal(list)
serial_changed = pyqtSignal(QSerialPort)
data_points_changed = pyqtSignal(list)
@ -92,8 +160,7 @@ class AutoTMModel(ModuleModel):
@property
def serial(self):
"""The serial property is used to store the current serial connection.
"""
"""The serial property is used to store the current serial connection."""
return self._serial
@serial.setter
@ -101,7 +168,9 @@ class AutoTMModel(ModuleModel):
self._serial = value
self.serial_changed.emit(value)
def add_data_point(self, frequency: float, return_loss: float, phase : float) -> None:
def add_data_point(
self, frequency: float, return_loss: float, phase: float
) -> None:
"""Add a data point to the model. These data points are our intermediate data points read in via the serial connection.
They will be saved in the according properties later on.
"""
@ -118,7 +187,7 @@ class AutoTMModel(ModuleModel):
"""The measurement property is used to store the current measurement.
This is the measurement that is shown in the main S11 plot"""
return self._measurement
@measurement.setter
def measurement(self, value):
"""The measurement value is a tuple of three lists: frequency, return loss and phase."""
@ -130,7 +199,7 @@ class AutoTMModel(ModuleModel):
@property
def active_calibration(self):
return self._active_calibration
@active_calibration.setter
def active_calibration(self, value):
self._active_calibration = value
@ -183,9 +252,16 @@ class AutoTMModel(ModuleModel):
@property
def calibration(self):
return self._calibration
@calibration.setter
def calibration(self, value):
logger.debug("Setting calibration")
self._calibration = value
@property
def LUT(self):
return self._LUT
@LUT.setter
def LUT(self, value):
self._LUT = value

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1280</width>
<height>705</height>
<height>745</height>
</rect>
</property>
<property name="sizePolicy">
@ -91,7 +91,7 @@
<item>
<widget class="QTabWidget" name="typeTab">
<property name="currentIndex">
<number>0</number>
<number>1</number>
</property>
<widget class="QWidget" name="mechTab">
<attribute name="title">
@ -111,44 +111,105 @@
<attribute name="title">
<string>Electrical</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3" rowstretch="0,0,0,1">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="matchingBox"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="setvoltagesButton">
<property name="text">
<string>Set Voltages</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="tuningBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Voltage Matching</string>
</property>
</widget>
<layout class="QGridLayout" name="gridLayout_3" rowstretch="0,0,0,0,0,0,0,0,0,0">
<item row="7" column="1">
<widget class="QDoubleSpinBox" name="stopfrequencyBox"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<widget class="QLabel" name="label_9">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Voltage Tuning</string>
<string>Set Voltages:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_11">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Generate LUT:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="tuningBox"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Start Frequency (MHz)</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Voltage Resolution</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Voltage Matching</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="matchingBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Voltage Tuning</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Stop Frequency (MHz)</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QPushButton" name="generateLUTButton">
<property name="text">
<string>Start Voltage Sweep</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QDoubleSpinBox" name="resolutionBox"/>
</item>
<item row="6" column="1">
<widget class="QDoubleSpinBox" name="startfrequencyBox"/>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="setvoltagesButton">
<property name="text">
<string>Set Voltages</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Frequency Step (MHz)</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QDoubleSpinBox" name="frequencystepBox"/>
</item>
</layout>
</widget>
</widget>
@ -247,7 +308,7 @@
<x>0</x>
<y>0</y>
<width>297</width>
<height>169</height>
<height>68</height>
</rect>
</property>
</widget>

View file

@ -3,7 +3,17 @@ 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, QHBoxLayout, QLineEdit, QPushButton, QDialog, QFileDialog
from PyQt6.QtWidgets import (
QWidget,
QLabel,
QVBoxLayout,
QApplication,
QHBoxLayout,
QLineEdit,
QPushButton,
QDialog,
QFileDialog,
)
from PyQt6.QtCore import pyqtSlot, Qt
from nqrduck.module.module_view import ModuleView
from nqrduck.contrib.mplwidget import MplWidget
@ -13,7 +23,6 @@ logger = logging.getLogger(__name__)
class AutoTMView(ModuleView):
def __init__(self, module):
super().__init__(module)
@ -26,57 +35,69 @@ class AutoTMView(ModuleView):
self._ui_form.connectButton.setDisabled(True)
# On clicking of the refresh button scan for available usb devices
self._ui_form.refreshButton.clicked.connect(
self.module.controller.find_devices)
self._ui_form.refreshButton.clicked.connect(self.module.controller.find_devices)
# Connect the available devices changed signal to the on_available_devices_changed slot
self.module.model.available_devices_changed.connect(
self.on_available_devices_changed)
self.on_available_devices_changed
)
# Connect the serial changed signal to the on_serial_changed slot
self.module.model.serial_changed.connect(self.on_serial_changed)
# On clicking of the connect button call the connect method
self._ui_form.connectButton.clicked.connect(
self.on_connect_button_clicked)
self._ui_form.connectButton.clicked.connect(self.on_connect_button_clicked)
# On clicking of the start button call the start_frequency_sweep method
self._ui_form.startButton.clicked.connect(lambda: self.module.controller.start_frequency_sweep(
self._ui_form.startEdit.text(),
self._ui_form.stopEdit.text()
))
self._ui_form.startButton.clicked.connect(
lambda: self.module.controller.start_frequency_sweep(
self._ui_form.startEdit.text(), self._ui_form.stopEdit.text()
)
)
# On clicking of the generateLUTButton call the generate_lut method
self._ui_form.generateLUTButton.clicked.connect(
lambda: self.module.controller.generate_lut(
self._ui_form.startfrequencyBox.text(),
self._ui_form.stopfrequencyBox.text(),
self._ui_form.frequencystepBox.text(),
self._ui_form.resolutionBox.text(),
)
)
# On clicking of the setvoltagesButton call the set_voltages method
self._ui_form.setvoltagesButton.clicked.connect(lambda: self.module.controller.set_voltages(
self._ui_form.tuningBox.text(),
self._ui_form.matchingBox.text()
))
self._ui_form.setvoltagesButton.clicked.connect(
lambda: self.module.controller.set_voltages(
self._ui_form.tuningBox.text(), self._ui_form.matchingBox.text()
)
)
# On clicking of the calibration button call the on_calibration_button_clicked method
self._ui_form.calibrationButton.clicked.connect(
self.on_calibration_button_clicked)
self.on_calibration_button_clicked
)
# Connect the measurement finished signal to the plot_measurement slot
self.module.model.measurement_finished.connect(self.plot_measurement)
# Add a vertical layout to the info box
self._ui_form.scrollAreaWidgetContents.setLayout(QVBoxLayout())
self._ui_form.scrollAreaWidgetContents.layout(
).setAlignment(Qt.AlignmentFlag.AlignTop)
self._ui_form.scrollAreaWidgetContents.layout().setAlignment(
Qt.AlignmentFlag.AlignTop
)
self.init_plot()
self.init_labels()
def init_labels(self) -> None:
"""Makes some of the labels bold for better readability.
"""
"""Makes some of the labels bold for better readability."""
self._ui_form.titleconnectionLabel.setStyleSheet("font-weight: bold;")
self._ui_form.titlefrequencyLabel.setStyleSheet("font-weight: bold;")
self._ui_form.titletypeLabel.setStyleSheet("font-weight: bold;")
self._ui_form.titleinfoLabel.setStyleSheet("font-weight: bold;")
def init_plot(self) -> None:
"""Initialize the S11 plot. """
"""Initialize the S11 plot."""
ax = self._ui_form.S11Plot.canvas.ax
ax.set_xlabel("Frequency (MHz)")
ax.set_ylabel("S11 (dB)")
@ -87,7 +108,7 @@ class AutoTMView(ModuleView):
self._ui_form.S11Plot.canvas.draw()
def on_calibration_button_clicked(self) -> None:
"""This method is called when the calibration button is clicked.
"""This method is called when the calibration button is clicked.
It opens the calibration window.
"""
logger.debug("Calibration button clicked")
@ -96,7 +117,7 @@ class AutoTMView(ModuleView):
@pyqtSlot(list)
def on_available_devices_changed(self, available_devices: list) -> None:
"""Update the available devices list in the view. """
"""Update the available devices list in the view."""
logger.debug("Updating available devices list")
self._ui_form.portBox.clear()
self._ui_form.portBox.addItems(available_devices)
@ -109,7 +130,7 @@ class AutoTMView(ModuleView):
@pyqtSlot()
def on_connect_button_clicked(self) -> None:
"""This method is called when the connect button is clicked.
"""This method is called when the connect button is clicked.
It calls the connect method of the controller with the currently selected device.
"""
logger.debug("Connect button clicked")
@ -118,7 +139,7 @@ class AutoTMView(ModuleView):
@pyqtSlot(QSerialPort)
def on_serial_changed(self, serial: QSerialPort) -> None:
"""Update the serial 'connectionLabel' according to the current serial connection.
"""Update the serial 'connectionLabel' according to the current serial connection.
Args:
serial (serial.Serial): The current serial connection."""
@ -131,12 +152,12 @@ class AutoTMView(ModuleView):
logger.debug("Updated serial connection label")
def plot_measurement(self, data: "S11Data") -> None:
"""Update the S11 plot with the current data points.
"""Update the S11 plot with the current data points.
Args:
data_points (list): List of data points to plot.
data_points (list): List of data points to plot.
@TODO: implement proper calibration. See the controller class for more information.
@TODO: implement proper calibration. See the controller class for more information.
"""
frequency = data.frequency
return_loss_db = data.return_loss_db
@ -165,14 +186,17 @@ class AutoTMView(ModuleView):
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]
# 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)]
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)
]
""" fig, ax = plt.subplots()
ax.plot([g.real for g in gamma_corr], [g.imag for g in gamma_corr])
ax.set_aspect('equal')
@ -181,8 +205,9 @@ class AutoTMView(ModuleView):
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]
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)|")
@ -204,10 +229,10 @@ class AutoTMView(ModuleView):
QApplication.processEvents()
def add_info_text(self, text: str) -> None:
""" Adds text to the info text box.
"""Adds text to the info text box.
Args:
text (str): Text to add to the info text box.
text (str): Text to add to the info text box.
"""
# Add a timestamp to the text
timestamp = datetime.now().strftime("%H:%M:%S")
@ -216,13 +241,14 @@ class AutoTMView(ModuleView):
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().maximum()
)
def add_error_text(self, text: str) -> None:
""" Adds text to the error text box.
"""Adds text to the error text box.
Args:
text (str): Text to add to the error text box.
text (str): Text to add to the error text box.
"""
# Add a timestamp to the text
timestamp = datetime.now().strftime("%H:%M:%S")
@ -231,10 +257,11 @@ class AutoTMView(ModuleView):
text_label.setStyleSheet("font-size: 25px; color: red;")
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().maximum()
)
def create_frequency_sweep_spinner_dialog(self) -> None:
"""Creates a frequency sweep spinner dialog. """
"""Creates a frequency sweep spinner dialog."""
self.frequency_sweep_spinner = self.FrequencySweepSpinner()
self.frequency_sweep_spinner.show()
@ -259,7 +286,6 @@ class AutoTMView(ModuleView):
self.spinner_movie.start()
class CalibrationWindow(QWidget):
def __init__(self, module, parent=None):
super().__init__()
self.module = module
@ -290,10 +316,11 @@ class AutoTMView(ModuleView):
# 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_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)
@ -303,10 +330,11 @@ class AutoTMView(ModuleView):
# 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_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)
@ -316,10 +344,11 @@ class AutoTMView(ModuleView):
# 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_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)
@ -347,26 +376,28 @@ class AutoTMView(ModuleView):
# Connect the calibration finished signals to the on_calibration_finished slot
self.module.model.short_calibration_finished.connect(
self.on_short_calibration_finished)
self.on_short_calibration_finished
)
self.module.model.open_calibration_finished.connect(
self.on_open_calibration_finished)
self.on_open_calibration_finished
)
self.module.model.load_calibration_finished.connect(
self.on_load_calibration_finished)
self.on_load_calibration_finished
)
def on_short_calibration_finished(self, short_calibration: "S11Data") -> None:
self.on_calibration_finished(
"short", self.short_plot, short_calibration)
self.on_calibration_finished("short", self.short_plot, short_calibration)
def on_open_calibration_finished(self, open_calibration: "S11Data") -> None:
self.on_calibration_finished(
"open", self.open_plot, open_calibration)
self.on_calibration_finished("open", self.open_plot, open_calibration)
def on_load_calibration_finished(self, load_calibration: "S11Data") -> None:
self.on_calibration_finished(
"load", self.load_plot, load_calibration)
self.on_calibration_finished("load", self.load_plot, load_calibration)
def on_calibration_finished(self, type: str, widget: MplWidget, data: "S11Data") -> None:
"""This method is called when a calibration has finished.
def on_calibration_finished(
self, type: str, widget: MplWidget, data: "S11Data"
) -> None:
"""This method is called when a calibration has finished.
It plots the calibration data on the given widget.
"""
frequency = data.frequency
@ -403,7 +434,7 @@ class AutoTMView(ModuleView):
self.module.controller.export_calibration(filename)
def on_import_button_clicked(self) -> None:
"""This method is called when the import button is clicked. """
"""This method is called when the import button is clicked."""
filedialog = QFileDialog()
filedialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
filedialog.setNameFilter("calibration files (*.cal)")
@ -414,7 +445,7 @@ class AutoTMView(ModuleView):
self.module.controller.import_calibration(filename)
def on_apply_button_clicked(self) -> None:
"""This method is called when the apply button is clicked. """
"""This method is called when the apply button is clicked."""
self.module.controller.calculate_calibration()
# Close the calibration window
self.close()

View file

@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(1280, 705)
Form.resize(1280, 745)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@ -70,28 +70,60 @@ class Ui_Form(object):
self.elecTab.setObjectName("elecTab")
self.gridLayout_3 = QtWidgets.QGridLayout(self.elecTab)
self.gridLayout_3.setObjectName("gridLayout_3")
self.matchingBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.matchingBox.setObjectName("matchingBox")
self.gridLayout_3.addWidget(self.matchingBox, 1, 1, 1, 1)
self.setvoltagesButton = QtWidgets.QPushButton(parent=self.elecTab)
self.setvoltagesButton.setObjectName("setvoltagesButton")
self.gridLayout_3.addWidget(self.setvoltagesButton, 2, 0, 1, 2)
self.stopfrequencyBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.stopfrequencyBox.setObjectName("stopfrequencyBox")
self.gridLayout_3.addWidget(self.stopfrequencyBox, 7, 1, 1, 1)
self.label_9 = QtWidgets.QLabel(parent=self.elecTab)
font = QtGui.QFont()
font.setBold(True)
self.label_9.setFont(font)
self.label_9.setObjectName("label_9")
self.gridLayout_3.addWidget(self.label_9, 0, 0, 1, 1)
self.label_11 = QtWidgets.QLabel(parent=self.elecTab)
font = QtGui.QFont()
font.setBold(True)
self.label_11.setFont(font)
self.label_11.setObjectName("label_11")
self.gridLayout_3.addWidget(self.label_11, 4, 0, 1, 1)
self.tuningBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.tuningBox.setObjectName("tuningBox")
self.gridLayout_3.addWidget(self.tuningBox, 0, 1, 1, 1)
self.label_3 = QtWidgets.QLabel(parent=self.elecTab)
self.label_3.setObjectName("label_3")
self.gridLayout_3.addWidget(self.label_3, 1, 0, 1, 1)
self.label_2 = QtWidgets.QLabel(parent=self.elecTab)
self.label_2.setObjectName("label_2")
self.gridLayout_3.addWidget(self.label_2, 0, 0, 1, 1)
self.gridLayout_3.addWidget(self.tuningBox, 1, 1, 1, 1)
self.label_12 = QtWidgets.QLabel(parent=self.elecTab)
self.label_12.setObjectName("label_12")
self.gridLayout_3.addWidget(self.label_12, 6, 0, 1, 1)
self.label_4 = QtWidgets.QLabel(parent=self.elecTab)
self.label_4.setObjectName("label_4")
self.gridLayout_3.addWidget(self.label_4, 3, 0, 1, 1)
self.gridLayout_3.addWidget(self.label_4, 5, 0, 1, 1)
self.label_3 = QtWidgets.QLabel(parent=self.elecTab)
self.label_3.setObjectName("label_3")
self.gridLayout_3.addWidget(self.label_3, 2, 0, 1, 1)
self.matchingBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.matchingBox.setObjectName("matchingBox")
self.gridLayout_3.addWidget(self.matchingBox, 2, 1, 1, 1)
self.label_2 = QtWidgets.QLabel(parent=self.elecTab)
self.label_2.setObjectName("label_2")
self.gridLayout_3.addWidget(self.label_2, 1, 0, 1, 1)
self.label_13 = QtWidgets.QLabel(parent=self.elecTab)
self.label_13.setObjectName("label_13")
self.gridLayout_3.addWidget(self.label_13, 7, 0, 1, 1)
self.generateLUTButton = QtWidgets.QPushButton(parent=self.elecTab)
self.generateLUTButton.setObjectName("generateLUTButton")
self.gridLayout_3.addWidget(self.generateLUTButton, 9, 0, 1, 2)
self.resolutionBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.resolutionBox.setObjectName("resolutionBox")
self.gridLayout_3.addWidget(self.resolutionBox, 3, 1, 1, 1)
self.gridLayout_3.setRowStretch(3, 1)
self.gridLayout_3.addWidget(self.resolutionBox, 5, 1, 1, 1)
self.startfrequencyBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.startfrequencyBox.setObjectName("startfrequencyBox")
self.gridLayout_3.addWidget(self.startfrequencyBox, 6, 1, 1, 1)
self.setvoltagesButton = QtWidgets.QPushButton(parent=self.elecTab)
self.setvoltagesButton.setObjectName("setvoltagesButton")
self.gridLayout_3.addWidget(self.setvoltagesButton, 3, 0, 1, 2)
self.label_14 = QtWidgets.QLabel(parent=self.elecTab)
self.label_14.setObjectName("label_14")
self.gridLayout_3.addWidget(self.label_14, 8, 0, 1, 1)
self.frequencystepBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab)
self.frequencystepBox.setObjectName("frequencystepBox")
self.gridLayout_3.addWidget(self.frequencystepBox, 8, 1, 1, 1)
self.typeTab.addTab(self.elecTab, "")
self.verticalLayout_2.addWidget(self.typeTab)
self.titlefrequencyLabel = QtWidgets.QLabel(parent=Form)
@ -140,7 +172,7 @@ class Ui_Form(object):
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setObjectName("scrollArea")
self.scrollAreaWidgetContents = QtWidgets.QWidget()
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 297, 169))
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 297, 68))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.verticalLayout_2.addWidget(self.scrollArea)
@ -159,7 +191,7 @@ class Ui_Form(object):
self.horizontalLayout_2.setStretch(1, 1)
self.retranslateUi(Form)
self.typeTab.setCurrentIndex(0)
self.typeTab.setCurrentIndex(1)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
@ -173,10 +205,16 @@ class Ui_Form(object):
self.titletypeLabel.setText(_translate("Form", "T&M Type:"))
self.pushButton.setText(_translate("Form", "Homing"))
self.typeTab.setTabText(self.typeTab.indexOf(self.mechTab), _translate("Form", "Mechanical"))
self.setvoltagesButton.setText(_translate("Form", "Set Voltages"))
self.label_9.setText(_translate("Form", "Set Voltages:"))
self.label_11.setText(_translate("Form", "Generate LUT:"))
self.label_12.setText(_translate("Form", "Start Frequency (MHz)"))
self.label_4.setText(_translate("Form", "Voltage Resolution"))
self.label_3.setText(_translate("Form", "Voltage Matching"))
self.label_2.setText(_translate("Form", "Voltage Tuning"))
self.label_4.setText(_translate("Form", "Voltage Resolution"))
self.label_13.setText(_translate("Form", "Stop Frequency (MHz)"))
self.generateLUTButton.setText(_translate("Form", "Start Voltage Sweep"))
self.setvoltagesButton.setText(_translate("Form", "Set Voltages"))
self.label_14.setText(_translate("Form", "Frequency Step (MHz)"))
self.typeTab.setTabText(self.typeTab.indexOf(self.elecTab), _translate("Form", "Electrical"))
self.titlefrequencyLabel.setText(_translate("Form", "Frequency Sweep:"))
self.label_8.setText(_translate("Form", "MHz"))