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 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 nqrduck.module.module import Module
from .model import AutoTMModel from .model import AutoTMModel
from .view import AutoTMView from .view import AutoTMView

View file

@ -1,3 +1,5 @@
"""The controller for the NQRduck AutoTM module."""
import logging import logging
import time import time
import numpy as np import numpy as np
@ -8,47 +10,75 @@ from PyQt6.QtCore import pyqtSlot
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from nqrduck.module.module_controller import ModuleController 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__) logger = logging.getLogger(__name__)
class AutoTMController(ModuleController): class AutoTMController(ModuleController):
"""The controller for the NQRduck AutoTM module. It handles the serial connection and the signals and slots."""
BAUDRATE = 115200 BAUDRATE = 115200
def on_loading(self): def on_loading(self) -> None:
"""This method is called when the module is loaded. """This method is called when the module is loaded.
It sets up the serial connection and connects the signals and slots. It sets up the serial connection and connects the signals and slots.
""" """
logger.debug("Setting up serial connection") logger.debug("Setting up serial connection")
self.find_devices() self.find_devices()
# Connect signals # 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_measurement_data)
self.module.model.serial_data_received.connect(self.process_calibration_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.print_info)
self.module.model.serial_data_received.connect(self.read_position_data) 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_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) self.module.model.serial_data_received.connect(self.process_signalpath_data)
@pyqtSlot(str, object) @pyqtSlot(str, object)
def process_signals(self, key: str, value: object) -> None: 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) logger.debug("Received signal: %s", key)
if key == "set_tune_and_match": if key == "set_tune_and_match":
self.tune_and_match(value) self.tune_and_match(value)
def tune_and_match(self, frequency: float) -> None: 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: if self.module.model.LUT is None:
logger.error("Could not tune and match. No LUT available.") logger.error("Could not tune and match. No LUT available.")
return return
elif self.module.model.LUT.TYPE == "Electrical": elif self.module.model.LUT.TYPE == "Electrical":
tuning_voltage, matching_voltage = self.module.model.LUT.get_voltages(frequency) tuning_voltage, matching_voltage = self.module.model.LUT.get_voltages(
confirmation = self.set_voltages(str(tuning_voltage), str(matching_voltage)) frequency
)
_ = self.set_voltages(str(tuning_voltage), str(matching_voltage))
# We need to change the signal pathway to preamp to measure the reflection # We need to change the signal pathway to preamp to measure the reflection
self.switch_to_atm() self.switch_to_atm()
reflection = self.read_reflection(frequency) reflection = self.read_reflection(frequency)
@ -57,7 +87,9 @@ class AutoTMController(ModuleController):
self.module.nqrduck_signal.emit("confirm_tune_and_match", reflection) self.module.nqrduck_signal.emit("confirm_tune_and_match", reflection)
elif self.module.model.LUT.TYPE == "Mechanical": 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.go_to_position(tuning_position, matching_position)
self.switch_to_atm() self.switch_to_atm()
# Switch to atm to measure the reflection # 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. # 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 # 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: 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."""
@ -127,6 +164,7 @@ class AutoTMController(ModuleController):
def start_frequency_sweep(self, start_frequency: str, stop_frequency: str) -> None: def start_frequency_sweep(self, start_frequency: str, stop_frequency: str) -> None:
"""This starts a frequency sweep on the device in the specified range. """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. The minimum start and stop frequency are specific to the AD4351 based frequency generator.
Args: Args:
@ -162,13 +200,8 @@ class AutoTMController(ModuleController):
return return
if start_frequency < MIN_FREQUENCY or stop_frequency > MAX_FREQUENCY: if start_frequency < MIN_FREQUENCY or stop_frequency > MAX_FREQUENCY:
error = ( error = f"Could not start frequency sweep. Start and stop frequency must be between {MIN_FREQUENCY / 1e6} and {MAX_FREQUENCY / 1e6} MHz"
"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) logger.error(error)
self.module.view.add_info_text(error) self.module.view.add_info_text(error)
return return
@ -182,7 +215,7 @@ class AutoTMController(ModuleController):
) )
# Print the command 'f<start>f<stop>f<step>' to the serial connection # 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() self.module.model.frequency_sweep_start = time.time()
confirmation = self.send_command(command) confirmation = self.send_command(command)
if confirmation: if confirmation:
@ -191,20 +224,31 @@ class AutoTMController(ModuleController):
self.module.view.create_frequency_sweep_spinner_dialog() self.module.view.create_frequency_sweep_spinner_dialog()
@pyqtSlot(str) @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. """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. 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") text = text[1:].split("r")
frequency = float(text[0]) frequency = float(text[0])
return_loss, phase = map(float, text[1].split("p")) return_loss, phase = map(float, text[1].split("p"))
self.module.model.add_data_point(frequency, return_loss, phase) self.module.model.add_data_point(frequency, return_loss, phase)
@pyqtSlot(str) @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. """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. 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"): if self.module.model.active_calibration is None and text.startswith("r"):
logger.debug("Measurement finished") logger.debug("Measurement finished")
@ -214,24 +258,33 @@ class AutoTMController(ModuleController):
self.finish_frequency_sweep() self.finish_frequency_sweep()
@pyqtSlot(str) @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. """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. It processes the data and adds it to the model.
Args: 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 calibration_type = self.module.model.active_calibration
logger.debug(f"{calibration_type.capitalize()} calibration finished") logger.debug(f"{calibration_type.capitalize()} calibration finished")
setattr(self.module.model, f"{calibration_type}_calibration", setattr(
S11Data(self.module.model.data_points.copy())) self.module.model,
f"{calibration_type}_calibration",
S11Data(self.module.model.data_points.copy()),
)
self.module.model.active_calibration = None self.module.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide() self.module.view.frequency_sweep_spinner.hide()
@pyqtSlot(str) @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. """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. It processes the data and adds it to the model.
Args: Args:
@ -243,25 +296,40 @@ class AutoTMController(ModuleController):
LUT = self.module.model.el_lut LUT = self.module.model.el_lut
if LUT is not None: if LUT is not None:
if LUT.is_incomplete(): 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) LUT.add_voltages(tuning_voltage, matching_voltage)
self.continue_or_finish_voltage_sweep(LUT) self.continue_or_finish_voltage_sweep(LUT)
self.module.model.tuning_voltage = tuning_voltage self.module.model.tuning_voltage = tuning_voltage
self.module.model.matching_voltage = matching_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. """This method is called when a frequency sweep is finished.
It hides the frequency sweep spinner dialog and adds the data to the model. It hides the frequency sweep spinner dialog and adds the data to the model.
""" """
self.module.view.frequency_sweep_spinner.hide() self.module.view.frequency_sweep_spinner.hide()
self.module.model.frequency_sweep_stop = time.time() self.module.model.frequency_sweep_stop = time.time()
duration = self.module.model.frequency_sweep_stop - self.module.model.frequency_sweep_start duration = (
self.module.view.add_info_text(f"Frequency sweep finished in {duration:.2f} seconds") 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. """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. It checks if the voltage sweep is finished or if the next voltage sweep should be started.
Args: Args:
@ -274,8 +342,9 @@ class AutoTMController(ModuleController):
# Finish voltage sweep # Finish voltage sweep
self.finish_voltage_sweep(LUT) 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. """This method is called when a voltage sweep is finished.
It starts the next voltage sweep. It starts the next voltage sweep.
Args: Args:
@ -286,16 +355,17 @@ class AutoTMController(ModuleController):
if self.module.view._ui_form.prevVoltagecheckBox.isChecked(): if self.module.view._ui_form.prevVoltagecheckBox.isChecked():
# Command format is s<frequency in MHz>o<optional tuning voltage>o<optional matching voltage> # Command format is s<frequency in MHz>o<optional tuning voltage>o<optional matching voltage>
# We use the currently set voltages # 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: else:
command = "s%s" % (next_frequency) command = f"s{next_frequency}"
LUT.started_frequency = next_frequency LUT.started_frequency = next_frequency
logger.debug("Starting next voltage sweep: %s", command) logger.debug("Starting next voltage sweep: %s", command)
self.send_command(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. """This method is called when a voltage sweep is finished.
It hides the voltage sweep spinner dialog and adds the data to the model. It hides the voltage sweep spinner dialog and adds the data to the model.
Args: Args:
@ -305,13 +375,18 @@ class AutoTMController(ModuleController):
self.module.view.el_LUT_spinner.hide() self.module.view.el_LUT_spinner.hide()
self.module.model.LUT = LUT self.module.model.LUT = LUT
self.module.model.voltage_sweep_stop = time.time() self.module.model.voltage_sweep_stop = time.time()
duration = self.module.model.voltage_sweep_stop - self.module.model.voltage_sweep_start duration = (
self.module.view.add_info_text(f"Voltage sweep finished in {duration:.2f} seconds") 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) self.module.nqrduck_signal.emit("LUT_finished", LUT)
@pyqtSlot(str) @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. """This method is called when data is received from the serial connection.
It prints the data to the info text box. It prints the data to the info text box.
Args: Args:
@ -325,8 +400,14 @@ class AutoTMController(ModuleController):
self.module.view.add_error_text(text) self.module.view.add_error_text(text)
@pyqtSlot(str) @pyqtSlot(str)
def read_position_data(self, text : str) -> None: def read_position_data(self, text: str) -> 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.
It processes the data and adds it to the model.
Args:
text (str): The data received from the serial connection.
"""
if text.startswith("p"): if text.startswith("p"):
# Format is p<tuning_position>m<matching_position> # Format is p<tuning_position>m<matching_position>
text = text[1:].split("m") text = text[1:].split("m")
@ -335,7 +416,11 @@ class AutoTMController(ModuleController):
self.module.model.matching_stepper.position = matching_position self.module.model.matching_stepper.position = matching_position
self.module.model.tuning_stepper.homed = True self.module.model.tuning_stepper.homed = True
self.module.model.matching_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() self.module.view.on_active_stepper_changed()
def on_ready_read(self) -> None: def on_ready_read(self) -> None:
@ -349,8 +434,9 @@ class AutoTMController(ModuleController):
self.module.model.serial_data_received.emit(text) self.module.model.serial_data_received.emit(text)
@pyqtSlot(str) @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. """This method is called when data is received from the serial connection.
It processes the data and adds it to the model. It processes the data and adds it to the model.
Args: Args:
@ -367,7 +453,12 @@ class AutoTMController(ModuleController):
self, start_frequency: float, stop_frequency: float self, start_frequency: float, stop_frequency: float
) -> None: ) -> None:
"""This method is called when the short calibration button is pressed. """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. 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") logger.debug("Starting short calibration")
self.module.model.init_short_calibration() self.module.model.init_short_calibration()
@ -377,7 +468,12 @@ class AutoTMController(ModuleController):
self, start_frequency: float, stop_frequency: float self, start_frequency: float, stop_frequency: float
) -> None: ) -> None:
"""This method is called when the open calibration button is pressed. """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. 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") logger.debug("Starting open calibration")
self.module.model.init_open_calibration() self.module.model.init_open_calibration()
@ -387,7 +483,12 @@ class AutoTMController(ModuleController):
self, start_frequency: float, stop_frequency: float self, start_frequency: float, stop_frequency: float
) -> None: ) -> None:
"""This method is called when the load calibration button is pressed. """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. 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") logger.debug("Starting load calibration")
self.module.model.init_load_calibration() self.module.model.init_load_calibration()
@ -395,6 +496,7 @@ class AutoTMController(ModuleController):
def calculate_calibration(self) -> None: def calculate_calibration(self) -> None:
"""This method is called when the calculate calibration button is pressed. """This method is called when the calculate calibration button is pressed.
It calculates the calibration from the short, open and calibration data points. It calculates the calibration from the short, open and calibration data points.
@TODO: Improvements to the calibrations can be made the following ways: @TODO: Improvements to the calibrations can be made the following ways:
@ -406,17 +508,17 @@ class AutoTMController(ModuleController):
""" """
logger.debug("Calculating calibration") logger.debug("Calculating calibration")
# First we check if the short and open calibration data points are available # 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( logger.error(
"Could not calculate calibration. No short calibration data points available." "Could not calculate calibration. No short calibration data points available."
) )
return return
if self.module.model.open_calibration == None: if self.module.model.open_calibration is None:
logger.error( logger.error(
"Could not calculate calibration. No open calibration data points available." "Could not calculate calibration. No open calibration data points available."
) )
return return
if self.module.model.load_calibration == None: if self.module.model.load_calibration is None:
logger.error( logger.error(
"Could not calculate calibration. No load calibration data points available." "Could not calculate calibration. No load calibration data points available."
) )
@ -459,6 +561,7 @@ class AutoTMController(ModuleController):
def export_calibration(self, filename: str) -> None: def export_calibration(self, filename: str) -> None:
"""This method is called when the export calibration button is pressed. """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. It exports the data of the short, open and load calibration to a file.
Args: Args:
@ -466,19 +569,19 @@ class AutoTMController(ModuleController):
""" """
logger.debug("Exporting calibration") logger.debug("Exporting calibration")
# First we check if the short and open calibration data points are available # 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( logger.error(
"Could not export calibration. No short calibration data points available." "Could not export calibration. No short calibration data points available."
) )
return return
if self.module.model.open_calibration == None: if self.module.model.open_calibration is None:
logger.error( logger.error(
"Could not export calibration. No open calibration data points available." "Could not export calibration. No open calibration data points available."
) )
return return
if self.module.model.load_calibration == None: if self.module.model.load_calibration is None:
logger.error( logger.error(
"Could not export calibration. No load calibration data points available." "Could not export calibration. No load calibration data points available."
) )
@ -496,6 +599,7 @@ class AutoTMController(ModuleController):
def import_calibration(self, filename: str) -> None: def import_calibration(self, filename: str) -> None:
"""This method is called when the import calibration button is pressed. """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. It imports the data of the short, open and load calibration from a file.
Args: Args:
@ -542,6 +646,7 @@ class AutoTMController(ModuleController):
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. """This method is called when the set voltages button is pressed.
It writes the specified tuning and matching voltage to the serial connection. It writes the specified tuning and matching voltage to the serial connection.
Args: Args:
@ -583,16 +688,22 @@ class AutoTMController(ModuleController):
matching_voltage, 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") logger.debug("Voltages already set")
return return
command = "v%sv%s" % (tuning_voltage, matching_voltage) command = f"v{tuning_voltage}v{matching_voltage}"
start_time = time.time() start_time = time.time()
confirmation = self.send_command(command) 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() QApplication.processEvents()
# Check for timeout # Check for timeout
if time.time() - start_time > timeout_duration: if time.time() - start_time > timeout_duration:
@ -614,6 +725,7 @@ class AutoTMController(ModuleController):
frequency_step: str, frequency_step: str,
) -> None: ) -> None:
"""This method is called when the generate LUT button is pressed. """This method is called when the generate LUT button is pressed.
It generates a lookup table for the specified frequency range and voltage resolution. It generates a lookup table for the specified frequency range and voltage resolution.
Args: Args:
@ -635,11 +747,7 @@ class AutoTMController(ModuleController):
self.module.view.add_info_text(error) self.module.view.add_info_text(error)
return return
if ( if start_frequency < 0 or stop_frequency < 0 or frequency_step < 0:
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" error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be positive"
logger.error(error) logger.error(error)
self.module.view.add_info_text(error) self.module.view.add_info_text(error)
@ -669,9 +777,7 @@ class AutoTMController(ModuleController):
# self.set_voltages("0", "0") # self.set_voltages("0", "0")
# We create the lookup table # We create the lookup table
LUT = ElectricalLookupTable( LUT = ElectricalLookupTable(start_frequency, stop_frequency, frequency_step)
start_frequency, stop_frequency, frequency_step
)
LUT.started_frequency = start_frequency LUT.started_frequency = start_frequency
@ -679,10 +785,14 @@ class AutoTMController(ModuleController):
if self.module.view._ui_form.prevVoltagecheckBox.isChecked(): if self.module.view._ui_form.prevVoltagecheckBox.isChecked():
# Command format is s<frequency in MHz>o<optional tuning voltage>o<optional matching voltage> # Command format is s<frequency in MHz>o<optional tuning voltage>o<optional matching voltage>
# We use the currently set voltages # 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) logger.debug(
command = "s%so%so%s" % (start_frequency, self.module.model.tuning_voltage, self.module.model.matching_voltage) "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: else:
command = "s%s" % (start_frequency) command = f"s{start_frequency}"
# For timing of the voltage sweep # For timing of the voltage sweep
self.module.model.voltage_sweep_start = time.time() self.module.model.voltage_sweep_start = time.time()
@ -694,6 +804,7 @@ class AutoTMController(ModuleController):
def switch_to_preamp(self) -> None: 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 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. 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": if self.module.model.signal_path == "preamp":
@ -714,6 +825,7 @@ class AutoTMController(ModuleController):
def switch_to_atm(self) -> None: 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. """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. In this state the atm system can be used to measure the reflection coefficient of the probecoils.
""" """
if self.module.model.signal_path == "atm": if self.module.model.signal_path == "atm":
@ -732,8 +844,9 @@ class AutoTMController(ModuleController):
logger.error("Switching to atm timed out") logger.error("Switching to atm timed out")
break 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. """This method is called when data is received from the serial connection.
It processes the data and adds it to the model. It processes the data and adds it to the model.
Args: Args:
@ -765,7 +878,7 @@ class AutoTMController(ModuleController):
) )
return False 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") logger.error("Could not send command. Serial connection is not open")
self.module.view.add_error_text( self.module.view.add_error_text(
"Could not send command. Serial connection is not open" "Could not send command. Serial connection is not open"
@ -796,12 +909,13 @@ class AutoTMController(ModuleController):
except Exception as e: except Exception as e:
logger.error("Could not send command. %s", 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 ### ### Stepper Motor Control ###
def homing(self) -> None: def homing(self) -> None:
"""This method is used to send the command 'h' to the atm system. """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. This command is used to home the stepper motors of the atm system.
""" """
logger.debug("Homing") logger.debug("Homing")
@ -812,6 +926,7 @@ class AutoTMController(ModuleController):
@pyqtSlot(str) @pyqtSlot(str)
def on_stepper_changed(self, stepper: str) -> None: def on_stepper_changed(self, stepper: str) -> None:
"""This method is called when the stepper position is changed. """This method is called when the stepper position is changed.
It sends the command to the atm system to change the stepper position. It sends the command to the atm system to change the stepper position.
Args: Args:
@ -824,25 +939,52 @@ class AutoTMController(ModuleController):
elif stepper == "matching": elif stepper == "matching":
self.module.model.active_stepper = self.module.model.matching_stepper self.module.model.active_stepper = self.module.model.matching_stepper
def validate_position(self, future_position: int, stepper : Stepper) -> bool: def validate_position(self, future_position: int, stepper: Stepper) -> bool:
"""Validate the stepper's future position.""" """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: 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 return False
if future_position > stepper.MAX_STEPS: 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 False
return True return True
def calculate_steps_for_absolute_move(self, target_position: int, stepper : Stepper) -> int: def calculate_steps_for_absolute_move(
"""Calculate the number of steps for an 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 current_position = stepper.position
return target_position - current_position return target_position - current_position
def send_stepper_command(self, steps: int, stepper : Stepper) -> None: def send_stepper_command(self, steps: int, stepper: Stepper) -> None:
"""Send a command to the stepper motor based on the number of steps.""" """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 # Here we handle backlash of the tuner
# Determine the direction of the current steps # Determine the direction of the current steps
backlash = 0 backlash = 0
@ -862,7 +1004,12 @@ class AutoTMController(ModuleController):
return confirmation return confirmation
def on_relative_move(self, steps: str, stepper: Stepper = None) -> None: 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 timeout_duration = 15 # timeout in seconds
start_time = time.time() start_time = time.time()
@ -876,7 +1023,9 @@ class AutoTMController(ModuleController):
return return
if self.validate_position(future_position, stepper): 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: while stepper_position == stepper.position:
QApplication.processEvents() QApplication.processEvents()
@ -888,7 +1037,12 @@ class AutoTMController(ModuleController):
return confirmation return confirmation
def on_absolute_move(self, steps: str, stepper: Stepper = None) -> None: 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 timeout_duration = 15 # timeout in seconds
start_time = time.time() start_time = time.time()
@ -903,7 +1057,9 @@ class AutoTMController(ModuleController):
return return
if self.validate_position(future_position, stepper): 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) confirmation = self.send_stepper_command(actual_steps, stepper)
while stepper_position == stepper.position: while stepper_position == stepper.position:
@ -917,7 +1073,7 @@ class AutoTMController(ModuleController):
### Position Saving and Loading ### ### 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. """Load the saved positions from a json file.
Args: Args:
@ -930,8 +1086,11 @@ class AutoTMController(ModuleController):
positions = json.load(f) positions = json.load(f)
for position in positions: for position in positions:
logger.debug("Loading position: %s", position) 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: def save_positions(self, path: str) -> None:
"""Save the current positions to a json file. """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_position = [position.to_json() for position in positions]
json.dump(json_position, f) 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. """Add a position to the lookup table.
Args: Args:
@ -953,7 +1114,9 @@ class AutoTMController(ModuleController):
matching_position (str): The matching position. matching_position (str): The matching position.
""" """
logger.debug("Adding new position at %s MHz", frequency) 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: def on_go_to_position(self, position: SavedPosition) -> None:
"""Go to the specified position. """Go to the specified position.
@ -962,9 +1125,13 @@ class AutoTMController(ModuleController):
position (SavedPosition): The position to go to. position (SavedPosition): The position to go to.
""" """
logger.debug("Going to position: %s", position) 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: 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: def on_delete_position(self, position: SavedPosition) -> None:
"""Delete the specified position. """Delete the specified position.
@ -975,10 +1142,11 @@ class AutoTMController(ModuleController):
logger.debug("Deleting position: %s", position) logger.debug("Deleting position: %s", position)
self.module.model.delete_saved_position(position) self.module.model.delete_saved_position(position)
#### Mechanical tuning and matching #### #### 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. """Generate a lookup table for the specified frequency range and voltage resolution.
Args: Args:
@ -999,11 +1167,7 @@ class AutoTMController(ModuleController):
self.module.view.add_info_text(error) self.module.view.add_info_text(error)
return return
if ( if start_frequency < 0 or stop_frequency < 0 or frequency_step < 0:
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" error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be positive"
logger.error(error) logger.error(error)
self.module.view.add_info_text(error) self.module.view.add_info_text(error)
@ -1032,9 +1196,7 @@ class AutoTMController(ModuleController):
self.switch_to_atm() self.switch_to_atm()
# We create the lookup table # We create the lookup table
LUT = MechanicalLookupTable( LUT = MechanicalLookupTable(start_frequency, stop_frequency, frequency_step)
start_frequency, stop_frequency, frequency_step
)
# Lock GUI # Lock GUI
self.module.view.create_mech_LUT_spinner_dialog() self.module.view.create_mech_LUT_spinner_dialog()
@ -1043,9 +1205,12 @@ class AutoTMController(ModuleController):
self.start_next_mechTM(LUT) 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): Args:
"""Start the next mechanical tuning and matching sweep.""" LUT (MechanicalLookupTable): The lookup table.
"""
next_frequency = LUT.get_next_frequency() next_frequency = LUT.get_next_frequency()
LUT.started_frequency = next_frequency LUT.started_frequency = next_frequency
logger.debug("Starting next mechanical tuning and matching:") 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 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}" 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) @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"): if text.startswith("z"):
text = text[1:] text = text[1:]
# Format is z<tuning_position>,<tuning_last_direction>m<matching_position>,<matching_last_direction> # 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.model.matching_stepper.position = matching_position
self.module.view.on_active_stepper_changed() 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 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) LUT.add_positions(tuning_position, matching_position)
self.continue_or_finish_position_sweep(LUT) self.continue_or_finish_position_sweep(LUT)
def continue_or_finish_position_sweep(self, LUT): def continue_or_finish_position_sweep(self, LUT) -> None:
"""Continue or finish the position sweep.""" """Continue or finish the position sweep.
Args:
LUT (MechanicalLookupTable): The lookup table.
"""
if LUT.is_incomplete(): if LUT.is_incomplete():
self.start_next_mechTM(LUT) self.start_next_mechTM(LUT)
else: else:
self.finish_position_sweep(LUT) self.finish_position_sweep(LUT)
def finish_position_sweep(self, LUT): def finish_position_sweep(self, LUT) -> None:
"""Finish the position sweep.""" """Finish the position sweep.
Args:
LUT (MechanicalLookupTable): The lookup table.
"""
logger.debug("Finished position sweep") logger.debug("Finished position sweep")
self.module.model.mech_lut = LUT self.module.model.mech_lut = LUT
self.module.model.LUT = LUT self.module.model.LUT = LUT
self.module.view.mech_LUT_spinner.hide() self.module.view.mech_LUT_spinner.hide()
self.module.nqrduck_signal.emit("LUT_finished", LUT) 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. """Go to the specified position.
Args: 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: 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: if confirmation:
return True return True
# This method isn't used anymore but it might be useful in the future so I'll keep it here # 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: def read_reflection(self, frequency : float) -> float:
"""Starts a reflection measurement and reads the reflection at the specified frequency.""" """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 # We send the command to the atm system
command = f"r{frequency}" command = f"r{frequency}"
try: try:
@ -1143,8 +1340,13 @@ class AutoTMController(ModuleController):
while reflection is None: while reflection is None:
# Check if the timeout has been reached # Check if the timeout has been reached
if time.time() - start_time > timeout_duration: if time.time() - start_time > timeout_duration:
logger.error("Reading reflection timed out after %d seconds", timeout_duration) logger.error(
self.module.view.add_error_text(f"Could not read reflection. Timed out after {timeout_duration} seconds") "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 return None
# Refresh the reflection data # Refresh the reflection data
@ -1163,12 +1365,12 @@ class AutoTMController(ModuleController):
else: else:
logger.error("Could not read reflection. No confirmation received") 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 return None
except Exception as e: except Exception as e:
logger.error("Could not read reflection. %s", e) logger.error("Could not read reflection. %s", e)
self.module.view.add_error_text(f"Could not read reflection. {e}") self.module.view.add_error_text(f"Could not read reflection. {e}")
return None 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 cmath
import numpy as np import numpy as np
import logging import logging
@ -10,6 +17,7 @@ logger = logging.getLogger(__name__)
class S11Data: class S11Data:
"""This class is used to store the S11 data that is read in via the serial connection."""
FILE_EXTENSION = "s11" FILE_EXTENSION = "s11"
# Conversion factors - the data is generally sent and received in mV # Conversion factors - the data is generally sent and received in mV
# These values are used to convert the data to dB and degrees # These values are used to convert the data to dB and degrees
@ -19,26 +27,32 @@ class S11Data:
PHASE_SLOPE = 10 # deg/mV PHASE_SLOPE = 10 # deg/mV
def __init__(self, data_points: list) -> None: 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.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.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]) self.phase_mv = np.array([data_point[2] for data_point in data_points])
@property @property
def millivolts(self): 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 return self.frequency, self.return_loss_mv, self.phase_mv
@property @property
def return_loss_db(self): def return_loss_db(self):
"""Returns the return loss in dB calculated from the return loss in mV."""
return ( return (
self.return_loss_mv - self.CENTER_POINT_MAGNITUDE self.return_loss_mv - self.CENTER_POINT_MAGNITUDE
) / self.MAGNITUDE_SLOPE ) / self.MAGNITUDE_SLOPE
@property @property
def phase_deg(self, phase_correction=True): def phase_deg(self, phase_correction=True) -> np.array:
"""Returns the absolute value of the phase in degrees """Returns the absolute value of the phase in degrees.
Keyword Arguments: Args:
phase_correction {bool} -- If True, the phase correction is applied. (default: {False}) 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 phase_deg = (self.phase_mv - self.CENTER_POINT_PHASE) / self.PHASE_SLOPE
if phase_correction: if phase_correction:
@ -48,11 +62,12 @@ class S11Data:
@property @property
def phase_rad(self): def phase_rad(self):
"""Returns the phase in radians."""
return self.phase_deg * cmath.pi / 180 return self.phase_deg * cmath.pi / 180
@property @property
def gamma(self): def gamma(self):
"""Complex reflection coefficient""" """Complex reflection coefficient."""
if len(self.return_loss_db) != len(self.phase_rad): if len(self.return_loss_db) != len(self.phase_rad):
raise ValueError("return_loss_db and phase_rad must be the same length") 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 self, frequency_data: np.array, phase_data: np.array
) -> np.array: ) -> np.array:
"""This method fixes the phase sign of the phase data. """This method fixes the phase sign of the phase data.
The AD8302 can only measure the absolute value of the phase. 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. 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. If the slope is negative, the phase is positive and vice versa.
@ -145,6 +161,7 @@ class S11Data:
return phase_data_corrected return phase_data_corrected
def to_json(self): def to_json(self):
"""Convert the S11 data to a JSON serializable format."""
return { return {
"frequency": self.frequency.tolist(), "frequency": self.frequency.tolist(),
"return_loss_mv": self.return_loss_mv.tolist(), "return_loss_mv": self.return_loss_mv.tolist(),
@ -153,6 +170,7 @@ class S11Data:
@classmethod @classmethod
def from_json(cls, json): def from_json(cls, json):
"""Create an S11Data object from a JSON serializable format."""
f = json["frequency"] f = json["frequency"]
rl = json["return_loss_mv"] rl = json["return_loss_mv"]
p = json["phase_mv"] p = json["phase_mv"]
@ -171,6 +189,7 @@ class LookupTable:
stop_frequency: float, stop_frequency: float,
frequency_step: float, frequency_step: float,
) -> None: ) -> None:
"""Initialize the lookup table."""
self.start_frequency = start_frequency self.start_frequency = start_frequency
self.stop_frequency = stop_frequency self.stop_frequency = stop_frequency
self.frequency_step = frequency_step self.frequency_step = frequency_step
@ -190,57 +209,78 @@ class LookupTable:
# Round to closest integer # Round to closest integer
return int(round((frequency - self.start_frequency) / self.frequency_step)) 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: def __init__(self) -> None:
"""Initialize the stepper motor."""
self.homed = False self.homed = False
self.position = 0 self.position = 0
class SavedPosition: class SavedPosition:
"""This class is used to store a saved position for tuning and matching of electrical probeheads.""" """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.frequency = frequency
self.tuning_position = tuning_position self.tuning_position = tuning_position
self.matching_position = matching_position self.matching_position = matching_position
def to_json(self): def to_json(self):
"""Convert the saved position to a JSON serializable format."""
return { return {
"frequency": self.frequency, "frequency": self.frequency,
"tuning_position": self.tuning_position, "tuning_position": self.tuning_position,
"matching_position": self.matching_position, "matching_position": self.matching_position,
} }
class TuningStepper(Stepper): class TuningStepper(Stepper):
"""This class is used to store the state of the tuning stepper motor."""
TYPE = "Tuning" TYPE = "Tuning"
MAX_STEPS = 1e6 MAX_STEPS = 1e6
BACKLASH_STEPS = 60 BACKLASH_STEPS = 60
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the tuning stepper motor."""
super().__init__() super().__init__()
# Backlash stepper # Backlash stepper
self.last_direction = None self.last_direction = None
class MatchingStepper(Stepper): class MatchingStepper(Stepper):
"""This class is used to store the state of the matching stepper motor."""
TYPE = "Matching" TYPE = "Matching"
MAX_STEPS = 1e6 MAX_STEPS = 1e6
BACKLASH_STEPS = 0 BACKLASH_STEPS = 0
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the matching stepper motor."""
super().__init__() super().__init__()
self.last_direction = None self.last_direction = None
class ElectricalLookupTable(LookupTable): class ElectricalLookupTable(LookupTable):
"""This class is used to store a lookup table for tuning and matching of electrical probeheads."""
TYPE = "Electrical" 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) super().__init__(start_frequency, stop_frequency, frequency_step)
self.init_voltages() self.init_voltages()
def init_voltages(self) -> None: def init_voltages(self) -> None:
"""Initialize the lookup table with default values.""" """Initialize the lookup table with default values."""
for frequency in np.arange( 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.started_frequency = frequency
self.add_voltages(None, None) self.add_voltages(None, None)
@ -268,8 +308,9 @@ class ElectricalLookupTable(LookupTable):
return self.data[key] return self.data[key]
def is_incomplete(self) -> bool: def is_incomplete(self) -> bool:
"""This method returns True if the lookup table is incomplete, """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.
I.e. if there are frequencies for which no the tuning or matching voltage is none.
Returns: Returns:
bool: True if the lookup table is incomplete, False otherwise. bool: True if the lookup table is incomplete, False otherwise.
@ -293,19 +334,25 @@ class ElectricalLookupTable(LookupTable):
return None return None
class MechanicalLookupTable(LookupTable): class MechanicalLookupTable(LookupTable):
"""This class is used to store a lookup table for tuning and matching of mechanical probeheads."""
# Hmm duplicate code # Hmm duplicate code
TYPE = "Mechanical" TYPE = "Mechanical"
def __init__(
def __init__(self, start_frequency: float, stop_frequency: float, frequency_step: float) -> None: self, start_frequency: float, stop_frequency: float, frequency_step: float
) -> None:
"""Initialize the lookup table."""
super().__init__(start_frequency, stop_frequency, frequency_step) super().__init__(start_frequency, stop_frequency, frequency_step)
self.init_positions() self.init_positions()
def init_positions(self) -> None: def init_positions(self) -> None:
"""Initialize the lookup table with default values.""" """Initialize the lookup table with default values."""
for frequency in np.arange( 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.started_frequency = frequency
self.add_positions(None, None) self.add_positions(None, None)
@ -333,8 +380,9 @@ class MechanicalLookupTable(LookupTable):
return self.data[key] return self.data[key]
def is_incomplete(self) -> bool: def is_incomplete(self) -> bool:
"""This method returns True if the lookup table is incomplete, """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.
I.e. if there are frequencies for which no the tuning or matching position is none.
Returns: Returns:
bool: True if the lookup table is incomplete, False otherwise. bool: True if the lookup table is incomplete, False otherwise.
@ -357,8 +405,10 @@ class MechanicalLookupTable(LookupTable):
return frequency return frequency
return None 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) available_devices_changed = pyqtSignal(list)
serial_changed = pyqtSignal(QSerialPort) serial_changed = pyqtSignal(QSerialPort)
data_points_changed = pyqtSignal(list) data_points_changed = pyqtSignal(list)
@ -372,6 +422,7 @@ class AutoTMModel(ModuleModel):
measurement_finished = pyqtSignal(S11Data) measurement_finished = pyqtSignal(S11Data)
def __init__(self, module) -> None: def __init__(self, module) -> None:
"""Initialize the AutoTM model."""
super().__init__(module) super().__init__(module)
self.data_points = [] self.data_points = []
self.active_calibration = None self.active_calibration = None
@ -398,6 +449,7 @@ class AutoTMModel(ModuleModel):
@property @property
def available_devices(self): def available_devices(self):
"""The available_devices property is used to store the available serial devices."""
return self._available_devices return self._available_devices
@available_devices.setter @available_devices.setter
@ -419,6 +471,7 @@ class AutoTMModel(ModuleModel):
self, frequency: float, return_loss: float, phase: float self, frequency: float, return_loss: float, phase: float
) -> None: ) -> None:
"""Add a data point to the model. These data points are our intermediate data points read in via the serial connection. """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. They will be saved in the according properties later on.
""" """
self.data_points.append((frequency, return_loss, phase)) self.data_points.append((frequency, return_loss, phase))
@ -431,6 +484,7 @@ class AutoTMModel(ModuleModel):
@property @property
def saved_positions(self): 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 return self._saved_positions
@saved_positions.setter @saved_positions.setter
@ -438,9 +492,13 @@ class AutoTMModel(ModuleModel):
self._saved_positions = value self._saved_positions = value
self.saved_positions_changed.emit(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.""" """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) self.saved_positions_changed.emit(self.saved_positions)
def delete_saved_position(self, position: SavedPosition) -> None: def delete_saved_position(self, position: SavedPosition) -> None:
@ -451,6 +509,7 @@ class AutoTMModel(ModuleModel):
@property @property
def measurement(self): def measurement(self):
"""The measurement property is used to store the current measurement. """The measurement property is used to store the current measurement.
This is the measurement that is shown in the main S11 plot This is the measurement that is shown in the main S11 plot
""" """
return self._measurement return self._measurement
@ -463,6 +522,7 @@ class AutoTMModel(ModuleModel):
@property @property
def active_stepper(self): def active_stepper(self):
"""The active_stepper property is used to store the active stepper motor."""
return self._active_stepper return self._active_stepper
@active_stepper.setter @active_stepper.setter
@ -474,6 +534,7 @@ class AutoTMModel(ModuleModel):
@property @property
def active_calibration(self): def active_calibration(self):
"""The active_calibration property is used to store the active calibration type."""
return self._active_calibration return self._active_calibration
@active_calibration.setter @active_calibration.setter
@ -482,6 +543,7 @@ class AutoTMModel(ModuleModel):
@property @property
def short_calibration(self): def short_calibration(self):
"""The short_calibration property is used to store the short calibration data."""
return self._short_calibration return self._short_calibration
@short_calibration.setter @short_calibration.setter
@ -497,6 +559,7 @@ class AutoTMModel(ModuleModel):
@property @property
def open_calibration(self): def open_calibration(self):
"""The open calibration data."""
return self._open_calibration return self._open_calibration
@open_calibration.setter @open_calibration.setter
@ -512,6 +575,7 @@ class AutoTMModel(ModuleModel):
@property @property
def load_calibration(self): def load_calibration(self):
"""The load calibration data."""
return self._load_calibration return self._load_calibration
@load_calibration.setter @load_calibration.setter
@ -527,6 +591,7 @@ class AutoTMModel(ModuleModel):
@property @property
def calibration(self): def calibration(self):
"""The calibration data."""
return self._calibration return self._calibration
@calibration.setter @calibration.setter
@ -536,6 +601,7 @@ class AutoTMModel(ModuleModel):
@property @property
def LUT(self): def LUT(self):
"""The lookup table for tuning and matching of the probeheads."""
return self._LUT return self._LUT
@LUT.setter @LUT.setter

View file

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