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
2023-08-16 14:23:23 +00:00
class S11Data :
2023-08-09 09:57:34 +00:00
# Conversion factors - the data is generally sent and received in mV
# These values are used to convert the data to dB and degrees
2023-08-16 14:23:23 +00:00
CENTER_POINT_MAGNITUDE = 900 # mV
CENTER_POINT_PHASE = 1800 # mV
MAGNITUDE_SLOPE = 30 # dB/mV
PHASE_SLOPE = 10 # deg/mV
2023-08-09 09:57:34 +00:00
2023-08-16 14:23:23 +00:00
def __init__ ( self , data_points : list ) - > None :
2023-08-09 09:57:34 +00:00
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
2023-08-16 14:23:23 +00:00
2023-08-09 09:57:34 +00:00
@property
def return_loss_db ( self ) :
2023-08-16 14:23:23 +00:00
return (
self . return_loss_mv - self . CENTER_POINT_MAGNITUDE
) / self . MAGNITUDE_SLOPE
2023-08-09 09:57:34 +00:00
@property
def phase_deg ( self ) :
2023-08-11 12:25:19 +00:00
""" Returns the absolute value of the phase in degrees """
return ( self . phase_mv - self . CENTER_POINT_PHASE ) / self . PHASE_SLOPE
2023-08-16 14:23:23 +00:00
2023-08-09 09:57:34 +00:00
@property
def phase_rad ( self ) :
return self . phase_deg * cmath . pi / 180
2023-08-16 14:23:23 +00:00
2023-08-09 09:57:34 +00:00
@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 " )
2023-08-16 14:23:23 +00:00
return [
cmath . rect ( 10 * * ( - loss_db / 20 ) , phase_rad )
for loss_db , phase_rad in zip ( self . return_loss_db , self . phase_rad )
]
2023-08-10 07:07:51 +00:00
2023-08-09 14:32:13 +00:00
def to_json ( self ) :
return {
" frequency " : self . frequency . tolist ( ) ,
" return_loss_mv " : self . return_loss_mv . tolist ( ) ,
2023-08-16 14:23:23 +00:00
" phase_mv " : self . phase_mv . tolist ( ) ,
2023-08-09 14:32:13 +00:00
}
2023-08-16 14:23:23 +00:00
2023-08-09 14:32:13 +00:00
@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
2023-08-16 14:23:23 +00:00
class LookupTable :
""" This class is used to store a lookup table for tuning and matching of electrical probeheads. """
data = dict ( )
def __init__ (
self ,
start_frequency : float ,
stop_frequency : float ,
frequency_step : float ,
voltage_resolution : float ,
) - > None :
self . start_frequency = start_frequency
self . stop_frequency = stop_frequency
self . frequency_step = frequency_step
self . voltage_resolution = voltage_resolution
# This is the frequency at which the tuning and matching process was started
self . started_frequency = None
self . init_voltages ( )
def init_voltages ( self ) - > None :
""" Initialize the lookup table with default values. """
for frequency in np . arange (
self . start_frequency , self . stop_frequency , self . frequency_step
) :
self . started_frequency = frequency
self . add_voltages ( None , None )
def is_incomplete ( self ) - > bool :
""" 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 .
Returns :
bool : True if the lookup table is incomplete , False otherwise .
"""
return any (
[
tuning_voltage is None or matching_voltage is None
for tuning_voltage , matching_voltage in self . data . values ( )
]
)
def get_next_frequency ( self ) - > float :
""" This method returns the next frequency for which the tuning and matching voltage is not yet set.
Returns :
float : The next frequency for which the tuning and matching voltage is not yet set .
"""
for frequency , ( tuning_voltage , matching_voltage ) in self . data . items ( ) :
if tuning_voltage is None or matching_voltage is None :
return frequency
return None
def add_voltages ( self , tuning_voltage : float , matching_voltage : float ) - > None :
""" Add a tuning and matching voltage for the last started frequency to the lookup table.
Args :
tuning_voltage ( float ) : The tuning voltage for the given frequency .
matching_voltage ( float ) : The matching voltage for the given frequency . """
self . data [ self . started_frequency ] = ( tuning_voltage , matching_voltage )
class AutoTMModel ( ModuleModel ) :
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-16 14:23:23 +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-16 14:23:23 +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
2023-08-16 14:23:23 +00:00
2023-08-09 09:57:34 +00:00
@measurement.setter
def measurement ( self , value ) :
""" The measurement value is a tuple of three lists: frequency, return loss and phase. """
2023-08-09 14:32:13 +00:00
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
2023-08-16 14:23:23 +00:00
2023-08-08 15:09:28 +00:00
@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 " )
2023-08-09 14:32:13 +00:00
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 " )
2023-08-09 14:32:13 +00:00
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 " )
2023-08-09 14:32:13 +00:00
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
2023-08-16 14:23:23 +00:00
2023-08-08 15:09:28 +00:00
@calibration.setter
def calibration ( self , value ) :
logger . debug ( " Setting calibration " )
self . _calibration = value
2023-08-16 14:23:23 +00:00
@property
def LUT ( self ) :
return self . _LUT
@LUT.setter
def LUT ( self , value ) :
self . _LUT = value