From f1aac102d24103279bd8ae1ca312b09f0c61ec14 Mon Sep 17 00:00:00 2001
From: Martin Killenberg <martin.killenberg@desy.de>
Date: Wed, 16 Aug 2023 15:05:01 +0200
Subject: [PATCH] feat: separate VNA into generic DUT class

All notions of VNA, magnitude and phase in prototype.py and the GUI have been
removed and generalised.
This is just refactoring, no functional change. However, the syntax of the
config file has been adapted.
---
 Python_script/climate-lab-gui.py        |   3 +-
 Python_script/dut_measurement.py        |  33 ++++
 Python_script/prototype.py              | 247 ++++++++----------------
 Python_script/test_stand_parameter.json |  21 +-
 Python_script/vna_measurement.py        | 106 ++++++++++
 5 files changed, 237 insertions(+), 173 deletions(-)
 create mode 100644 Python_script/dut_measurement.py
 create mode 100644 Python_script/vna_measurement.py

diff --git a/Python_script/climate-lab-gui.py b/Python_script/climate-lab-gui.py
index c30bd98..b77c3da 100755
--- a/Python_script/climate-lab-gui.py
+++ b/Python_script/climate-lab-gui.py
@@ -74,8 +74,7 @@ class TestStandMainWindow(QMainWindow):
             self.setEnabled(True)
             return
         
