diff --git a/src/nqrduck_autotm/controller.py b/src/nqrduck_autotm/controller.py index e313f6a..592f5d5 100644 --- a/src/nqrduck_autotm/controller.py +++ b/src/nqrduck_autotm/controller.py @@ -1,9 +1,11 @@ import logging import numpy as np +import json from serial.tools.list_ports import comports from PyQt6 import QtSerialPort from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot from nqrduck.module.module_controller import ModuleController +from .model import S11Data logger = logging.getLogger(__name__) @@ -63,24 +65,24 @@ class AutoTMController(ModuleController): # If the text starts with 'r' and no calibration is active we know that the data is a measurement elif text.startswith("r") and self.module.model.active_calibration == None: logger.debug("Measurement finished") - self.module.model.measurement = self.module.model.data_points.copy() + self.module.model.measurement = S11Data(self.module.model.data_points.copy()) self.module.view.frequency_sweep_spinner.hide() # If the text starts with 'r' and a short calibration is active we know that the data is a short calibration elif text.startswith("r") and self.module.model.active_calibration == "short": logger.debug("Short calibration finished") - self.module.model.short_calibration = self.module.model.data_points.copy() + self.module.model.short_calibration = S11Data(self.module.model.data_points.copy()) self.module.model.active_calibration = None self.module.view.frequency_sweep_spinner.hide() # If the text starts with 'r' and an open calibration is active we know that the data is an open calibration elif text.startswith("r") and self.module.model.active_calibration == "open": logger.debug("Open calibration finished") - self.module.model.open_calibration = self.module.model.data_points.copy() + self.module.model.open_calibration = S11Data(self.module.model.data_points.copy()) self.module.model.active_calibration = None self.module.view.frequency_sweep_spinner.hide() # If the text starts with 'r' and a load calibration is active we know that the data is a load calibration elif text.startswith("r") and self.module.model.active_calibration == "load": logger.debug("Load calibration finished") - self.module.model.load_calibration = self.module.model.data_points.copy() + self.module.model.load_calibration = S11Data(self.module.model.data_points.copy()) self.module.model.active_calibration = None self.module.view.frequency_sweep_spinner.hide() # If the text starts with 'i' we know that the data is an info message @@ -123,20 +125,13 @@ class AutoTMController(ModuleController): logger.debug("Calculating calibration") # First we check if the short and open calibration data points are available if self.module.model.short_calibration == None: - logger.error("No short calibration data points available") + logger.error("Could not calculate calibration. No short calibration data points available.") return - if self.module.model.open_calibration == None: - logger.error("No open calibration data points available") + logger.error("Could not calculate calibration. No open calibration data points available.") return - if self.module.model.load_calibration == None: - logger.error("No load calibration data points available") - return - - # Then we check if the short, open and load calibration data points have the same length - if len(self.module.model.short_calibration) != len(self.module.model.open_calibration) or len(self.module.model.short_calibration) != len(self.module.model.load_calibration): - logger.error("The short, open and load calibration data points do not have the same length") + logger.error("Could not calculate calibration. No load calibration data points available.") return # Then we calculate the calibration @@ -144,14 +139,14 @@ class AutoTMController(ModuleController): ideal_gamma_open = 1 ideal_gamma_load = 0 - 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] + measured_gamma_short = self.module.model.short_calibration.gamma + measured_gamma_open = self.module.model.open_calibration.gamma + measured_gamma_load = self.module.model.load_calibration.gamma e_00s = [] e11s = [] delta_es = [] - for gamma_s, gamma_o, gamma_l in zip(short_calibration, open_calibration, load_calibration): + for gamma_s, gamma_o, gamma_l in zip(measured_gamma_short, measured_gamma_open, measured_gamma_load): A = np.array([ [1, ideal_gamma_short * gamma_s, -ideal_gamma_short], [1, ideal_gamma_open * gamma_o, -ideal_gamma_open], @@ -167,4 +162,51 @@ class AutoTMController(ModuleController): e11s.append(e11) delta_es.append(delta_e) - self.module.model.calibration = (e_00s, e11s, delta_es) \ No newline at end of file + self.module.model.calibration = (e_00s, e11s, delta_es) + + def export_calibration(self, filename: str) -> None: + """This method is called when the export calibration button is pressed. + It exports the data of the short, open and load calibration to a file. + + Args: + filename (str): The filename of the file to export to. + """ + logger.debug("Exporting calibration") + # First we check if the short and open calibration data points are available + if self.module.model.short_calibration == None: + logger.error("Could not export calibration. No short calibration data points available.") + return + + if self.module.model.open_calibration == None: + logger.error("Could not export calibration. No open calibration data points available.") + return + + if self.module.model.load_calibration == None: + logger.error("Could not export calibration. No load calibration data points available.") + return + + # Then we export the different calibrations as a json file + data = { + "short": self.module.model.short_calibration.to_json(), + "open": self.module.model.open_calibration.to_json(), + "load": self.module.model.load_calibration.to_json() + } + + with open(filename, "w") as f: + json.dump(data, f) + + def import_calibration(self, filename: str) -> None: + """This method is called when the import calibration button is pressed. + It imports the data of the short, open and load calibration from a file. + + Args: + filename (str): The filename of the file to import from. + """ + logger.debug("Importing calibration") + + # We import the different calibrations from a json file + with open(filename, "r") as f: + data = json.load(f) + self.module.model.short_calibration = S11Data.from_json(data["short"]) + self.module.model.open_calibration = S11Data.from_json(data["open"]) + self.module.model.load_calibration = S11Data.from_json(data["load"]) \ No newline at end of file diff --git a/src/nqrduck_autotm/model.py b/src/nqrduck_autotm/model.py index 7eea343..355c079 100644 --- a/src/nqrduck_autotm/model.py +++ b/src/nqrduck_autotm/model.py @@ -40,7 +40,22 @@ class S11Data: @property def gamma(self): """Complex reflection coefficient""" - return cmath.rect(10 ** (-self.return_loss_db / 20), self.phase_rad) + return map(cmath.rect, (10 ** (-self.return_loss_db / 20), self.phase_rad)) + + def to_json(self): + return { + "frequency": self.frequency.tolist(), + "return_loss_mv": self.return_loss_mv.tolist(), + "phase_mv": self.phase_mv.tolist() + } + + @classmethod + def from_json(cls, json): + f = json["frequency"] + rl = json["return_loss_mv"] + p = json["phase_mv"] + data = [(f[i], rl[i], p[i]) for i in range(len(f))] + return cls(data) class AutoTMModel(ModuleModel): @@ -99,8 +114,8 @@ class AutoTMModel(ModuleModel): @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) + self._measurement = value + self.measurement_finished.emit(value) # Calibration properties @@ -119,8 +134,8 @@ class AutoTMModel(ModuleModel): @short_calibration.setter def short_calibration(self, value): logger.debug("Setting short calibration") - self._short_calibration = S11Data(value) - self.short_calibration_finished.emit(self._short_calibration) + self._short_calibration = value + self.short_calibration_finished.emit(value) 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.""" @@ -134,8 +149,8 @@ class AutoTMModel(ModuleModel): @open_calibration.setter def open_calibration(self, value): logger.debug("Setting open calibration") - self._open_calibration = S11Data(value) - self.open_calibration_finished.emit(self._open_calibration) + self._open_calibration = value + self.open_calibration_finished.emit(value) 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.""" @@ -149,8 +164,8 @@ class AutoTMModel(ModuleModel): @load_calibration.setter def load_calibration(self, value): logger.debug("Setting load calibration") - self._load_calibration = S11Data(value) - self.load_calibration_finished.emit(self._load_calibration) + self._load_calibration = value + self.load_calibration_finished.emit(value) 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.""" diff --git a/src/nqrduck_autotm/view.py b/src/nqrduck_autotm/view.py index 56c5d1e..bb68374 100644 --- a/src/nqrduck_autotm/view.py +++ b/src/nqrduck_autotm/view.py @@ -1,9 +1,11 @@ import logging from datetime import datetime from pathlib import Path +import smithplot +from smithplot import SmithAxes from PyQt6.QtGui import QMovie from PyQt6.QtSerialPort import QSerialPort -from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QApplication, QHBoxLayout, QLineEdit, QPushButton, QDialog +from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QApplication, QHBoxLayout, QLineEdit, QPushButton, QDialog, QFileDialog from PyQt6.QtCore import pyqtSlot, Qt from nqrduck.module.module_view import ModuleView from nqrduck.contrib.mplwidget import MplWidget @@ -129,6 +131,7 @@ class AutoTMView(ModuleView): return_loss_db = data.return_loss_db phase = data.phase_deg + gamma = data.gamma # Calibration test: #calibration = self.module.model.calibration #e_00 = calibration[0] @@ -335,12 +338,25 @@ class AutoTMView(ModuleView): widget.canvas.flush_events() def on_export_button_clicked(self) -> None: - """This method is called when the export button is clicked. """ - pass + filedialog = QFileDialog() + filedialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + filedialog.setNameFilter("calibration files (*.cal)") + filedialog.setDefaultSuffix("cal") + filedialog.exec() + filename = filedialog.selectedFiles()[0] + logger.debug("Exporting calibration to %s" % filename) + self.module.controller.export_calibration(filename) def on_import_button_clicked(self) -> None: """This method is called when the import button is clicked. """ - pass + filedialog = QFileDialog() + filedialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) + filedialog.setNameFilter("calibration files (*.cal)") + filedialog.setDefaultSuffix("cal") + filedialog.exec() + filename = filedialog.selectedFiles()[0] + logger.debug("Importing calibration from %s" % filename) + self.module.controller.import_calibration(filename) def on_apply_button_clicked(self) -> None: """This method is called when the apply button is clicked. """