From 711a04f94521aad6b7e9f621a62f8afc66fa1517 Mon Sep 17 00:00:00 2001 From: jupfi Date: Wed, 9 Aug 2023 11:57:34 +0200 Subject: [PATCH] Changed to S11Data structure. --- src/nqrduck_autotm/controller.py | 21 +++++++-- src/nqrduck_autotm/model.py | 79 +++++++++++++++++++++++++++----- src/nqrduck_autotm/view.py | 56 ++++++++++++---------- 3 files changed, 117 insertions(+), 39 deletions(-) diff --git a/src/nqrduck_autotm/controller.py b/src/nqrduck_autotm/controller.py index dba1055..e313f6a 100644 --- a/src/nqrduck_autotm/controller.py +++ b/src/nqrduck_autotm/controller.py @@ -54,31 +54,42 @@ class AutoTMController(ModuleController): text = text.rstrip('\r\n') # logger.debug("Received data: %s", text) # If the text starts with 'f' and the frequency sweep spinner is visible we know that the data is a data point + # then we have the data for the return loss and the phase at a certain frequency if text.startswith("f") and self.module.view.frequency_sweep_spinner.isVisible(): 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) + # If the text starts with 'r' and no calibration is active we know that the data is a measurement elif text.startswith("r") and self.module.model.active_calibration == None: logger.debug("Measurement finished") - self.module.view.plot_data() + self.module.model.measurement = self.module.model.data_points.copy() self.module.view.frequency_sweep_spinner.hide() + # If the text starts with 'r' and a short calibration is active we know that the data is a short calibration elif text.startswith("r") and self.module.model.active_calibration == "short": logger.debug("Short calibration finished") self.module.model.short_calibration = self.module.model.data_points.copy() self.module.model.active_calibration = None self.module.view.frequency_sweep_spinner.hide() + # If the text starts with 'r' and an open calibration is active we know that the data is an open calibration elif text.startswith("r") and self.module.model.active_calibration == "open": logger.debug("Open calibration finished") self.module.model.open_calibration = self.module.model.data_points.copy() self.module.model.active_calibration = None self.module.view.frequency_sweep_spinner.hide() + # If the text starts with 'r' and a load calibration is active we know that the data is a load calibration elif text.startswith("r") and self.module.model.active_calibration == "load": logger.debug("Load calibration finished") self.module.model.load_calibration = self.module.model.data_points.copy() self.module.model.active_calibration = None self.module.view.frequency_sweep_spinner.hide() - else: + # If the text starts with 'i' we know that the data is an info message + elif text.startswith("i"): + text = "ATM Info: " + text[1:] + self.module.view.add_info_text(text) + # If the text starts with 'e' we know that the data is an error message + elif text.startswith("e"): + text = "ATM Error: " + text[1:] self.module.view.add_info_text(text) def on_short_calibration(self, start_frequency : float, stop_frequency : float) -> None: @@ -133,9 +144,9 @@ class AutoTMController(ModuleController): ideal_gamma_open = 1 ideal_gamma_load = 0 - short_calibration = [10 **(-returnloss_s[1] /6 / 24 / 20) for returnloss_s in self.module.model.short_calibration] - open_calibration = [10 **(-returnloss_o[1] / 6 / 24 / 20) for returnloss_o in self.module.model.open_calibration] - load_calibration = [10 **(-returnloss_l[1] / 6 / 24 / 20) for returnloss_l in self.module.model.load_calibration] + short_calibration = [10 **(-returnloss_s[1]) for returnloss_s in self.module.model.short_calibration] + open_calibration = [10 **(-returnloss_o[1]) for returnloss_o in self.module.model.open_calibration] + load_calibration = [10 **(-returnloss_l[1]) for returnloss_l in self.module.model.load_calibration] e_00s = [] e11s = [] diff --git a/src/nqrduck_autotm/model.py b/src/nqrduck_autotm/model.py index 98a4d4a..7eea343 100644 --- a/src/nqrduck_autotm/model.py +++ b/src/nqrduck_autotm/model.py @@ -1,4 +1,5 @@ -import serial +import cmath +import numpy as np import logging from PyQt6.QtCore import pyqtSignal from PyQt6.QtSerialPort import QSerialPort @@ -6,14 +7,51 @@ from nqrduck.module.module_model import ModuleModel logger = logging.getLogger(__name__) +class S11Data: + + # Conversion factors - the data is generally sent and received in mV + # These values are used to convert the data to dB and degrees + CENTER_POINT = 900 # mV + MAGNITUDE_SLOPE = 30 # dB/mV + PHASE_SLOPE = 10 # deg/mV + + def __init__(self, data_points : list) -> None: + self.frequency = np.array([data_point[0] for data_point in data_points]) + self.return_loss_mv = np.array([data_point[1] for data_point in data_points]) + self.phase_mv = np.array([data_point[2] for data_point in data_points]) + + + @property + def millivolts(self): + return self.frequency, self.return_loss_mv, self.phase_mv + + @property + def return_loss_db(self): + return (self.return_loss_mv - self.CENTER_POINT) / self.MAGNITUDE_SLOPE + + @property + def phase_deg(self): + return (self.phase_mv - self.CENTER_POINT) / self.PHASE_SLOPE + + @property + def phase_rad(self): + return self.phase_deg * cmath.pi / 180 + + @property + def gamma(self): + """Complex reflection coefficient""" + return cmath.rect(10 ** (-self.return_loss_db / 20), self.phase_rad) + class AutoTMModel(ModuleModel): + available_devices_changed = pyqtSignal(list) serial_changed = pyqtSignal(QSerialPort) data_points_changed = pyqtSignal(list) - short_calibration_finished = pyqtSignal(list) - open_calibration_finished = pyqtSignal(list) - load_calibration_finished = pyqtSignal(list) + short_calibration_finished = pyqtSignal(S11Data) + open_calibration_finished = pyqtSignal(S11Data) + load_calibration_finished = pyqtSignal(S11Data) + measurement_finished = pyqtSignal(S11Data) def __init__(self, module) -> None: super().__init__(module) @@ -31,6 +69,8 @@ class AutoTMModel(ModuleModel): @property def serial(self): + """The serial property is used to store the current serial connection. + """ return self._serial @serial.setter @@ -39,7 +79,9 @@ class AutoTMModel(ModuleModel): self.serial_changed.emit(value) def add_data_point(self, frequency: float, return_loss: float, phase : float) -> None: - """Add a data point to the model.""" + """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)) self.data_points_changed.emit(self.data_points) @@ -48,6 +90,20 @@ class AutoTMModel(ModuleModel): self.data_points.clear() self.data_points_changed.emit(self.data_points) + @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 + + @measurement.setter + def measurement(self, value): + """The measurement value is a tuple of three lists: frequency, return loss and phase.""" + self._measurement = S11Data(value) + self.measurement_finished.emit(self._measurement) + + # Calibration properties + @property def active_calibration(self): return self._active_calibration @@ -63,8 +119,8 @@ class AutoTMModel(ModuleModel): @short_calibration.setter def short_calibration(self, value): logger.debug("Setting short calibration") - self._short_calibration = value - self.short_calibration_finished.emit(value) + self._short_calibration = S11Data(value) + self.short_calibration_finished.emit(self._short_calibration) def init_short_calibration(self): """This method is called when a frequency sweep has been started for a short calibration in this way the module knows that the next data points are for a short calibration.""" @@ -78,8 +134,8 @@ class AutoTMModel(ModuleModel): @open_calibration.setter def open_calibration(self, value): logger.debug("Setting open calibration") - self._open_calibration = value - self.open_calibration_finished.emit(value) + self._open_calibration = S11Data(value) + self.open_calibration_finished.emit(self._open_calibration) def init_open_calibration(self): """This method is called when a frequency sweep has been started for an open calibration in this way the module knows that the next data points are for an open calibration.""" @@ -93,8 +149,8 @@ class AutoTMModel(ModuleModel): @load_calibration.setter def load_calibration(self, value): logger.debug("Setting load calibration") - self._load_calibration = value - self.load_calibration_finished.emit(value) + self._load_calibration = S11Data(value) + self.load_calibration_finished.emit(self._load_calibration) def init_load_calibration(self): """This method is called when a frequency sweep has been started for a load calibration in this way the module knows that the next data points are for a load calibration.""" @@ -109,3 +165,4 @@ class AutoTMModel(ModuleModel): def calibration(self, value): logger.debug("Setting calibration") self._calibration = value + \ No newline at end of file diff --git a/src/nqrduck_autotm/view.py b/src/nqrduck_autotm/view.py index 2b4443e..56c5d1e 100644 --- a/src/nqrduck_autotm/view.py +++ b/src/nqrduck_autotm/view.py @@ -45,10 +45,14 @@ class AutoTMView(ModuleView): # On clicking of the calibration button call the on_calibration_button_clicked method self._ui_form.calibrationButton.clicked.connect(self.on_calibration_button_clicked) + # Connect the measurement finished signal to the plot_measurement slot + self.module.model.measurement_finished.connect(self.plot_measurement) + # Add a vertical layout to the info box self._ui_form.scrollAreaWidgetContents.setLayout(QVBoxLayout()) self._ui_form.scrollAreaWidgetContents.layout().setAlignment(Qt.AlignmentFlag.AlignTop) + self.init_plot() self.init_labels() @@ -115,15 +119,15 @@ class AutoTMView(ModuleView): self._ui_form.connectionLabel.setText("Disconnected") logger.debug("Updated serial connection label") - def plot_data(self) -> None: + def plot_measurement(self, data : "S11Data") -> None: """Update the S11 plot with the current data points. Args: data_points (list): List of data points to plot. """ - x = [data_point[0] for data_point in self.module.model.data_points] - y = [(data_point[1] - 900) / 30 for data_point in self.module.model.data_points] - phase = [(data_point[2] - 900) / 10 for data_point in self.module.model.data_points] + frequency = data.frequency + return_loss_db = data.return_loss_db + phase = data.phase_deg # Calibration test: #calibration = self.module.model.calibration @@ -143,7 +147,7 @@ class AutoTMView(ModuleView): phase_ax = self._ui_form.S11Plot.canvas.ax.twinx() phase_ax.set_ylabel("Phase (deg)") - phase_ax.plot(x, phase, color="orange", linestyle="--") + phase_ax.plot(frequency, phase, color="orange", linestyle="--") phase_ax.set_ylim(-180, 180) phase_ax.invert_yaxis() @@ -153,7 +157,7 @@ class AutoTMView(ModuleView): magnitude_ax.set_ylabel("S11 (dB)") magnitude_ax.set_title("S11") magnitude_ax.grid(True) - magnitude_ax.plot(x, y) + magnitude_ax.plot(frequency, return_loss_db, color="blue") # make the y axis go down instead of up magnitude_ax.invert_yaxis() @@ -294,33 +298,39 @@ class AutoTMView(ModuleView): self.module.model.open_calibration_finished.connect(self.on_open_calibration_finished) self.module.model.load_calibration_finished.connect(self.on_load_calibration_finished) - def on_short_calibration_finished(self, short_calibration : list) -> None: + def on_short_calibration_finished(self, short_calibration : "S11Data") -> None: self.on_calibration_finished("short", self.short_plot, short_calibration) - def on_open_calibration_finished(self, open_calibration : list) -> None: + def on_open_calibration_finished(self, open_calibration : "S11Data") -> None: self.on_calibration_finished("open", self.open_plot, open_calibration) - def on_load_calibration_finished(self, load_calibration : list) -> None: + def on_load_calibration_finished(self, load_calibration : "S11Data") -> None: self.on_calibration_finished("load", self.load_plot, load_calibration) - def on_calibration_finished(self, type : str, widget: MplWidget, data :list) -> None: + 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. """ - x = [data_point[0] for data_point in data] - magnitude = [data_point[1] for data_point in data] - phase = [data_point[2] for data_point in data] - ax = widget.canvas.ax - ax.clear() - ax.set_xlabel("Frequency (MHz)") - ax.set_ylabel("S11 (dB)") - ax.set_title("S11") - ax.grid(True) - ax.plot(x, magnitude, label="Magnitude") - ax.plot(x, phase, label="Phase") - ax.legend() + frequency = data.frequency + return_loss_db = data.return_loss_db + phase = data.phase_deg + + phase_ax = widget.canvas.ax.twinx() + phase_ax.set_ylabel("Phase (deg)") + phase_ax.plot(frequency, phase, color="orange", linestyle="--") + phase_ax.set_ylim(-180, 180) + phase_ax.invert_yaxis() + + magnitude_ax = widget.canvas.ax + magnitude_ax.clear() + magnitude_ax.set_xlabel("Frequency (MHz)") + magnitude_ax.set_ylabel("S11 (dB)") + magnitude_ax.set_title("S11") + magnitude_ax.grid(True) + magnitude_ax.plot(frequency, return_loss_db, color="blue") # make the y axis go down instead of up - ax.invert_yaxis() + magnitude_ax.invert_yaxis() + widget.canvas.draw() widget.canvas.flush_events()