This commit is contained in:
jupfi 2024-05-08 16:26:57 +02:00
parent 3a23b126fe
commit bfbc20c2e9
5 changed files with 556 additions and 212 deletions

View file

@ -1 +1,3 @@
"""The NQRduck AutoTM module. It is used to automatically tune and match magnetic resonance probe coils."""
from .autotm import AutoTM as Module

View file

@ -1,3 +1,5 @@
"""The Module creation snippet for the NQRduck AutoTM module."""
from nqrduck.module.module import Module
from .model import AutoTMModel
from .view import AutoTMView

View file

@ -1,3 +1,5 @@
"""The controller for the NQRduck AutoTM module."""
import logging
import time
import numpy as np
@ -8,47 +10,75 @@ from PyQt6.QtCore import pyqtSlot
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication
from nqrduck.module.module_controller import ModuleController
from .model import S11Data, ElectricalLookupTable, MechanicalLookupTable, SavedPosition, Stepper
from .model import (
S11Data,
ElectricalLookupTable,
MechanicalLookupTable,
SavedPosition,
Stepper,
)
logger = logging.getLogger(__name__)
class AutoTMController(ModuleController):
"""The controller for the NQRduck AutoTM module. It handles the serial connection and the signals and slots."""
BAUDRATE = 115200
def on_loading(self):
def on_loading(self) -> None:
"""This method is called when the module is loaded.
It sets up the serial connection and connects the signals and slots.
"""
logger.debug("Setting up serial connection")
self.find_devices()
# Connect signals
self.module.model.serial_data_received.connect(self.process_frequency_sweep_data)
self.module.model.serial_data_received.connect(
self.process_frequency_sweep_data
)
self.module.model.serial_data_received.connect(self.process_measurement_data)
self.module.model.serial_data_received.connect(self.process_calibration_data)
self.module.model.serial_data_received.connect(self.process_voltage_sweep_result)
self.module.model.serial_data_received.connect(
self.process_voltage_sweep_result
)
self.module.model.serial_data_received.connect(self.print_info)
self.module.model.serial_data_received.connect(self.read_position_data)
self.module.model.serial_data_received.connect(self.process_reflection_data)
self.module.model.serial_data_received.connect(self.process_position_sweep_result)
self.module.model.serial_data_received.connect(
self.process_position_sweep_result
)
self.module.model.serial_data_received.connect(self.process_signalpath_data)
@pyqtSlot(str, object)
def process_signals(self, key: str, value: object) -> None:
"""Slot for setting the tune and match frequency.
Args:
key (str): The key of the signal. If the key is "set_tune_and_match", the tune and match method is called.
value (object): The value of the signal. The value is the frequency to tune and match to.
"""
logger.debug("Received signal: %s", key)
if key == "set_tune_and_match":
self.tune_and_match(value)
def tune_and_match(self, frequency: float) -> None:
"""This method is called when this module already has a LUT table. It should then tune and match the probe coil to the specified frequency.
"""This method is called when this module already has a LUT table.
It should then tune and match the probe coil to the specified frequency.
Args:
frequency (float): The frequency to tune and match to.
"""
if self.module.model.LUT is None:
logger.error("Could not tune and match. No LUT available.")
return
elif self.module.model.LUT.TYPE == "Electrical":
tuning_voltage, matching_voltage = self.module.model.LUT.get_voltages(frequency)
confirmation = self.set_voltages(str(tuning_voltage), str(matching_voltage))
tuning_voltage, matching_voltage = self.module.model.LUT.get_voltages(
frequency
)
_ = self.set_voltages(str(tuning_voltage), str(matching_voltage))
# We need to change the signal pathway to preamp to measure the reflection
self.switch_to_atm()
reflection = self.read_reflection(frequency)
@ -57,7 +87,9 @@ class AutoTMController(ModuleController):
self.module.nqrduck_signal.emit("confirm_tune_and_match", reflection)
elif self.module.model.LUT.TYPE == "Mechanical":
tuning_position, matching_position = self.module.model.LUT.get_positions(frequency)
tuning_position, matching_position = self.module.model.LUT.get_positions(
frequency
)
self.go_to_position(tuning_position, matching_position)
self.switch_to_atm()
# Switch to atm to measure the reflection
@ -68,7 +100,12 @@ class AutoTMController(ModuleController):
# The Lime doesn"t like it if we send the command to switch to atm and then immediately send the command to measure the reflection.
# So we wait a bit before starting the measurement
QTimer.singleShot(100, lambda: self.module.nqrduck_signal.emit("confirm_tune_and_match", reflection))
QTimer.singleShot(
100,
lambda: self.module.nqrduck_signal.emit(
"confirm_tune_and_match", reflection
),
)
def find_devices(self) -> None:
"""Scan for available serial devices and add them to the model as available devices."""
@ -127,6 +164,7 @@ class AutoTMController(ModuleController):
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:
@ -162,13 +200,8 @@ 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 = f"Could not start frequency sweep. Start and stop frequency must be between {MIN_FREQUENCY / 1e6} and {MAX_FREQUENCY / 1e6} MHz"
logger.error(error)
self.module.view.add_info_text(error)
return
@ -182,7 +215,7 @@ class AutoTMController(ModuleController):
)
# Print the command 'f<start>f<stop>f<step>' to the serial connection
command = "f%sf%sf%s" % (start_frequency, stop_frequency, frequency_step)
command = f"f{start_frequency}f{stop_frequency}f{frequency_step}"
self.module.model.frequency_sweep_start = time.time()
confirmation = self.send_command(command)
if confirmation:
@ -191,20 +224,31 @@ class AutoTMController(ModuleController):
self.module.view.create_frequency_sweep_spinner_dialog()
@pyqtSlot(str)
def process_frequency_sweep_data(self, text : str) -> None:
def process_frequency_sweep_data(self, text: str) -> None:
"""This method is called when data is received from the serial connection during a frequency sweep.
It processes the data and adds it to the model.
Args:
text (str): The data received from the serial connection.
"""
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"))
self.module.model.add_data_point(frequency, return_loss, phase)
@pyqtSlot(str)
def process_measurement_data(self, text : str) -> None:
def process_measurement_data(self, text: str) -> None:
"""This method is called when data is received from the serial connection during a measurement.
It processes the data and adds it to the model.
Args:
text (str): The data received from the serial connection.
"""
if self.module.model.active_calibration is None and text.startswith("r"):
logger.debug("Measurement finished")
@ -214,24 +258,33 @@ class AutoTMController(ModuleController):
self.finish_frequency_sweep()
@pyqtSlot(str)
def process_calibration_data(self, text : str) -> None:
def process_calibration_data(self, text: str) -> None:
"""This method is called when data is received from the serial connection during a calibration.
It processes the data and adds it to the model.
Args:
calibration_type (str): The type of calibration that is being performed.
text (str): The data received from the serial connection.
"""
if text.startswith("r") and self.module.model.active_calibration in ["short", "open", "load"]:
if text.startswith("r") and self.module.model.active_calibration in [
"short",
"open",
"load",
]:
calibration_type = self.module.model.active_calibration
logger.debug(f"{calibration_type.capitalize()} calibration finished")
setattr(self.module.model, f"{calibration_type}_calibration",
S11Data(self.module.model.data_points.copy()))
setattr(
self.module.model,
f"{calibration_type}_calibration",
S11Data(self.module.model.data_points.copy()),
)
self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide()
@pyqtSlot(str)
def process_voltage_sweep_result(self, text : str) -> None:
def process_voltage_sweep_result(self, text: str) -> None:
"""This method is called when data is received from the serial connection during a voltage sweep.
It processes the data and adds it to the model.
Args:
@ -243,25 +296,40 @@ class AutoTMController(ModuleController):
LUT = self.module.model.el_lut
if LUT is not None:
if LUT.is_incomplete():
logger.debug("Received voltage sweep result: Tuning %s Matching %s", tuning_voltage, matching_voltage)
logger.debug(
"Received voltage sweep result: Tuning %s Matching %s",
tuning_voltage,
matching_voltage,
)
LUT.add_voltages(tuning_voltage, matching_voltage)
self.continue_or_finish_voltage_sweep(LUT)
self.module.model.tuning_voltage = tuning_voltage
self.module.model.matching_voltage = matching_voltage
logger.debug("Updated voltages: Tuning %s Matching %s", self.module.model.tuning_voltage, self.module.model.matching_voltage)
logger.debug(
"Updated voltages: Tuning %s Matching %s",
self.module.model.tuning_voltage,
self.module.model.matching_voltage,
)
def finish_frequency_sweep(self):
def finish_frequency_sweep(self) -> None:
"""This method is called when a frequency sweep is finished.
It hides the frequency sweep spinner dialog and adds the data to the model.
"""
self.module.view.frequency_sweep_spinner.hide()
self.module.model.frequency_sweep_stop = time.time()
duration = self.module.model.frequency_sweep_stop - self.module.model.frequency_sweep_start
self.module.view.add_info_text(f"Frequency sweep finished in {duration:.2f} seconds")
duration = (
self.module.model.frequency_sweep_stop
- self.module.model.frequency_sweep_start
)
self.module.view.add_info_text(
f"Frequency sweep finished in {duration:.2f} seconds"
)
def continue_or_finish_voltage_sweep(self, LUT):
def continue_or_finish_voltage_sweep(self, LUT) -> None:
"""This method is called when a voltage sweep is finished.
It checks if the voltage sweep is finished or if the next voltage sweep should be started.
Args:
@ -274,8 +342,9 @@ class AutoTMController(ModuleController):
# Finish voltage sweep
self.finish_voltage_sweep(LUT)
def start_next_voltage_sweep(self, LUT):
def start_next_voltage_sweep(self, LUT) -> None:
"""This method is called when a voltage sweep is finished.
It starts the next voltage sweep.
Args:
@ -286,16 +355,17 @@ class AutoTMController(ModuleController):
if self.module.view._ui_form.prevVoltagecheckBox.isChecked():
# Command format is s<frequency in MHz>o<optional tuning voltage>o<optional matching voltage>
# We use the currently set voltages
command = "s%so%so%s" % (next_frequency, self.module.model.tuning_voltage, self.module.model.matching_voltage)
command = f"s{next_frequency}o{self.module.model.tuning_voltage}o{self.module.model.matching_voltage}"
else:
command = "s%s" % (next_frequency)
command = f"s{next_frequency}"
LUT.started_frequency = next_frequency
logger.debug("Starting next voltage sweep: %s", command)
self.send_command(command)
def finish_voltage_sweep(self, LUT):
def finish_voltage_sweep(self, LUT) -> None:
"""This method is called when a voltage sweep is finished.
It hides the voltage sweep spinner dialog and adds the data to the model.
Args:
@ -305,13 +375,18 @@ class AutoTMController(ModuleController):
self.module.view.el_LUT_spinner.hide()
self.module.model.LUT = LUT
self.module.model.voltage_sweep_stop = time.time()
duration = self.module.model.voltage_sweep_stop - self.module.model.voltage_sweep_start
self.module.view.add_info_text(f"Voltage sweep finished in {duration:.2f} seconds")
duration = (
self.module.model.voltage_sweep_stop - self.module.model.voltage_sweep_start
)
self.module.view.add_info_text(
f"Voltage sweep finished in {duration:.2f} seconds"
)
self.module.nqrduck_signal.emit("LUT_finished", LUT)
@pyqtSlot(str)
def print_info(self, text : str) -> None:
def print_info(self, text: str) -> None:
"""This method is called when data is received from the serial connection.
It prints the data to the info text box.
Args:
@ -325,8 +400,14 @@ class AutoTMController(ModuleController):
self.module.view.add_error_text(text)
@pyqtSlot(str)
def read_position_data(self, text : str) -> None:
"""This method is called when data is received from the serial connection."""
def read_position_data(self, text: str) -> None:
"""This method is called when data is received from the serial connection.
It processes the data and adds it to the model.
Args:
text (str): The data received from the serial connection.
"""
if text.startswith("p"):
# Format is p<tuning_position>m<matching_position>
text = text[1:].split("m")
@ -335,7 +416,11 @@ class AutoTMController(ModuleController):
self.module.model.matching_stepper.position = matching_position
self.module.model.tuning_stepper.homed = True
self.module.model.matching_stepper.homed = True
logger.debug("Tuning position: %s, Matching position: %s", tuning_position, matching_position)
logger.debug(
"Tuning position: %s, Matching position: %s",
tuning_position,
matching_position,
)
self.module.view.on_active_stepper_changed()
def on_ready_read(self) -> None:
@ -349,8 +434,9 @@ class AutoTMController(ModuleController):
self.module.model.serial_data_received.emit(text)
@pyqtSlot(str)
def process_reflection_data(self, text):
def process_reflection_data(self, text: str) -> None:
"""This method is called when data is received from the serial connection.
It processes the data and adds it to the model.
Args:
@ -367,7 +453,12 @@ class AutoTMController(ModuleController):
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.
Args:
start_frequency (float): The start frequency in MHz.
stop_frequency (float): The stop frequency in MHz.
"""
logger.debug("Starting short calibration")
self.module.model.init_short_calibration()
@ -377,7 +468,12 @@ class AutoTMController(ModuleController):
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.
Args:
start_frequency (float) : The start frequency in MHz.
stop_frequency (float) : The stop frequency in MHz.
"""
logger.debug("Starting open calibration")
self.module.model.init_open_calibration()
@ -387,7 +483,12 @@ class AutoTMController(ModuleController):
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.
Args:
start_frequency (float) : The start frequency in MHz.
stop_frequency (float) : The stop frequency in MHz.
"""
logger.debug("Starting load calibration")
self.module.model.init_load_calibration()
@ -395,6 +496,7 @@ class AutoTMController(ModuleController):
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.
@TODO: Improvements to the calibrations can be made the following ways:
@ -406,17 +508,17 @@ 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:
if self.module.model.short_calibration is None:
logger.error(
"Could not calculate calibration. No short calibration data points available."
)
return
if self.module.model.open_calibration == None:
if self.module.model.open_calibration is None:
logger.error(
"Could not calculate calibration. No open calibration data points available."
)
return
if self.module.model.load_calibration == None:
if self.module.model.load_calibration is None:
logger.error(
"Could not calculate calibration. No load calibration data points available."
)
@ -459,6 +561,7 @@ class AutoTMController(ModuleController):
def export_calibration(self, filename: str) -> None:
"""This method is called when the export calibration button is pressed.
It exports the data of the short, open and load calibration to a file.
Args:
@ -466,19 +569,19 @@ 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:
if self.module.model.short_calibration is None:
logger.error(
"Could not export calibration. No short calibration data points available."
)
return
if self.module.model.open_calibration == None:
if self.module.model.open_calibration is None:
logger.error(
"Could not export calibration. No open calibration data points available."
)
return
if self.module.model.load_calibration == None:
if self.module.model.load_calibration is None:
logger.error(
"Could not export calibration. No load calibration data points available."
)
@ -496,6 +599,7 @@ class AutoTMController(ModuleController):
def import_calibration(self, filename: str) -> None:
"""This method is called when the import calibration button is pressed.
It imports the data of the short, open and load calibration from a file.
Args:
@ -542,6 +646,7 @@ class AutoTMController(ModuleController):
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:
@ -583,16 +688,22 @@ class AutoTMController(ModuleController):
matching_voltage,
)
if tuning_voltage == self.module.model.tuning_voltage and matching_voltage == self.module.model.matching_voltage:
if (
tuning_voltage == self.module.model.tuning_voltage
and matching_voltage == self.module.model.matching_voltage
):
logger.debug("Voltages already set")
return
command = "v%sv%s" % (tuning_voltage, matching_voltage)
command = f"v{tuning_voltage}v{matching_voltage}"
start_time = time.time()
confirmation = self.send_command(command)
while matching_voltage != self.module.model.matching_voltage and tuning_voltage != self.module.model.tuning_voltage:
while (
matching_voltage != self.module.model.matching_voltage
and tuning_voltage != self.module.model.tuning_voltage
):
QApplication.processEvents()
# Check for timeout
if time.time() - start_time > timeout_duration:
@ -614,6 +725,7 @@ class AutoTMController(ModuleController):
frequency_step: 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:
@ -635,11 +747,7 @@ class AutoTMController(ModuleController):
self.module.view.add_info_text(error)
return
if (
start_frequency < 0
or stop_frequency < 0
or frequency_step < 0
):
if start_frequency < 0 or stop_frequency < 0 or frequency_step < 0:
error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be positive"
logger.error(error)
self.module.view.add_info_text(error)
@ -669,9 +777,7 @@ class AutoTMController(ModuleController):
# self.set_voltages("0", "0")
# We create the lookup table
LUT = ElectricalLookupTable(
start_frequency, stop_frequency, frequency_step
)
LUT = ElectricalLookupTable(start_frequency, stop_frequency, frequency_step)
LUT.started_frequency = start_frequency
@ -679,10 +785,14 @@ class AutoTMController(ModuleController):
if self.module.view._ui_form.prevVoltagecheckBox.isChecked():
# Command format is s<frequency in MHz>o<optional tuning voltage>o<optional matching voltage>
# We use the currently set voltages
logger.debug("Starting preset Voltage sweep with voltage Tuning: %s V and Matching: %s V", self.module.model.tuning_voltage, self.module.model.matching_voltage)
command = "s%so%so%s" % (start_frequency, self.module.model.tuning_voltage, self.module.model.matching_voltage)
logger.debug(
"Starting preset Voltage sweep with voltage Tuning: %s V and Matching: %s V",
self.module.model.tuning_voltage,
self.module.model.matching_voltage,
)
command = f"s{start_frequency}o{self.module.model.tuning_voltage}o{self.module.model.matching_voltage}"
else:
command = "s%s" % (start_frequency)
command = f"s{start_frequency}"
# For timing of the voltage sweep
self.module.model.voltage_sweep_start = time.time()
@ -694,6 +804,7 @@ class AutoTMController(ModuleController):
def switch_to_preamp(self) -> None:
"""This method is used to send the command 'cp' to the atm system. This switches the signal pathway of the atm system to 'RX' to 'Preamp'.
This is the mode for either NQR or NMR measurements or if on wants to check the tuning of the probe coil on a network analyzer.
"""
if self.module.model.signal_path == "preamp":
@ -714,6 +825,7 @@ class AutoTMController(ModuleController):
def switch_to_atm(self) -> None:
"""This method is used to send the command 'ca' to the atm system. This switches the signal pathway of the atm system to 'RX' to 'ATM.
In this state the atm system can be used to measure the reflection coefficient of the probecoils.
"""
if self.module.model.signal_path == "atm":
@ -732,8 +844,9 @@ class AutoTMController(ModuleController):
logger.error("Switching to atm timed out")
break
def process_signalpath_data(self, text : str) -> None:
def process_signalpath_data(self, text: str) -> None:
"""This method is called when data is received from the serial connection.
It processes the data and adds it to the model.
Args:
@ -765,7 +878,7 @@ class AutoTMController(ModuleController):
)
return False
if self.module.model.serial.isOpen() == False:
if self.module.model.serial.isOpen() is False:
logger.error("Could not send command. Serial connection is not open")
self.module.view.add_error_text(
"Could not send command. Serial connection is not open"
@ -796,12 +909,13 @@ class AutoTMController(ModuleController):
except Exception as e:
logger.error("Could not send command. %s", e)
self.module.view.add_error_text("Could not send command. %s" % e)
self.module.view.add_error_text(f"Could not send command. {e}")
### Stepper Motor Control ###
def homing(self) -> None:
"""This method is used to send the command 'h' to the atm system.
This command is used to home the stepper motors of the atm system.
"""
logger.debug("Homing")
@ -812,6 +926,7 @@ class AutoTMController(ModuleController):
@pyqtSlot(str)
def on_stepper_changed(self, stepper: str) -> None:
"""This method is called when the stepper position is changed.
It sends the command to the atm system to change the stepper position.
Args:
@ -824,25 +939,52 @@ class AutoTMController(ModuleController):
elif stepper == "matching":
self.module.model.active_stepper = self.module.model.matching_stepper
def validate_position(self, future_position: int, stepper : Stepper) -> bool:
"""Validate the stepper's future position."""
def validate_position(self, future_position: int, stepper: Stepper) -> bool:
"""Validate the stepper's future position.
Args:
future_position (int): The future position of the stepper.
stepper (Stepper): The stepper that is being moved.
Returns:
bool: True if the position is valid, False otherwise.
"""
if future_position < 0:
self.module.view.add_error_text("Could not move stepper. Stepper position cannot be negative")
self.module.view.add_error_text(
"Could not move stepper. Stepper position cannot be negative"
)
return False
if future_position > stepper.MAX_STEPS:
self.module.view.add_error_text(f"Could not move stepper. Stepper position cannot be larger than {stepper.MAX_STEPS}")
self.module.view.add_error_text(
f"Could not move stepper. Stepper position cannot be larger than {stepper.MAX_STEPS}"
)
return False
return True
def calculate_steps_for_absolute_move(self, target_position: int, stepper : Stepper) -> int:
"""Calculate the number of steps for an absolute move."""
def calculate_steps_for_absolute_move(
self, target_position: int, stepper: Stepper
) -> int:
"""Calculate the number of steps for an absolute move.
Args:
target_position (int): The target position of the stepper.
stepper (Stepper): The stepper that is being moved.
Returns:
int: The number of steps to move.
"""
current_position = stepper.position
return target_position - current_position
def send_stepper_command(self, steps: int, stepper : Stepper) -> None:
"""Send a command to the stepper motor based on the number of steps."""
def send_stepper_command(self, steps: int, stepper: Stepper) -> None:
"""Send a command to the stepper motor based on the number of steps.
Args:
steps (int): The number of steps to move.
stepper (Stepper): The stepper that is being moved.
"""
# Here we handle backlash of the tuner
# Determine the direction of the current steps
backlash = 0
@ -862,7 +1004,12 @@ class AutoTMController(ModuleController):
return confirmation
def on_relative_move(self, steps: str, stepper: Stepper = None) -> None:
"""This method is called when the relative move button is pressed."""
"""This method is called when the relative move button is pressed.
Args:
steps (str): The number of steps to move.
stepper (Stepper): The stepper that is being moved. Default is None.
"""
timeout_duration = 15 # timeout in seconds
start_time = time.time()
@ -876,7 +1023,9 @@ class AutoTMController(ModuleController):
return
if self.validate_position(future_position, stepper):
confirmation = self.send_stepper_command(int(steps), stepper) # Convert the steps string to an integer
confirmation = self.send_stepper_command(
int(steps), stepper
) # Convert the steps string to an integer
while stepper_position == stepper.position:
QApplication.processEvents()
@ -888,7 +1037,12 @@ class AutoTMController(ModuleController):
return confirmation
def on_absolute_move(self, steps: str, stepper: Stepper = None) -> None:
"""This method is called when the absolute move button is pressed."""
"""This method is called when the absolute move button is pressed.
Args:
steps (str): The number of steps to move.
stepper (Stepper): The stepper that is being moved. Default is None.
"""
timeout_duration = 15 # timeout in seconds
start_time = time.time()
@ -903,7 +1057,9 @@ class AutoTMController(ModuleController):
return
if self.validate_position(future_position, stepper):
actual_steps = self.calculate_steps_for_absolute_move(future_position, stepper)
actual_steps = self.calculate_steps_for_absolute_move(
future_position, stepper
)
confirmation = self.send_stepper_command(actual_steps, stepper)
while stepper_position == stepper.position:
@ -917,7 +1073,7 @@ class AutoTMController(ModuleController):
### Position Saving and Loading ###
def load_positions(self, path : str) -> None:
def load_positions(self, path: str) -> None:
"""Load the saved positions from a json file.
Args:
@ -930,8 +1086,11 @@ class AutoTMController(ModuleController):
positions = json.load(f)
for position in positions:
logger.debug("Loading position: %s", position)
self.add_position(position["frequency"], position["tuning_position"], position["matching_position"])
self.add_position(
position["frequency"],
position["tuning_position"],
position["matching_position"],
)
def save_positions(self, path: str) -> None:
"""Save the current positions to a json file.
@ -944,7 +1103,9 @@ class AutoTMController(ModuleController):
json_position = [position.to_json() for position in positions]
json.dump(json_position, f)
def add_position(self, frequency: str, tuning_position: str, matching_position: str) -> None:
def add_position(
self, frequency: str, tuning_position: str, matching_position: str
) -> None:
"""Add a position to the lookup table.
Args:
@ -953,7 +1114,9 @@ class AutoTMController(ModuleController):
matching_position (str): The matching position.
"""
logger.debug("Adding new position at %s MHz", frequency)
self.module.model.add_saved_position(frequency, tuning_position, matching_position)
self.module.model.add_saved_position(
frequency, tuning_position, matching_position
)
def on_go_to_position(self, position: SavedPosition) -> None:
"""Go to the specified position.
@ -962,9 +1125,13 @@ class AutoTMController(ModuleController):
position (SavedPosition): The position to go to.
"""
logger.debug("Going to position: %s", position)
confirmation = self.on_absolute_move(position.tuning_position, self.module.model.tuning_stepper)
confirmation = self.on_absolute_move(
position.tuning_position, self.module.model.tuning_stepper
)
if confirmation:
self.on_absolute_move(position.matching_position, self.module.model.matching_stepper)
self.on_absolute_move(
position.matching_position, self.module.model.matching_stepper
)
def on_delete_position(self, position: SavedPosition) -> None:
"""Delete the specified position.
@ -975,10 +1142,11 @@ class AutoTMController(ModuleController):
logger.debug("Deleting position: %s", position)
self.module.model.delete_saved_position(position)
#### Mechanical tuning and matching ####
def generate_mechanical_lut(self, start_frequency: str, stop_frequency: str, frequency_step: str) -> None:
def generate_mechanical_lut(
self, start_frequency: str, stop_frequency: str, frequency_step: str
) -> None:
"""Generate a lookup table for the specified frequency range and voltage resolution.
Args:
@ -999,11 +1167,7 @@ class AutoTMController(ModuleController):
self.module.view.add_info_text(error)
return
if (
start_frequency < 0
or stop_frequency < 0
or frequency_step < 0
):
if start_frequency < 0 or stop_frequency < 0 or frequency_step < 0:
error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be positive"
logger.error(error)
self.module.view.add_info_text(error)
@ -1032,9 +1196,7 @@ class AutoTMController(ModuleController):
self.switch_to_atm()
# We create the lookup table
LUT = MechanicalLookupTable(
start_frequency, stop_frequency, frequency_step
)
LUT = MechanicalLookupTable(start_frequency, stop_frequency, frequency_step)
# Lock GUI
self.module.view.create_mech_LUT_spinner_dialog()
@ -1043,9 +1205,12 @@ class AutoTMController(ModuleController):
self.start_next_mechTM(LUT)
def start_next_mechTM(self, LUT) -> None:
"""Start the next mechanical tuning and matching sweep.
def start_next_mechTM(self, LUT):
"""Start the next mechanical tuning and matching sweep."""
Args:
LUT (MechanicalLookupTable): The lookup table.
"""
next_frequency = LUT.get_next_frequency()
LUT.started_frequency = next_frequency
logger.debug("Starting next mechanical tuning and matching:")
@ -1068,10 +1233,15 @@ class AutoTMController(ModuleController):
matching_last_direction = self.module.model.matching_stepper.last_direction
command = f"p{next_frequency}t{TUNING_RANGE},{TUNER_STEP_SIZE},{tuning_backlash},{tuning_last_direction}m{MATCHING_RANGE},{MATCHER_STEP_SIZE},{matching_backlash},{matching_last_direction}"
confirmation = self.send_command(command)
_ = self.send_command(command)
@pyqtSlot(str)
def process_position_sweep_result(self, text):
def process_position_sweep_result(self, text) -> None:
"""Process the result of the position sweep.
Args:
text (str): The text received from the serial connection.
"""
if text.startswith("z"):
text = text[1:]
# Format is z<tuning_position>,<tuning_last_direction>m<matching_position>,<matching_last_direction>
@ -1088,44 +1258,71 @@ class AutoTMController(ModuleController):
self.module.model.matching_stepper.position = matching_position
self.module.view.on_active_stepper_changed()
logger.debug("Tuning position: %s, Matching position: %s", tuning_position, matching_position)
logger.debug(
"Tuning position: %s, Matching position: %s",
tuning_position,
matching_position,
)
LUT = self.module.model.mech_lut
logger.debug("Received position sweep result: %s %s", matching_position, tuning_position)
logger.debug(
"Received position sweep result: %s %s",
matching_position,
tuning_position,
)
LUT.add_positions(tuning_position, matching_position)
self.continue_or_finish_position_sweep(LUT)
def continue_or_finish_position_sweep(self, LUT):
"""Continue or finish the position sweep."""
def continue_or_finish_position_sweep(self, LUT) -> None:
"""Continue or finish the position sweep.
Args:
LUT (MechanicalLookupTable): The lookup table.
"""
if LUT.is_incomplete():
self.start_next_mechTM(LUT)
else:
self.finish_position_sweep(LUT)
def finish_position_sweep(self, LUT):
"""Finish the position sweep."""
def finish_position_sweep(self, LUT) -> None:
"""Finish the position sweep.
Args:
LUT (MechanicalLookupTable): The lookup table.
"""
logger.debug("Finished position sweep")
self.module.model.mech_lut = LUT
self.module.model.LUT = LUT
self.module.view.mech_LUT_spinner.hide()
self.module.nqrduck_signal.emit("LUT_finished", LUT)
def go_to_position(self, tuning_position : int, matching_position : int) -> None:
def go_to_position(self, tuning_position: int, matching_position: int) -> None:
"""Go to the specified position.
Args:
position (SavedPosition): The position to go to.
tuning_position (int): The tuning position.
matching_position (int): The matching position.
"""
confirmation = self.on_absolute_move(tuning_position, self.module.model.tuning_stepper)
confirmation = self.on_absolute_move(
tuning_position, self.module.model.tuning_stepper
)
if confirmation:
confirmation = self.on_absolute_move(matching_position, self.module.model.matching_stepper)
confirmation = self.on_absolute_move(
matching_position, self.module.model.matching_stepper
)
if confirmation:
return True
# This method isn't used anymore but it might be useful in the future so I'll keep it here
def read_reflection(self, frequency) -> float:
"""Starts a reflection measurement and reads the reflection at the specified frequency."""
def read_reflection(self, frequency : float) -> float:
"""Starts a reflection measurement and reads the reflection at the specified frequency.
Args:
frequency (float): The frequency at which to read the reflection.
Returns:
float: The reflection at the specified frequency.
"""
# We send the command to the atm system
command = f"r{frequency}"
try:
@ -1143,8 +1340,13 @@ class AutoTMController(ModuleController):
while reflection is None:
# Check if the timeout has been reached
if time.time() - start_time > timeout_duration:
logger.error("Reading reflection timed out after %d seconds", timeout_duration)
self.module.view.add_error_text(f"Could not read reflection. Timed out after {timeout_duration} seconds")
logger.error(
"Reading reflection timed out after %d seconds",
timeout_duration,
)
self.module.view.add_error_text(
f"Could not read reflection. Timed out after {timeout_duration} seconds"
)
return None
# Refresh the reflection data
@ -1163,12 +1365,12 @@ class AutoTMController(ModuleController):
else:
logger.error("Could not read reflection. No confirmation received")
self.module.view.add_error_text("Could not read reflection. No confirmation received")
self.module.view.add_error_text(
"Could not read reflection. No confirmation received"
)
return None
except Exception as e:
logger.error("Could not read reflection. %s", e)
self.module.view.add_error_text(f"Could not read reflection. {e}")
return None

