Added exporting and importing of calibration.

This commit is contained in:
jupfi 2023-08-09 16:32:13 +02:00
parent 711a04f945
commit ae3a42f869
3 changed files with 105 additions and 32 deletions

View file

@ -1,9 +1,11 @@
import logging import logging
import numpy as np import numpy as np
import json
from serial.tools.list_ports import comports from serial.tools.list_ports import comports
from PyQt6 import QtSerialPort from PyQt6 import QtSerialPort
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
from nqrduck.module.module_controller import ModuleController from nqrduck.module.module_controller import ModuleController
from .model import S11Data
logger = logging.getLogger(__name__) 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 # 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: elif text.startswith("r") and self.module.model.active_calibration == None:
logger.debug("Measurement finished") 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() 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 # If the text starts with 'r' and a short calibration is active we know that the data is a short calibration
elif text.startswith("r") and self.module.model.active_calibration == "short": elif text.startswith("r") and self.module.model.active_calibration == "short":
logger.debug("Short calibration finished") 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.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide() 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 # If the text starts with 'r' and an open calibration is active we know that the data is an open calibration
elif text.startswith("r") and self.module.model.active_calibration == "open": elif text.startswith("r") and self.module.model.active_calibration == "open":
logger.debug("Open calibration finished") 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.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide() 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 # If the text starts with 'r' and a load calibration is active we know that the data is a load calibration
elif text.startswith("r") and self.module.model.active_calibration == "load": elif text.startswith("r") and self.module.model.active_calibration == "load":
logger.debug("Load calibration finished") 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.model.active_calibration = None
self.module.view.frequency_sweep_spinner.hide() self.module.view.frequency_sweep_spinner.hide()
# If the text starts with 'i' we know that the data is an info message # 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") 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 == None:
logger.error("No short calibration data points available") logger.error("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 == None:
logger.error("No open calibration data points available") logger.error("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 == None:
logger.error("No load calibration data points available") logger.error("Could not calculate calibration. 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")
return return
# Then we calculate the calibration # Then we calculate the calibration
@ -144,14 +139,14 @@ class AutoTMController(ModuleController):
ideal_gamma_open = 1 ideal_gamma_open = 1
ideal_gamma_load = 0 ideal_gamma_load = 0
short_calibration = [10 **(-returnloss_s[1]) for returnloss_s in self.module.model.short_calibration] measured_gamma_short = self.module.model.short_calibration.gamma
open_calibration = [10 **(-returnloss_o[1]) for returnloss_o in self.module.model.open_calibration] measured_gamma_open = self.module.model.open_calibration.gamma
load_calibration = [10 **(-returnloss_l[1]) for returnloss_l in self.module.model.load_calibration] measured_gamma_load = self.module.model.load_calibration.gamma
e_00s = [] e_00s = []
e11s = [] e11s = []
delta_es = [] 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([ A = np.array([
[1, ideal_gamma_short * gamma_s, -ideal_gamma_short], [1, ideal_gamma_short * gamma_s, -ideal_gamma_short],
[1, ideal_gamma_open * gamma_o, -ideal_gamma_open], [1, ideal_gamma_open * gamma_o, -ideal_gamma_open],
@ -168,3 +163,50 @@ class AutoTMController(ModuleController):
delta_es.append(delta_e) delta_es.append(delta_e)
self.module.model.calibration = (e_00s, e11s, delta_es) 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"])

View file

@ -40,7 +40,22 @@ class S11Data:
@property @property
def gamma(self): def gamma(self):
"""Complex reflection coefficient""" """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): class AutoTMModel(ModuleModel):
@ -99,8 +114,8 @@ class AutoTMModel(ModuleModel):
@measurement.setter @measurement.setter
def measurement(self, value): def measurement(self, value):
"""The measurement value is a tuple of three lists: frequency, return loss and phase.""" """The measurement value is a tuple of three lists: frequency, return loss and phase."""
self._measurement = S11Data(value) self._measurement = value
self.measurement_finished.emit(self._measurement) self.measurement_finished.emit(value)
# Calibration properties # Calibration properties
@ -119,8 +134,8 @@ class AutoTMModel(ModuleModel):
@short_calibration.setter @short_calibration.setter
def short_calibration(self, value): def short_calibration(self, value):
logger.debug("Setting short calibration") logger.debug("Setting short calibration")
self._short_calibration = S11Data(value) self._short_calibration = value
self.short_calibration_finished.emit(self._short_calibration) self.short_calibration_finished.emit(value)
def init_short_calibration(self): 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.""" """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 @open_calibration.setter
def open_calibration(self, value): def open_calibration(self, value):
logger.debug("Setting open calibration") logger.debug("Setting open calibration")
self._open_calibration = S11Data(value) self._open_calibration = value
self.open_calibration_finished.emit(self._open_calibration) self.open_calibration_finished.emit(value)
def init_open_calibration(self): 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.""" """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 @load_calibration.setter
def load_calibration(self, value): def load_calibration(self, value):
logger.debug("Setting load calibration") logger.debug("Setting load calibration")
self._load_calibration = S11Data(value) self._load_calibration = value
self.load_calibration_finished.emit(self._load_calibration) self.load_calibration_finished.emit(value)
def init_load_calibration(self): 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.""" """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."""

View file

@ -1,9 +1,11 @@
import logging import logging
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import smithplot
from smithplot import SmithAxes
from PyQt6.QtGui import QMovie from PyQt6.QtGui import QMovie
from PyQt6.QtSerialPort import QSerialPort 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 PyQt6.QtCore import pyqtSlot, Qt
from nqrduck.module.module_view import ModuleView from nqrduck.module.module_view import ModuleView
from nqrduck.contrib.mplwidget import MplWidget from nqrduck.contrib.mplwidget import MplWidget
@ -129,6 +131,7 @@ class AutoTMView(ModuleView):
return_loss_db = data.return_loss_db return_loss_db = data.return_loss_db
phase = data.phase_deg phase = data.phase_deg
gamma = data.gamma
# Calibration test: # Calibration test:
#calibration = self.module.model.calibration #calibration = self.module.model.calibration
#e_00 = calibration[0] #e_00 = calibration[0]
@ -335,12 +338,25 @@ 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:
"""This method is called when the export button is clicked. """ filedialog = QFileDialog()
pass 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: def on_import_button_clicked(self) -> None:
"""This method is called when the import button is clicked. """ """This method is called when the import button is clicked. """
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: def on_apply_button_clicked(self) -> None:
"""This method is called when the apply button is clicked. """ """This method is called when the apply button is clicked. """