-        meas = prototype.Measurements(config_data['chamber_ip'], config_data['vna_ip'], output_basename,
-                                      False, config_data, ext_sensor_channels, config_data['logger_ip'])
+        meas = prototype.Measurements(config_data, output_basename,False, ext_sensor_channels)
         try:
             if self.tempSweepButton.isChecked():
                 temperatures = meas.perform_sweep(self.startParameter.value(), self.stopParameter.value(),
diff --git a/Python_script/dut_measurement.py b/Python_script/dut_measurement.py
new file mode 100644
index 0000000..bffb95d
--- /dev/null
+++ b/Python_script/dut_measurement.py
@@ -0,0 +1,33 @@
+from abc import ABC, abstractmethod
+
+
+class DutMeasurement(ABC):
+    """
+    Interface for DUT measurements
+    """
+    @abstractmethod
+    def get_dut_measurements(self):
+        """
+        Returns a dictionary with column names a keys and scalar values
+        """
+        pass
+
+    @abstractmethod
+    def get_dut_signal_names(self):
+        """
+        Returns a list of names used as keys used in the dut measurements dictionary
+        """
+        pass
+
+    @abstractmethod
+    def get_dut_reference_signal_names(self):
+        """
+        Returns a list of signal names used for stability checks
+        """
+        pass
+
+    @abstractmethod
+    def get_dut_max_delta_signals(self):
+        """
+        Return the maximum deltas of the reference signals
+        """
diff --git a/Python_script/prototype.py b/Python_script/prototype.py
index 2095a3a..2b708cd 100755
--- a/Python_script/prototype.py
+++ b/Python_script/prototype.py
@@ -1,14 +1,12 @@
 #!/usr/bin/python3
 import csv
-import math
-import cmath
 import time
 import numpy
 from argparse import ArgumentParser
 import pandas as pd
 import matplotlib.pyplot as plt
 import climate_chamber
-import VNA
+import vna_measurement
 import virtual_time
 import json
 import MeasurementPlot
@@ -17,34 +15,28 @@ import analysis
 import external_sensors
 import PostPlot
 import os
-import pyvisa
-
 
+# Only use these when calculating the equilibrium indicator. Don't use in the algorithm.
 TEMPERATURE_STABLE = 0x1
 HUMIDITY_STABLE = 0x2
-MAGNITUDE_STABLE = 0x4
-PHASE_STABLE = 0x8
+DUT_SIGNAL0_STABLE = 0x4
+DUT_SIGNAL1_STABLE = 0x8
 MEASUREMENT_STABLE = 0x10
 
 
-# Class has only attributes which we are using in the read_data_function to read data from VNA and Chamber
+# Class has only attributes which we are using in the read_data_function to read data from DUT and Chamber
 class MeasurementData:
     
-    def __init__(self, timestamp, temp, hum, power, frequency, s11, s21, s12, s22, perc_temp_heater,
-                 perc_hum_heater, temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room,
+    def __init__(self, timestamp, temp, hum, dut_data, percent_temp_heater,
+                 percent_hum_heater, temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room,
                  hum_meas_instr, air_press_room, temp_chamber_meas_instr, hum_chamber_meas_instr):
         self.timestamp = timestamp
         self.temp = temp
         self.hum = hum
-        self.power = power
-        self.frequency = frequency
-        self.s11 = s11
-        self.s21 = s21
-        self.s12 = s12
-        self.s22 = s22
-
-        self.perc_temp_heater = perc_temp_heater
-        self.perc_hum_heater = perc_hum_heater
+        self.dut_data = dut_data
+
+        self.percent_temp_heater = percent_temp_heater
+        self.percent_hum_heater = percent_hum_heater
         self.temp_dut = temp_dut
         self.temp_room = temp_room
         self.temp_meas_instr = temp_meas_instr
@@ -57,40 +49,35 @@ class MeasurementData:
 
 
 class Measurements:
-    def __init__(self, chamber_address, vna_address, output_basename, standby, config_data,
-                 ext_sensor_channels, logger_address):
+    def __init__(self, config_data, output_basename, standby, ext_sensor_channels):
         self.max_delta_temp = config_data['delta_temp']
         self.max_delta_hum = config_data['delta_hum']
-        self.max_delta_mag = config_data['delta_mag']
-        self.max_delta_phase = config_data['delta_phase']
+
         self.ext_sensor_channels = ext_sensor_channels
-        self.logger_model = config_data['logger_model']
         self.sleep_time = config_data["sleep_time"]
-        self.frequency = config_data["frequency"]
-        self.vna_config_file = config_data["vna_config_file"]
         target_accuracy = [self.max_delta_temp, self.max_delta_hum]
-        self.chamber = climate_chamber.create_chamber(chamber_address, target_accuracy)
+        self.chamber = climate_chamber.create_chamber(config_data['chamber_ip'], target_accuracy)
         self.instr_chamber = climate_chamber.create_chamber(config_data['instr_chamber_ip'],
                                                             target_accuracy)
-        self.vna = VNA.create_vna(vna_address, target_accuracy)
+        # FIXME: Do we want a factory function for the DUTs?
+        if config_data['dut']['type'] == 'VNA':
+            self.dut = vna_measurement.VnaMeasurement(config_data['dut'], target_accuracy)
+        else:
+            raise Exception('Unknown DUT type: '+config_data['dut']['type'])
+
+        self.max_delta_dut_signals = self.dut.get_dut_max_delta_signals()
 
         # data logger for external sensors
-        self.ext_sensors = external_sensors.create_sensors(config_data['logger_model'], logger_address,
+        self.ext_sensors = external_sensors.create_sensors(config_data['logger_model'], config_data['logger_ip'],
                                                            ext_sensor_channels)
 
         self.standby = standby
         self.output_basename = output_basename
-        self.clock = virtual_time.get_clock(chamber_address, target_accuracy)
+        self.clock = virtual_time.get_clock(config_data['chamber_ip'], target_accuracy)
         self.temperature_stable = False
         self.humidity_stable = False
-        self.magnitude_stable = False
-        self.phase_stable = False
-
-        self.vna.load_config(self.vna_config_file, self.frequency)
-        self.vna.create_new_trace("Trace1", "S11")
-        self.vna.create_new_trace("Trace2", "S12")
-        self.vna.create_new_trace("Trace3", "S21")
-        self.vna.create_new_trace("Trace4", "S22")
+        self.dut_signals_stable = [False, False]
+
         self.postplot_obj = None
         
         self.measurement_plot = MeasurementPlot.MeasurementPlot(trace_subplot5=config_data['trace_subplot5'])
@@ -170,13 +157,11 @@ class Measurements:
     def perform_single_measurement(self, output, target_temp, target_hum, soaking_time, n_stable_reads):
         with open(output, mode='w', newline='') as csv_file:
             fieldnames = ['TIMESTAMP', 'TARGET_TEMPERATURE', 'READBACK_TEMPERATURE', 'TARGET_HUMIDITY',
-                          'READBACK_HUMIDITY', 'RF_POWER', 'RF_FREQUENCY', 'DUT_IDENTIFIER', 'RUN_ID',
-                          'EQUILIBRIUM_INDICATOR', 'TEMP_HEATER', 'HUM_HEATER',
-                          'TEMP_DUT', 'TEMP_ROOM', 'TEMP_MEAS_INSTR',
-                          'HUM_DUT','HUM_ROOM', 'HUM_MEAS_INSTR', 'AIR_PRESS_ROOM',
-                          'READBACK_TEMP_MEAS_INSTR', 'READBACK_HUM_MEAS_INSTR',
-                          'S11_MAGNITUDE', 'S11_PHASE', 'S12_MAGNITUDE',
-                          'S12_PHASE', 'S21_MAGNITUDE', 'S21_PHASE', 'S22_MAGNITUDE', 'S22_PHASE']
+                          'READBACK_HUMIDITY', 'DUT_IDENTIFIER', 'RUN_ID', 'EQUILIBRIUM_INDICATOR', 'TEMP_HEATER',
+                          'HUM_HEATER', 'TEMP_DUT', 'TEMP_ROOM', 'TEMP_MEAS_INSTR', 'HUM_DUT', 'HUM_ROOM',
+                          'HUM_MEAS_INSTR', 'AIR_PRESS_ROOM', 'READBACK_TEMP_MEAS_INSTR', 'READBACK_HUM_MEAS_INSTR']
+            fieldnames.extend(self.dut.get_dut_signal_names())
+
             # csv.dict writer add adda row wise
             writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
             writer.writeheader()
@@ -196,8 +181,7 @@ class Measurements:
             next_read_time = self.clock.time() + self.sleep_time
             while do_another_measurement:
                 # wait until set point is reached (+soaking time)
-                magnitudes_queue = []
-                phase_queue = []
+                dut_signal_queues = [[], []]
 
                 while True:
                     data = self.read_data()
@@ -208,45 +192,35 @@ class Measurements:
                     self.temperature_stable = self.calculate_temperature_stability(target_temp, float(data.temp))
                     self.humidity_stable = self.calculate_humidity_stability(target_hum, float(data.hum))
 
-                    # The queue must not be longer than the max number of soaking reads.
-                    # If the queue is already full, we have to pop the first element before we can add the
-                    # current measurement.
-                    if len(magnitudes_queue) >= number_of_soaking_reads:
-                        magnitudes_queue.pop(0)
-                    if self.temperature_stable and self.humidity_stable:
-                        magnitudes_queue.append(self.calculate_mean_magnitude_db(data.s21))
-                    else:
-                        magnitudes_queue.clear()
-                    # check cable stability parameters
-                    self.magnitude_stable = False
-                    if len(magnitudes_queue) >= number_of_soaking_reads:
-                        spread = max(magnitudes_queue) - min(magnitudes_queue)
-                        if spread < 2*self.max_delta_mag:
-                            self.magnitude_stable = True
-
-                    if len(phase_queue) >= number_of_soaking_reads:
-                        phase_queue.pop(0)
-                    if self.temperature_stable and self.humidity_stable:
-                        phase_queue.append(self.calculate_mean_phase(data.s21))
-                    else:
-                        phase_queue.clear()
-
-                    self.phase_stable = False
-                    if len(phase_queue) >= number_of_soaking_reads:
-                        spread = max(phase_queue) - min(phase_queue)
-                        if spread < 2*self.max_delta_phase:
-                            self.phase_stable = True
+                    # Use indexed loop because we need to modify, and zip seems to copy the content :-(
+                    for i, signal_queue in enumerate(dut_signal_queues):
+                        # The queue must not be longer than the max number of soaking reads.
+                        # If the queue is already full, we have to pop the first element before we can add the
+                        # current measurement.
+                        if len(signal_queue) >= number_of_soaking_reads:
+                            signal_queue.pop(0)
+                        if self.temperature_stable and self.humidity_stable:
+                            signal_queue.append(data.dut_data[self.dut.get_dut_reference_signal_names()[i]])
+                        else:
+                            signal_queue.clear()
+
+                        self.dut_signals_stable[i] = False
+                        if len(signal_queue) >= number_of_soaking_reads:
+                            spread = max(signal_queue) - min(signal_queue)
+                            if spread < 2*self.max_delta_dut_signals[i]:
+                                self.dut_signals_stable[i] = True
 
                     print('Setpoint: ' + str(target_temp) + ' ' + str(target_hum) + ' | Temp: ' + data.temp +
                           ' °C' + ' | Humid: ' + data.hum + '%'
-                          + ' | soaking read nr' + str(len(magnitudes_queue)))
+                          + ' | soaking read nr' + str(len(dut_signal_queues[0])))
                     self.store_and_plot_data(target_temp, target_hum, data, self.cook_up_equi_indicator())
                     writer.writerow(self.data_collection[-1])
 
-                    if self.temperature_stable and self.humidity_stable and self.magnitude_stable and\
-                            self.phase_stable:
-                        reference_magnitude = magnitudes_queue[-1]
-                        reference_phase = phase_queue[-1]
+                    if self.temperature_stable and self.humidity_stable and self.dut_signals_stable[0] and\
+                            self.dut_signals_stable[1]:
+                        reference_values = []
+                        for signal_queue in dut_signal_queues:
+                            reference_values.append(signal_queue[-1])
                         print('SOAKING FINISHED!')
                         break
                     else:
@@ -261,17 +235,18 @@ class Measurements:
                     data = self.read_data()
                     self.temperature_stable = self.calculate_temperature_stability(target_temp, float(data.temp))
                     self.humidity_stable = self.calculate_humidity_stability(target_hum, float(data.hum))
-                    mag = self.calculate_mean_magnitude_db(data.s21)
-                    phase = self.calculate_mean_phase(data.s21)
-                    self.magnitude_stable = (reference_magnitude-self.max_delta_mag <= mag) and\
-                                            (mag <= reference_magnitude+self.max_delta_mag)
-                    self.phase_stable = (reference_phase-self.max_delta_phase <= phase) and\
-                                        (phase <= reference_phase+self.max_delta_phase)
+                    this_measurement_stable = (self.temperature_stable and self.humidity_stable)
+                    for j, max_delta in enumerate(self.max_delta_dut_signals):
+                        value = data.dut_data[self.dut.get_dut_reference_signal_names()[j]]
+                        self.dut_signals_stable[j] = (reference_values[j] - max_delta <= value) and\
+                           (value <= reference_values[j] + max_delta)
+                        if not self.dut_signals_stable[j]:
+                            this_measurement_stable = False
+
                     self.store_and_plot_data(target_temp, target_hum, data, self.cook_up_equi_indicator())
                     supposedly_stable_measurements.append(self.data_collection[-1])
                                 
-                    if (self.temperature_stable and self.humidity_stable and self.magnitude_stable and
-                            self.phase_stable):
+                    if this_measurement_stable:
                         print('Stable measurement ' + str(i+1) + '/' + str(n_stable_reads))
                         self.sleep_until(next_read_time)
                         next_read_time += self.sleep_time
@@ -282,8 +257,8 @@ class Measurements:
 
                 for measurement in supposedly_stable_measurements:
                     if all_measurements_stable:
-                        measurement['EQUILIBRIUM_INDICATOR'] = TEMPERATURE_STABLE | HUMIDITY_STABLE |\
-                                                               MAGNITUDE_STABLE | PHASE_STABLE |\
+                        measurement['EQUILIBRIUM_INDICATOR'] = TEMPERATURE_STABLE | HUMIDITY_STABLE | \
+                                                               DUT_SIGNAL0_STABLE | DUT_SIGNAL1_STABLE | \
                                                                MEASUREMENT_STABLE
                         do_another_measurement = False
 
@@ -298,35 +273,15 @@ class Measurements:
         [temp, hum, mode, alarms] = self.chamber.read_monitor().split(',')
         perc_temp_heater, perc_hum_heater = self.chamber.get_heater_percentage()
 
-        # loop with 10 iterations in case of VNA readout error
-        iteration = 0
-        while iteration < 10:
-            try:
-                power = self.vna.get_current_power()
-                frequency = self.vna.get_current_cw_frequency()
-                self.vna.do_single_sweep()
-                s11 = self.get_trace_data("Trace1")
-                s12 = self.get_trace_data("Trace2")
-                s21 = self.get_trace_data("Trace3")
-                s22 = self.get_trace_data("Trace4")
-                # if readout is successful, leave loop
-                break
-            # exception for pyvisa error or timeout
-            except pyvisa.errors.VisaIOError:
-                # call reset function for VNA status register and error queue
-                self.vna.reset_status()
-                print('An error occurred during VNA read out')
-                # wait one second for next try
-                time.sleep(1)
-                iteration += 1
+        dut_data = self.dut.get_dut_measurements()
 
         temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room = \
             self.ext_sensors.get_sensor_values()
         
         [temp_chamber_meas_instr, hum_chamber_meas_instr, mode_meas_instr, alarms_meas_instr] = \
             self.instr_chamber.read_monitor().split(',')
-        
-        return MeasurementData(int(self.clock.time()), temp, hum, power, frequency, s11, s21, s12, s22,
+
+        return MeasurementData(int(self.clock.time()), temp, hum, dut_data,
                                perc_temp_heater, perc_hum_heater, temp_dut, temp_room, temp_meas_instr,
                                hum_dut, hum_room, hum_meas_instr, air_press_room, temp_chamber_meas_instr,
                                hum_chamber_meas_instr)
@@ -338,11 +293,9 @@ class Measurements:
             'READBACK_TEMPERATURE': float(data.temp),
             'TARGET_HUMIDITY': target_hum,
             'READBACK_HUMIDITY': float(data.hum),
-            'RF_POWER': data.power,
-            'RF_FREQUENCY': data.frequency,
             'EQUILIBRIUM_INDICATOR': equi_indicator,
-            'TEMP_HEATER': data.perc_temp_heater,
-             'HUM_HEATER': data.perc_hum_heater,
+            'TEMP_HEATER': data.percent_temp_heater,
+             'HUM_HEATER': data.percent_hum_heater,
             'TEMP_DUT': data.temp_dut,
             'TEMP_ROOM': data.temp_room,
             'TEMP_MEAS_INSTR': data.temp_meas_instr,
@@ -352,15 +305,8 @@ class Measurements:
             'AIR_PRESS_ROOM': data.air_press_room,
             'READBACK_TEMP_MEAS_INSTR': float(data.temp_chamber_meas_instr),
             'READBACK_HUM_MEAS_INSTR': float(data.hum_chamber_meas_instr),
-            'S11_PHASE': self.calculate_mean_phase(data.s11),
-            'S11_MAGNITUDE': self.calculate_mean_magnitude_db(data.s11),
-            'S21_PHASE': self.calculate_mean_phase(data.s21),
-            'S21_MAGNITUDE': self.calculate_mean_magnitude_db(data.s21),
-            'S12_PHASE': self.calculate_mean_phase(data.s12),
-            'S12_MAGNITUDE': self.calculate_mean_magnitude_db(data.s12),
-            'S22_PHASE': self.calculate_mean_phase(data.s22),
-            'S22_MAGNITUDE': self.calculate_mean_magnitude_db(data.s22)
         }
+        measurement.update(data.dut_data)
         self.data_collection.append(measurement)
         data_frame = pd.DataFrame(self.data_collection)
         self.measurement_plot.draw(data_frame)
@@ -368,38 +314,6 @@ class Measurements:
     def current_milli_time(self):
         return int(round(self.clock.time() * 1000))
 
-    def get_trace_data(self, trace):
-        return self.vna.get_list_of_measurement_values(trace, "SDAT")
-
-    def calculate_complex_numbers(self, values_list):
-        real_num = values_list[::2]
-        imaginary_num = values_list[1::2]
-        complex_numbers = []
-        for i, q in zip(real_num, imaginary_num):
-            complex_numbers.append(complex(i, q))
-        return complex_numbers
-
-    def calculate_magnitudes(self, values_list):
-        complex_numbers = self.calculate_complex_numbers(values_list)
-        magnitudes = [abs(val) for val in complex_numbers]
-        return magnitudes
-
-    def calculate_mean_magnitude(self, values_list):
-        magnitudes = self.calculate_magnitudes(values_list)
-        return numpy.mean(magnitudes)
-
-    def calculate_mean_magnitude_db(self, values_list):
-        return 20*math.log10(self.calculate_mean_magnitude(values_list))
-
-    def calculate_phases(self, values_list):
-        complex_numbers = self.calculate_complex_numbers(values_list)
-        phases = [math.degrees(cmath.phase(val)) for val in complex_numbers]
-        return phases
-
-    def calculate_mean_phase(self, values_list):
-        phases = self.calculate_phases(values_list)
-        return numpy.mean(phases)
-
     def cook_up_equi_indicator(self):
         equilibrium_indicator = 0
 
@@ -409,11 +323,11 @@ class Measurements:
         if self.humidity_stable:
             equilibrium_indicator = equilibrium_indicator | HUMIDITY_STABLE
 
-        if self.magnitude_stable:
-            equilibrium_indicator = equilibrium_indicator | MAGNITUDE_STABLE
+        if self.dut_signals_stable[0]:
+            equilibrium_indicator = equilibrium_indicator | DUT_SIGNAL0_STABLE
 
-        if self.phase_stable:
-            equilibrium_indicator = equilibrium_indicator | PHASE_STABLE
+        if self.dut_signals_stable[1]:
+            equilibrium_indicator = equilibrium_indicator | DUT_SIGNAL1_STABLE
 
         return equilibrium_indicator
 
@@ -474,12 +388,6 @@ def run_temperature_sweep_from_file(temperature_sweep_file, meas):
 
 if __name__ == '__main__':
     parser = ArgumentParser()
-    parser.add_argument("-c", "--chamber",
-                        help="IP address of climate chamber", metavar="ADDR",
-                        required=True)
-    parser.add_argument("-v", "--vna",
-                        help="IP address of VNA", metavar="ADDR",
-                        required=True)
     parser.add_argument('-f', '--file',
                         help='File containing custom list of measurements',
                         default='')
@@ -509,7 +417,7 @@ if __name__ == '__main__':
     else:
         output_basename = args.output
 
-    print(args.chamber, args.vna, args.file, output_basename, args.standby)
+    print(args.file, output_basename, args.standby)
 
     # reading json file for target accuracy
     with open('test_stand_parameter.json', 'r') as f:
@@ -518,8 +426,7 @@ if __name__ == '__main__':
     with open('ext_sensor_channels.json', 'r') as f2:
         ext_sensor_channels = json.load(f2)
 
-    mes = Measurements(args.chamber, args.vna, output_basename, args.standby, config_data, ext_sensor_channels,
-                       config_data['logger_ip'])
+    mes = Measurements(config_data, output_basename, args.standby, ext_sensor_channels)
     try:
         if args.file:
             n_measurements = mes.perform_measurements(args.file)
diff --git a/Python_script/test_stand_parameter.json b/Python_script/test_stand_parameter.json
index c32eb2a..11fabf9 100644
--- a/Python_script/test_stand_parameter.json
+++ b/Python_script/test_stand_parameter.json
@@ -1 +1,20 @@
-{"delta_temp": 0.1, "delta_hum": 1, "delta_mag": 0.13 , "delta_phase": 1.5, "sleep_time": 10.0, "frequency": 1300000000, "vna_config_file": "CalSetup2.znxml","chamber_ip":"192.168.115.186", "instr_chamber_ip": "192.168.115.187", "vna_ip":"192.168.115.39", "data_folder":"measurements", "logger_ip": "192.168.115.94", "time_unit": "min", "trace_subplot5": "logger_sens", "logger_model": "710"}
+{
+  "delta_temp": 0.1,
+  "delta_hum": 1,
+  "dut": {
+    "type": "VNA",
+    "delta_mag": 0.13,
+    "delta_phase": 1.5,
+    "frequency": 1300000000,
+    "vna_ip": "192.168.115.39",
+    "vna_config_file": "CalSetup2.znxml"
+  },
+  "sleep_time": 10,
+  "chamber_ip": "192.168.115.186",
+  "instr_chamber_ip": "192.168.115.187",
+  "data_folder": "measurements",
+  "logger_ip": "192.168.115.94",
+  "time_unit": "min",
+  "trace_subplot5": "logger_sens",
+  "logger_model": "710"
+}
diff --git a/Python_script/vna_measurement.py b/Python_script/vna_measurement.py
new file mode 100644
index 0000000..8185049
--- /dev/null
+++ b/Python_script/vna_measurement.py
@@ -0,0 +1,106 @@
+import VNA
+import pyvisa
+import time
+import numpy
+import math
+import cmath
+
+import dut_measurement
+
+class VnaData:
+    def __init__(self, power, frequency, s11, s21, s12, s22):
+        self.power = power
+        self.frequency = frequency
+        self.s11 = s11
+        self.s21 = s21
+        self.s12 = s12
+        self.s22 = s22
+
+
+class VnaMeasurement(dut_measurement.DutMeasurement):
+    def __init__(self, config_data, target_accuracy):
+        self.delta_mag = config_data['delta_mag']
+        self.delta_phase = config_data['delta_phase']
+
+        self.vna = VNA.create_vna(config_data['vna_ip'], target_accuracy)
+
+        self.vna.load_config(config_data['vna_config_file'],  config_data['frequency'])
+        self.vna.create_new_trace("Trace1", "S11")
+        self.vna.create_new_trace("Trace2", "S12")
+        self.vna.create_new_trace("Trace3", "S21")
+        self.vna.create_new_trace("Trace4", "S22")
+
+    def _get_trace_data(self, trace):
+        return self.vna.get_list_of_measurement_values(trace, "SDAT")
+
+    def get_dut_measurements(self):
+        # FIXME: The try/catch should be way down in the VNA class
+        for iteration in range(10):
+            try:
+                power = self.vna.get_current_power()
+                frequency = self.vna.get_current_cw_frequency()
+                self.vna.do_single_sweep()
+                s11 = self._get_trace_data("Trace1")
+                s12 = self._get_trace_data("Trace2")
+                s21 = self._get_trace_data("Trace3")
+                s22 = self._get_trace_data("Trace4")
+
+                return {'RF_POWER': power, 'RF_FREQUENCY': frequency,
+                        'S11_MAGNITUDE': self.calculate_mean_magnitude_db(s11),
+                        'S11_PHASE': self.calculate_mean_phase(s11),
+                        'S12_MAGNITUDE': self.calculate_mean_magnitude_db(s12),
+                        'S12_PHASE': self.calculate_mean_phase(s12),
+                        'S21_MAGNITUDE': self.calculate_mean_magnitude_db(s21),
+                        'S21_PHASE': self.calculate_mean_phase(s21),
+                        'S22_MAGNITUDE': self.calculate_mean_magnitude_db(s22),
+                        'S22_PHASE': self.calculate_mean_phase(s21)}
+
+            # exception for pyvisa error or timeout
+            except pyvisa.errors.VisaIOError:
+                # call reset function for VNA status register and error queue
+                self.vna.reset_status()
+                print('An error occurred during VNA read out')
+                # wait one second for next try
+                time.sleep(1)
+        # FIXME: In case we did not succeed we need something like a stop_measurement exception
+
+        raise Exception('FIXME: Throw stop_measurement here, and handle it. Don\'t crash!')
+
+    def get_dut_signal_names(self):
+        return ['RF_POWER', 'RF_FREQUENCY', 'S11_MAGNITUDE', 'S11_PHASE', 'S12_MAGNITUDE',
+                'S12_PHASE', 'S21_MAGNITUDE', 'S21_PHASE', 'S22_MAGNITUDE', 'S22_PHASE']
+
+    def get_dut_reference_signal_names(self):
+        return ['S21_MAGNITUDE', 'S21_PHASE']
+
+    def get_dut_max_delta_signals(self):
+        return [self.delta_mag, self.delta_phase]
+
+    def calculate_complex_numbers(self, values_list):
+        real_num = values_list[::2]
+        imaginary_num = values_list[1::2]
+        complex_numbers = []
+        for i, q in zip(real_num, imaginary_num):
+            complex_numbers.append(complex(i, q))
+        return complex_numbers
+
+    def calculate_magnitudes(self, values_list):
+        complex_numbers = self.calculate_complex_numbers(values_list)
+        magnitudes = [abs(val) for val in complex_numbers]
+        return magnitudes
+
+    def calculate_mean_magnitude(self, values_list):
+        magnitudes = self.calculate_magnitudes(values_list)
+        return numpy.mean(magnitudes)
+
+    def calculate_mean_magnitude_db(self, values_list):
+        return 20*math.log10(self.calculate_mean_magnitude(values_list))
+
+    def calculate_phases(self, values_list):
+        complex_numbers = self.calculate_complex_numbers(values_list)
+        phases = [math.degrees(cmath.phase(val)) for val in complex_numbers]
+        return phases
+
+    def calculate_mean_phase(self, values_list):
+        phases = self.calculate_phases(values_list)
+        return numpy.mean(phases)
-- 
GitLab