View file

@ -1,3 +1,10 @@
"""The module model for the NQRduck AutoTM module. It is used to store the data and state of the AutoTM module.
Additionally it includes the LookupTable class which is used to store tuning and matching voltages for different frequencies.
The S11Data class is used to store the S11 data that is read in via the serial connection.
"""
import cmath
import numpy as np
import logging
@ -10,6 +17,7 @@ logger = logging.getLogger(__name__)
class S11Data:
"""This class is used to store the S11 data that is read in via the serial connection."""
FILE_EXTENSION = "s11"
# Conversion factors - the data is generally sent and received in mV
# These values are used to convert the data to dB and degrees
@ -19,26 +27,32 @@ class S11Data:
PHASE_SLOPE = 10 # deg/mV
def __init__(self, data_points: list) -> None:
"""Initialize the S11 data."""
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):
"""The reflection data in millivolts. This is the raw data that is read in via the serial connection."""
return self.frequency, self.return_loss_mv, self.phase_mv
@property
def return_loss_db(self):
"""Returns the return loss in dB calculated from the return loss in mV."""
return (
self.return_loss_mv - self.CENTER_POINT_MAGNITUDE
) / self.MAGNITUDE_SLOPE
@property
def phase_deg(self, phase_correction=True):
"""Returns the absolute value of the phase in degrees
def phase_deg(self, phase_correction=True) -> np.array:
"""Returns the absolute value of the phase in degrees.
Keyword Arguments:
phase_correction {bool} -- If True, the phase correction is applied. (default: {False})
Args:
phase_correction (bool, optional): If True, the phase correction is applied. Defaults to True.
Returns:
np.array: The absolute value of the phase in degrees.
"""
phase_deg = (self.phase_mv - self.CENTER_POINT_PHASE) / self.PHASE_SLOPE
if phase_correction:
@ -48,11 +62,12 @@ class S11Data:
@property
def phase_rad(self):
"""Returns the phase in radians."""
return self.phase_deg * cmath.pi / 180
@property
def gamma(self):
"""Complex reflection coefficient"""
"""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")
@ -65,6 +80,7 @@ class S11Data:
self, frequency_data: np.array, phase_data: np.array
) -> np.array:
"""This method fixes the phase sign of the phase data.
The AD8302 can only measure the absolute value of the phase.
Therefore we need to correct the phase sign. This can be done via the slope of the phase.
If the slope is negative, the phase is positive and vice versa.
@ -145,6 +161,7 @@ class S11Data:
return phase_data_corrected
def to_json(self):
"""Convert the S11 data to a JSON serializable format."""
return {
"frequency": self.frequency.tolist(),
"return_loss_mv": self.return_loss_mv.tolist(),
@ -153,6 +170,7 @@ class S11Data:
@classmethod
def from_json(cls, json):
"""Create an S11Data object from a JSON serializable format."""
f = json["frequency"]
rl = json["return_loss_mv"]
p = json["phase_mv"]
@ -171,6 +189,7 @@ class LookupTable:
stop_frequency: float,
frequency_step: float,
) -> None:
"""Initialize the lookup table."""
self.start_frequency = start_frequency
self.stop_frequency = stop_frequency
self.frequency_step = frequency_step
@ -190,57 +209,78 @@ class LookupTable:
# Round to closest integer
return int(round((frequency - self.start_frequency) / self.frequency_step))
class Stepper:
class Stepper:
"""This class is used to store the state of a stepper motor."""
def __init__(self) -> None:
"""Initialize the stepper motor."""
self.homed = False
self.position = 0
class SavedPosition:
"""This class is used to store a saved position for tuning and matching of electrical probeheads."""
def __init__(self, frequency: float, tuning_position : int, matching_position : int) -> None:
def __init__(
self, frequency: float, tuning_position: int, matching_position: int
) -> None:
"""Initialize the saved position."""
self.frequency = frequency
self.tuning_position = tuning_position
self.matching_position = matching_position
def to_json(self):
"""Convert the saved position to a JSON serializable format."""
return {
"frequency": self.frequency,
"tuning_position": self.tuning_position,
"matching_position": self.matching_position,
}
class TuningStepper(Stepper):
"""This class is used to store the state of the tuning stepper motor."""
TYPE = "Tuning"
MAX_STEPS = 1e6
BACKLASH_STEPS = 60
def __init__(self) -> None:
"""Initialize the tuning stepper motor."""
super().__init__()
# Backlash stepper
self.last_direction = None
class MatchingStepper(Stepper):
"""This class is used to store the state of the matching stepper motor."""
TYPE = "Matching"
MAX_STEPS = 1e6
BACKLASH_STEPS = 0
def __init__(self) -> None:
"""Initialize the matching stepper motor."""
super().__init__()
self.last_direction = None
class ElectricalLookupTable(LookupTable):
"""This class is used to store a lookup table for tuning and matching of electrical probeheads."""
TYPE = "Electrical"
def __init__(self, start_frequency: float, stop_frequency: float, frequency_step: float) -> None:
def __init__(
self, start_frequency: float, stop_frequency: float, frequency_step: float
) -> None:
"""Initialize the lookup table."""
super().__init__(start_frequency, stop_frequency, frequency_step)
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.frequency_step
self.start_frequency,
self.stop_frequency + self.frequency_step,
self.frequency_step,
):
self.started_frequency = frequency
self.add_voltages(None, None)
@ -268,8 +308,9 @@ class ElectricalLookupTable(LookupTable):
return self.data[key]
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.
"""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.
@ -293,19 +334,25 @@ class ElectricalLookupTable(LookupTable):
return None
class MechanicalLookupTable(LookupTable):
"""This class is used to store a lookup table for tuning and matching of mechanical probeheads."""
# Hmm duplicate code
TYPE = "Mechanical"
def __init__(self, start_frequency: float, stop_frequency: float, frequency_step: float) -> None:
def __init__(
self, start_frequency: float, stop_frequency: float, frequency_step: float
) -> None:
"""Initialize the lookup table."""
super().__init__(start_frequency, stop_frequency, frequency_step)
self.init_positions()
def init_positions(self) -> None:
"""Initialize the lookup table with default values."""
for frequency in np.arange(
self.start_frequency, self.stop_frequency + self.frequency_step, self.frequency_step
self.start_frequency,
self.stop_frequency + self.frequency_step,
self.frequency_step,
):
self.started_frequency = frequency
self.add_positions(None, None)
@ -333,8 +380,9 @@ class MechanicalLookupTable(LookupTable):
return self.data[key]
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 position is none.
"""This method returns True if the lookup table is incomplete.
I.e. if there are frequencies for which no the tuning or matching position is none.
Returns:
bool: True if the lookup table is incomplete, False otherwise.
@ -357,8 +405,10 @@ class MechanicalLookupTable(LookupTable):
return frequency
return None
class AutoTMModel(ModuleModel):
class AutoTMModel(ModuleModel):
"""The module model for the NQRduck AutoTM module. It is used to store the data and state of the AutoTM module."""
available_devices_changed = pyqtSignal(list)
serial_changed = pyqtSignal(QSerialPort)
data_points_changed = pyqtSignal(list)
@ -372,6 +422,7 @@ class AutoTMModel(ModuleModel):
measurement_finished = pyqtSignal(S11Data)
def __init__(self, module) -> None:
"""Initialize the AutoTM model."""
super().__init__(module)
self.data_points = []
self.active_calibration = None
@ -398,6 +449,7 @@ class AutoTMModel(ModuleModel):
@property
def available_devices(self):
"""The available_devices property is used to store the available serial devices."""
return self._available_devices
@available_devices.setter
@ -419,6 +471,7 @@ class AutoTMModel(ModuleModel):
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.
"""
self.data_points.append((frequency, return_loss, phase))
@ -431,6 +484,7 @@ class AutoTMModel(ModuleModel):
@property
def saved_positions(self):
"""The saved_positions property is used to store the saved positions for tuning and matching of the probeheads."""
return self._saved_positions
@saved_positions.setter
@ -438,9 +492,13 @@ class AutoTMModel(ModuleModel):
self._saved_positions = value
self.saved_positions_changed.emit(value)
def add_saved_position(self, frequency: float, tuning_position: int, matching_position: int) -> None:
def add_saved_position(
self, frequency: float, tuning_position: int, matching_position: int
) -> None:
"""Add a saved position to the model."""
self.saved_positions.append(SavedPosition(frequency, tuning_position, matching_position))
self.saved_positions.append(
SavedPosition(frequency, tuning_position, matching_position)
)
self.saved_positions_changed.emit(self.saved_positions)
def delete_saved_position(self, position: SavedPosition) -> None:
@ -451,6 +509,7 @@ class AutoTMModel(ModuleModel):
@property
def measurement(self):
"""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
@ -463,6 +522,7 @@ class AutoTMModel(ModuleModel):
@property
def active_stepper(self):
"""The active_stepper property is used to store the active stepper motor."""
return self._active_stepper
@active_stepper.setter
@ -474,6 +534,7 @@ class AutoTMModel(ModuleModel):
@property
def active_calibration(self):
"""The active_calibration property is used to store the active calibration type."""
return self._active_calibration
@active_calibration.setter
@ -482,6 +543,7 @@ class AutoTMModel(ModuleModel):
@property
def short_calibration(self):
"""The short_calibration property is used to store the short calibration data."""
return self._short_calibration
@short_calibration.setter
@ -497,6 +559,7 @@ class AutoTMModel(ModuleModel):
@property
def open_calibration(self):
"""The open calibration data."""
return self._open_calibration
@open_calibration.setter
@ -512,6 +575,7 @@ class AutoTMModel(ModuleModel):
@property
def load_calibration(self):
"""The load calibration data."""
return self._load_calibration
@load_calibration.setter
@ -527,6 +591,7 @@ class AutoTMModel(ModuleModel):
@property
def calibration(self):
"""The calibration data."""
return self._calibration
@calibration.setter
@ -536,6 +601,7 @@ class AutoTMModel(ModuleModel):
@property
def LUT(self):
"""The lookup table for tuning and matching of the probeheads."""
return self._LUT
@LUT.setter

View file

@ -1,3 +1,5 @@
"""This module contains the view class for the AutoTM module."""
import logging
from datetime import datetime
import cmath
@ -27,7 +29,9 @@ logger = logging.getLogger(__name__)
class AutoTMView(ModuleView):
"""The view class for the AutoTM module."""
def __init__(self, module):
"""Initializes the AutoTM view."""
super().__init__(module)
widget = QWidget()
@ -127,11 +131,27 @@ class AutoTMView(ModuleView):
self._ui_form.startButton.setIconSize(self._ui_form.startButton.size())
# Stepper selection
self._ui_form.stepperselectBox.currentIndexChanged.connect(lambda: self.module.controller.on_stepper_changed(self._ui_form.stepperselectBox.currentText()))
self._ui_form.increaseButton.clicked.connect(lambda: self.module.controller.on_relative_move(self._ui_form.stepsizeBox.text()))
self._ui_form.decreaseButton.clicked.connect(lambda: self.module.controller.on_relative_move("-" + self._ui_form.stepsizeBox.text()))
self._ui_form.stepperselectBox.currentIndexChanged.connect(
lambda: self.module.controller.on_stepper_changed(
self._ui_form.stepperselectBox.currentText()
)
)
self._ui_form.increaseButton.clicked.connect(
lambda: self.module.controller.on_relative_move(
self._ui_form.stepsizeBox.text()
)
)
self._ui_form.decreaseButton.clicked.connect(
lambda: self.module.controller.on_relative_move(
"-" + self._ui_form.stepsizeBox.text()
)
)
self._ui_form.absoluteGoButton.clicked.connect(lambda: self.module.controller.on_absolute_move(self._ui_form.absoluteposBox.text()))
self._ui_form.absoluteGoButton.clicked.connect(
lambda: self.module.controller.on_absolute_move(
self._ui_form.absoluteposBox.text()
)
)
# Active stepper changed
self.module.model.active_stepper_changed.connect(self.on_active_stepper_changed)
@ -175,6 +195,7 @@ class AutoTMView(ModuleView):
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")
@ -183,7 +204,11 @@ 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.
Args:
available_devices (list): List of available devices.
"""
logger.debug("Updating available devices list")
self._ui_form.portBox.clear()
self._ui_form.portBox.addItems(available_devices)
@ -201,6 +226,7 @@ class AutoTMView(ModuleView):
@pyqtSlot()
def on_connect_button_clicked(self) -> None:
"""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")
@ -217,7 +243,7 @@ class AutoTMView(ModuleView):
logger.debug("Updating serial connection label")
if serial.isOpen():
self._ui_form.connectionLabel.setText(serial.portName())
self.add_info_text("Connected to device %s" % serial.portName())
self.add_info_text(f"Connected to device {serial.portName()}")
# Change the connectButton to a disconnectButton
self._ui_form.connectButton.setText("Disconnect")
else:
@ -231,7 +257,9 @@ class AutoTMView(ModuleView):
def on_active_stepper_changed(self) -> None:
"""Update the stepper position label according to the current stepper position."""
logger.debug("Updating stepper position label")
self._ui_form.stepperposLabel.setText(str(self.module.model.active_stepper.position))
self._ui_form.stepperposLabel.setText(
str(self.module.model.active_stepper.position)
)
logger.debug("Updated stepper position label")
# Only allow position change when stepper is homed
@ -253,6 +281,7 @@ class AutoTMView(ModuleView):
@pyqtSlot()
def on_position_button_clicked(self) -> None:
"""This method is called when the position button is clicked.
It opens the position window.
"""
logger.debug("Position button clicked")
@ -263,7 +292,7 @@ class AutoTMView(ModuleView):
"""Update the S11 plot with the current data points.
Args:
data_points (list): List of data points to plot.
data (S11Data): The current S11 data points.
@TODO: implement proper calibration. See the controller class for more information.
"""
@ -328,7 +357,7 @@ class AutoTMView(ModuleView):
"""
# Add a timestamp to the text
timestamp = datetime.now().strftime("%H:%M:%S")
text = "[%s] %s" % (timestamp, text)
text = f"[{timestamp}] {text}"
text_label = QLabel(text)
text_label.setStyleSheet("font-size: 25px;")
self._ui_form.scrollAreaWidgetContents.layout().addWidget(text_label)
@ -351,7 +380,7 @@ class AutoTMView(ModuleView):
)
# Add a timestamp to the text
timestamp = datetime.now().strftime("%H:%M:%S")
text = "[%s] %s" % (timestamp, text)
text = f"[{timestamp}] {text}"
text_label = QLabel(text)
text_label.setStyleSheet("font-size: 25px; color: red;")
@ -365,7 +394,9 @@ class AutoTMView(ModuleView):
def create_frequency_sweep_spinner_dialog(self) -> None:
"""Creates a frequency sweep spinner dialog."""
self.frequency_sweep_spinner = self.LoadingSpinner("Performing frequency sweep ...", self)
self.frequency_sweep_spinner = self.LoadingSpinner(
"Performing frequency sweep ...", self
)
self.frequency_sweep_spinner.show()
def create_el_LUT_spinner_dialog(self) -> None:
@ -375,7 +406,9 @@ class AutoTMView(ModuleView):
def create_mech_LUT_spinner_dialog(self) -> None:
"""Creates a mechanical LUT spinner dialog."""
self.mech_LUT_spinner = self.LoadingSpinner("Generating mechanical LUT ...", self)
self.mech_LUT_spinner = self.LoadingSpinner(
"Generating mechanical LUT ...", self
)
self.mech_LUT_spinner.show()
def view_el_lut(self) -> None:
@ -417,7 +450,9 @@ class AutoTMView(ModuleView):
self.module.controller.load_measurement(file_name)
class StepperSavedPositionsWindow(QDialog):
"""This class implements a window that shows the saved positions of the stepper."""
def __init__(self, module, parent=None):
"""Initializes the StepperSavedPositionsWindow."""
super().__init__(parent)
self.setParent(parent)
self.module = module
@ -432,7 +467,13 @@ class AutoTMView(ModuleView):
self.table_widget = QTableWidget()
self.table_widget.setColumnCount(5)
self.table_widget.setHorizontalHeaderLabels(
["Frequency (MHz)", "Tuning Position", "Matching Position", "Button", "Delete"]
[
"Frequency (MHz)",
"Tuning Position",
"Matching Position",
"Button",
"Delete",
]
)
self.table_widget.setColumnWidth(0, 150)
@ -461,8 +502,9 @@ class AutoTMView(ModuleView):
main_layout.addWidget(self.table_widget)
# On saved positions changed
self.module.model.saved_positions_changed.connect(self.on_saved_positions_changed)
self.module.model.saved_positions_changed.connect(
self.on_saved_positions_changed
)
self.setLayout(main_layout)
@ -482,13 +524,13 @@ class AutoTMView(ModuleView):
def on_load_position_button_clicked(self) -> None:
"""File picker for loading a position from a file."""
filename = self.file_selector("load")
logger.debug("Loading position from %s" % filename)
logger.debug(f"Loading position from {filename}")
self.module.controller.load_positions(filename)
def on_save_position_button_clicked(self) -> None:
"""File picker for saving a position to a file."""
filename = self.file_selector("save")
logger.debug("Saving position to %s" % filename)
logger.debug(f"Saving position to {filename}")
self.module.controller.save_positions(filename)
def on_new_position_button_clicked(self) -> None:
@ -497,9 +539,9 @@ class AutoTMView(ModuleView):
self.new_position_window = self.NewPositionWindow(self.module, self)
self.new_position_window.show()
def on_saved_positions_changed(self) -> None:
"""This method is called when the saved positions changed.
It updates the table widget.
"""
logger.debug("Updating saved positions table")
@ -508,7 +550,9 @@ class AutoTMView(ModuleView):
for row, position in enumerate(self.module.model.saved_positions):
self.table_widget.insertRow(row)
self.table_widget.setItem(row, 0, QTableWidgetItem(str(position.frequency)))
self.table_widget.setItem(
row, 0, QTableWidgetItem(str(position.frequency))
)
self.table_widget.setItem(
row, 1, QTableWidgetItem(position.tuning_position)
)
@ -517,7 +561,8 @@ class AutoTMView(ModuleView):
)
go_button = QPushButton("Go")
go_button.clicked.connect(
lambda _, position=position: self.module.controller.on_go_to_position(
lambda _,
position=position: self.module.controller.on_go_to_position(
position
)
)
@ -525,7 +570,8 @@ class AutoTMView(ModuleView):
delete_button = QPushButton("Delete")
delete_button.clicked.connect(
lambda _, position=position: self.module.controller.on_delete_position(
lambda _,
position=position: self.module.controller.on_delete_position(
position
)
)
@ -534,7 +580,9 @@ class AutoTMView(ModuleView):
logger.debug("Updated saved positions table")
class NewPositionWindow(QDialog):
"""This class implements a window for adding a new position."""
def __init__(self, module, parent=None):
"""Initializes the NewPositionWindow."""
super().__init__(parent)
self.setParent(parent)
self.module = module
@ -578,22 +626,32 @@ class AutoTMView(ModuleView):
data_layout = QVBoxLayout()
# Apply button
apply_button = QPushButton("Apply")
apply_button.clicked.connect(lambda: self.on_apply_button_clicked(frequency_edit.text(), tuning_edit.text(), matching_edit.text()))
apply_button.clicked.connect(
lambda: self.on_apply_button_clicked(
frequency_edit.text(), tuning_edit.text(), matching_edit.text()
)
)
data_layout.addWidget(apply_button)
main_layout.addLayout(data_layout)
self.setLayout(main_layout)
def on_apply_button_clicked(self, frequency: str, tuning_position: str, matching_position: str) -> None:
def on_apply_button_clicked(
self, frequency: str, tuning_position: str, matching_position: str
) -> None:
"""This method is called when the apply button is clicked."""
self.module.controller.add_position(frequency, tuning_position, matching_position)
self.module.controller.add_position(
frequency, tuning_position, matching_position
)
# Close the calibration window
self.close()
class LoadingSpinner(QDialog):
"""This class implements a spinner dialog that is shown during a frequency sweep."""
def __init__(self, text : str, parent=None):
def __init__(self, text: str, parent=None):
"""Initializes the LoadingSpinner."""
super().__init__(parent)
self.setWindowTitle("Loading")
self.setModal(True)
@ -611,7 +669,9 @@ class AutoTMView(ModuleView):
self.spinner_movie.start()
class LutWindow(QDialog):
"""This class implements a window that shows the LUT."""
def __init__(self, module, parent=None):
"""Initializes the LutWindow."""
super().__init__()
self.module = module
self.setParent(parent)
@ -659,7 +719,9 @@ class AutoTMView(ModuleView):
tuning_voltage = str(LUT.data[frequency][0])
matching_voltage = str(LUT.data[frequency][1])
test_button.clicked.connect(
lambda _, tuning_voltage=tuning_voltage, matching_voltage=matching_voltage: self.module.controller.set_voltages(
lambda _,
tuning_voltage=tuning_voltage,
matching_voltage=matching_voltage: self.module.controller.set_voltages(
tuning_voltage, matching_voltage
)
)
@ -668,7 +730,9 @@ class AutoTMView(ModuleView):
tuning_position = str(LUT.data[frequency][0])
matching_position = str(LUT.data[frequency][1])
test_button.clicked.connect(
lambda _, tuning_position=tuning_position, matching_position=matching_position: self.module.controller.go_to_position(
lambda _,
tuning_position=tuning_position,
matching_position=matching_position: self.module.controller.go_to_position(
tuning_position, matching_position
)
)
@ -681,6 +745,7 @@ class AutoTMView(ModuleView):
def test_lut(self):
"""This method is called when the Test LUT button is clicked. It sets all of the voltages from the lut with a small delay.
One can then view the matching on a seperate VNA.
"""
# This should be in the controller
@ -690,7 +755,9 @@ class AutoTMView(ModuleView):
self.module.controller.set_voltages(tuning_voltage, matching_voltage)
class CalibrationWindow(QDialog):
"""The calibration Dialog."""
def __init__(self, module, parent=None):
"""Initializes the CalibrationWindow."""
super().__init__(parent)
self.setParent(parent)
self.module = module
@ -791,18 +858,22 @@ class AutoTMView(ModuleView):
)
def on_short_calibration_finished(self, short_calibration: "S11Data") -> None:
"""This method is called when the short calibration has finished. It plots the calibration data on the short_plot widget."""
self.on_calibration_finished("short", self.short_plot, short_calibration)
def on_open_calibration_finished(self, open_calibration: "S11Data") -> None:
"""This method is called when the open calibration has finished. It plots the calibration data on the open_plot widget."""
self.on_calibration_finished("open", self.open_plot, open_calibration)
def on_load_calibration_finished(self, load_calibration: "S11Data") -> None:
"""This method is called when the load calibration has finished. It plots the calibration data on the load_plot widget."""
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.
It plots the calibration data on the given widget.
"""
frequency = data.frequency
@ -829,13 +900,14 @@ class AutoTMView(ModuleView):
widget.canvas.flush_events()
def on_export_button_clicked(self) -> None:
"""Called when the export button was clicked."""
filedialog = QFileDialog()
filedialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
filedialog.setNameFilter("calibration files (*.cal)")
filedialog.setDefaultSuffix("cal")
filedialog.exec()
filename = filedialog.selectedFiles()[0]
logger.debug("Exporting calibration to %s" % filename)
logger.debug(f"Exporting calibration to {filename}")
self.module.controller.export_calibration(filename)
def on_import_button_clicked(self) -> None:
@ -846,7 +918,7 @@ class AutoTMView(ModuleView):
filedialog.setDefaultSuffix("cal")
filedialog.exec()
filename = filedialog.selectedFiles()[0]
logger.debug("Importing calibration from %s" % filename)
logger.debug(f"Importing calibration from {filename}")
self.module.controller.import_calibration(filename)
def on_apply_button_clicked(self) -> None: