nqrduck-autotm/src/nqrduck_autotm/model.py

189 lines
6.2 KiB
Python
Raw Normal View History

2023-08-09 09:57:34 +00:00
import cmath
import numpy as np
2023-08-08 15:09:28 +00:00
import logging
2023-07-31 11:20:14 +00:00
from PyQt6.QtCore import pyqtSignal
2023-08-07 12:34:41 +00:00
from PyQt6.QtSerialPort import QSerialPort
2023-03-23 15:08:59 +00:00
from nqrduck.module.module_model import ModuleModel
2023-08-08 15:09:28 +00:00
logger = logging.getLogger(__name__)
2023-08-09 09:57:34 +00:00
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"""
2023-08-10 07:07:51 +00:00
if len(self.return_loss_db) != len(self.phase_rad):
raise ValueError("return_loss_db and phase_rad must be the same length")
return [cmath.rect(10 ** (-loss_db / 20), phase_rad) for loss_db, phase_rad in zip(self.return_loss_db, 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)
2023-08-09 09:57:34 +00:00
class AutoTMModel(ModuleModel):
2023-08-09 09:57:34 +00:00
2023-07-31 11:20:14 +00:00
available_devices_changed = pyqtSignal(list)
2023-08-07 12:34:41 +00:00
serial_changed = pyqtSignal(QSerialPort)
data_points_changed = pyqtSignal(list)
2023-08-08 15:09:28 +00:00
2023-08-09 09:57:34 +00:00
short_calibration_finished = pyqtSignal(S11Data)
open_calibration_finished = pyqtSignal(S11Data)
load_calibration_finished = pyqtSignal(S11Data)
measurement_finished = pyqtSignal(S11Data)
2023-08-07 12:34:41 +00:00
def __init__(self, module) -> None:
super().__init__(module)
self.data_points = []
2023-08-08 15:09:28 +00:00
self.active_calibration = None
2023-08-10 07:07:51 +00:00
self.calibration = None
2023-07-31 11:20:14 +00:00
@property
def available_devices(self):
return self._available_devices
2023-08-08 15:09:28 +00:00
2023-07-31 11:20:14 +00:00
@available_devices.setter
def available_devices(self, value):
self._available_devices = value
self.available_devices_changed.emit(value)
2023-07-31 13:24:46 +00:00
@property
def serial(self):
2023-08-09 09:57:34 +00:00
"""The serial property is used to store the current serial connection.
"""
2023-07-31 13:24:46 +00:00
return self._serial
2023-08-08 15:09:28 +00:00
2023-07-31 13:24:46 +00:00
@serial.setter
def serial(self, value):
self._serial = value
self.serial_changed.emit(value)
2023-08-07 12:34:41 +00:00
2023-08-08 15:09:28 +00:00
def add_data_point(self, frequency: float, return_loss: float, phase : float) -> None:
2023-08-09 09:57:34 +00:00
"""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.
"""
2023-08-08 15:09:28 +00:00
self.data_points.append((frequency, return_loss, phase))
2023-08-07 12:34:41 +00:00
self.data_points_changed.emit(self.data_points)
def clear_data_points(self) -> None:
2023-08-08 15:09:28 +00:00
"""Clear all data points from the model."""
2023-08-07 12:34:41 +00:00
self.data_points.clear()
self.data_points_changed.emit(self.data_points)
2023-08-08 15:09:28 +00:00
2023-08-09 09:57:34 +00:00
@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 = value
self.measurement_finished.emit(value)
2023-08-09 09:57:34 +00:00
# Calibration properties
2023-08-08 15:09:28 +00:00
@property
def active_calibration(self):
return self._active_calibration
@active_calibration.setter
def active_calibration(self, value):
self._active_calibration = value
@property
def short_calibration(self):
return self._short_calibration
@short_calibration.setter
def short_calibration(self, value):
logger.debug("Setting short calibration")
self._short_calibration = value
self.short_calibration_finished.emit(value)
2023-08-08 15:09:28 +00:00
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."""
self.active_calibration = "short"
self.clear_data_points()
@property
def open_calibration(self):
return self._open_calibration
@open_calibration.setter
def open_calibration(self, value):
logger.debug("Setting open calibration")
self._open_calibration = value
self.open_calibration_finished.emit(value)
2023-08-08 15:09:28 +00:00
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."""
self.active_calibration = "open"
self.clear_data_points()
@property
def load_calibration(self):
return self._load_calibration
@load_calibration.setter
def load_calibration(self, value):
logger.debug("Setting load calibration")
self._load_calibration = value
self.load_calibration_finished.emit(value)
2023-08-08 15:09:28 +00:00
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."""
self.active_calibration = "load"
self.clear_data_points()
@property
def calibration(self):
return self._calibration
@calibration.setter
def calibration(self, value):
logger.debug("Setting calibration")
self._calibration = value
2023-08-09 09:57:34 +00:00