From 90d6b10902ede07d225d349eac5c3ab9731cd38d Mon Sep 17 00:00:00 2001
From: Martin Killenberg <martin.killenberg@desy.de>
Date: Mon, 21 Aug 2023 11:52:33 +0200
Subject: [PATCH] feat: multi frequency measurements for the VNA

- introduce multiple measurement sets in the dut_measuremets
- create one plot per measurement set for time traces
- analysis is showing all sets against temperature/humidity
---
 Python_script/PostPlot.py               |  17 ++--
 Python_script/VNA.py                    |   4 +-
 Python_script/VNA_dummy.py              |  39 +++++----
 Python_script/analysis.py               | 106 ++++++++++++++----------
 Python_script/climate-lab-gui.py        |   9 +-
 Python_script/dut_measurement.py        |  12 ++-
 Python_script/first_tempsweep.txt       |   2 +-
 Python_script/prototype.py              |  94 ++++++++++++++-------
 Python_script/test_stand_parameter.json |   2 +-
 Python_script/vna_measurement.py        |  28 +++----
 10 files changed, 193 insertions(+), 120 deletions(-)

diff --git a/Python_script/PostPlot.py b/Python_script/PostPlot.py
index 40cb084..3c185ad 100644
--- a/Python_script/PostPlot.py
+++ b/Python_script/PostPlot.py
@@ -33,7 +33,7 @@ class PostPlot:
         
         return data_frame
                 
-    def plot_frame_data(self, data_frame, title ='', time_unit ='min'):
+    def plot_frame_data(self, data_frame, title, time_unit ='min', measurement_set=None, ):
         
         # set title of plot 
         self.fig.suptitle("Measurement "+title, color="red")
@@ -46,8 +46,11 @@ class PostPlot:
             element.set_xlabel("Time [%s]" %time_unit)         
         
         # make a copy of data_frame in parameter list without changing original during modification
-        postplot_data_frame = data_frame.copy()
-        
+        if measurement_set is None:
+            postplot_data_frame = data_frame.copy()
+        else:
+            postplot_data_frame = data_frame.loc[data_frame['SET_NAME'] == measurement_set].copy().reset_index()
+
         # time stamp of index = 0 is the start point of time axis
         time_sec = postplot_data_frame.TIMESTAMP - postplot_data_frame.TIMESTAMP[0]
         
@@ -62,7 +65,7 @@ class PostPlot:
         self.measplot.draw(postplot_data_frame, pdf_name='')
         
         # cal PK2PK values of magnitude and phase 
-        PK2PK = self.calc_mag_phase_pkpk_values(data_frame)
+        PK2PK = self.calc_mag_phase_pkpk_values(postplot_data_frame)
 
         self.edit_annotation_in_plot(annotate_string=PK2PK)
         
@@ -141,7 +144,7 @@ if __name__ == '__main__':
     
     time_unit = 'min'
     
-    storepath = Results_Path + '\\PostPlots'
+    storepath = os.path.join(Results_Path, 'PostPlots')
     
     # search all csv files in results folder 
     csv_file_list = list(Path(Results_Path).glob("**/*.csv"))
@@ -157,7 +160,7 @@ if __name__ == '__main__':
     
     trace_selection = ""
     
-    plot_obj = PostPlot(trace_subplot5= trace_selection)
+    plot_obj = PostPlot(trace_subplot5=trace_selection)
     
     # empty data frame for concat the data frames from csv import to plot full transition
     concat_data_frame = pd.DataFrame()
@@ -169,7 +172,7 @@ if __name__ == '__main__':
         title = csv_file.name
         
         # concatenate data frames for plotting full transition data
-        concat_data_frame = pd.concat([concat_data_frame,data_frame], ignore_index=True, sort=False)
+        concat_data_frame = pd.concat([concat_data_frame, data_frame], ignore_index=True, sort=False)
 
         plot_obj.plot_frame_data(data_frame, title, time_unit)
 
diff --git a/Python_script/VNA.py b/Python_script/VNA.py
index 35d240c..f85fe26 100755
--- a/Python_script/VNA.py
+++ b/Python_script/VNA.py
@@ -196,9 +196,7 @@ class Vna:
         self.vna.write("SYST:PRES; *OPC?")
         self.vna.read()
 
-    #Hack: put in the frequency which is configured in the config file to guess if loading was
-    #successful. FIXME: Do proper error checking here
-    def load_config(self, config_file, frequency):
+    def load_config(self, config_file):
         """
         Load the config from the VNA's internal drive. It must be in the folder
         C:\\Users\\Public\\Documents\\Rohde-Schwarz\\ZNA\\RecallSets\\
diff --git a/Python_script/VNA_dummy.py b/Python_script/VNA_dummy.py
index ab2e317..1429a03 100644
--- a/Python_script/VNA_dummy.py
+++ b/Python_script/VNA_dummy.py
@@ -23,17 +23,17 @@ class VnaDummy:
         self.simulated_traces = [('Trc1', 's11')]
         self.result_format = 'SDAT'
         self.simulated_power = '10.dBm'
-        self.simulated_frequency = '10.MHz'
+        self.simulated_frequency = 1000000
         self.simulated_magnitude_difference_temp = 0.0
         self.simulated_phase_difference_temp = 0.0
         self.simulated_magnitude_difference_hum = 0.0
         self.simulated_phase_difference_hum = 0.0
         self.simulated_state = shared_simulated_state.get_simulated_state()
         self.last_simulated_time = self.simulated_state.simulated_time
-        self.tau_mag_temp = 25     # change rate of magnitude in dependence on temperature
-        self.tau_phase_temp = 35   # change rate of phase in dependence on temperature
-        self.tau_mag_hum = 250     # change rate of magnitude in dependence on humidity
-        self.tau_phase_hum = 350   # change rate of phase in dependence on humidity
+        self.tau_mag_temp = 25  # change rate of magnitude in dependence on temperature
+        self.tau_phase_temp = 35  # change rate of phase in dependence on temperature
+        self.tau_mag_hum = 250  # change rate of magnitude in dependence on humidity
+        self.tau_phase_hum = 350  # change rate of phase in dependence on humidity
         self.reference_magnitude = 0.7  # at reference temp and reference hum
         self.reference_phase = 70  # at reference temp and reference hum
         self.reference_temp = 25
@@ -65,9 +65,8 @@ class VnaDummy:
                                                    delta_magnitude_temp * \
                                                    (1 - (math.exp(-1. * delta_time / self.tau_mag_temp)))
         self.simulated_magnitude_difference_hum = self.simulated_magnitude_difference_hum + \
-                                                   delta_magnitude_hum * \
-                                                   (1 - (math.exp(-1. * delta_time / self.tau_mag_hum)))
-
+                                                  delta_magnitude_hum * \
+                                                  (1 - (math.exp(-1. * delta_time / self.tau_mag_hum)))
 
     def simulate_phase(self):
 
@@ -75,17 +74,17 @@ class VnaDummy:
                                        (self.simulated_state.simulated_temperature - self.reference_temp)
         delta_phase_temp = target_phase_difference_temp - self.simulated_phase_difference_temp
         target_phase_difference_hum = self.phase_slope_hum * \
-                                       (self.simulated_state.simulated_humidity - self.reference_hum)
+                                      (self.simulated_state.simulated_humidity - self.reference_hum)
         delta_phase_hum = target_phase_difference_hum - self.simulated_phase_difference_hum
 
         delta_time = self.simulated_state.simulated_time - self.last_simulated_time
 
         self.simulated_phase_difference_temp = self.simulated_phase_difference_temp + \
-                                               delta_phase_temp *\
+                                               delta_phase_temp * \
                                                (1 - (math.exp(-1. * delta_time / self.tau_phase_temp)))
         self.simulated_phase_difference_hum = self.simulated_phase_difference_hum + \
-                                               delta_phase_hum *\
-                                               (1 - (math.exp(-1. * delta_time / self.tau_phase_hum)))
+                                              delta_phase_hum * \
+                                              (1 - (math.exp(-1. * delta_time / self.tau_phase_hum)))
 
     def get_tupple_of_current_traces(self):
         """
@@ -108,7 +107,9 @@ class VnaDummy:
         self.simulated_power = power
 
     def get_current_cw_frequency(self):
-        return self.simulated_frequency
+        # Fixme: This returns a string, just like the real VNA. Needs to be fixed in both. We keep this for
+        # consistency (although the real thing adapt the unit to orders of magnitude (1.MHz instead of 1000000.Hz
+        return str(self.simulated_frequency) + 'Hz'
 
     def set_cw_frequency(self, frequency):
         self.simulated_frequency = frequency
@@ -123,10 +124,12 @@ class VnaDummy:
             raise Exception('trace ' + trace + ' is not known to VNA dummy!')
         self.result_format = result_format
 
-        simulated_magnitude = self.reference_magnitude + self.simulated_magnitude_difference_temp + \
-            self.simulated_magnitude_difference_hum
-        simulated_phase = self.reference_phase + self.simulated_phase_difference_temp + \
-            self.simulated_phase_difference_hum
+        simulated_magnitude = (self.reference_magnitude +
+                               (self.simulated_magnitude_difference_temp + self.simulated_magnitude_difference_hum)
+                               * 1.3e9 / self.simulated_frequency)
+        simulated_phase = (self.reference_phase +
+                           (self.simulated_phase_difference_temp + self.simulated_phase_difference_hum) *
+                           self.simulated_frequency / 1.3e9)
         phase_radian = simulated_phase / 180. * math.pi
         real_val = simulated_magnitude * math.cos(phase_radian)
         imaginary_val = simulated_magnitude * math.sin(phase_radian)
@@ -148,7 +151,7 @@ class VnaDummy:
                 result.append(float(x))
         return result
 
-    def load_config(self, config_file, frequency):
+    def load_config(self, config_file):
         pass
 
     def do_single_sweep(self):
diff --git a/Python_script/analysis.py b/Python_script/analysis.py
index fed48b8..1bfdd9a 100644
--- a/Python_script/analysis.py
+++ b/Python_script/analysis.py
@@ -1,28 +1,29 @@
 import pandas as pd
 import matplotlib.pyplot as plt
-import math
+import numpy as np
+from matplotlib import gridspec
 
-def extract_stable_data(datafile):
+def extract_stable_data(datafile, measurement_set):
     datapoint = {}
     # df is a pandas data frame
     df = pd.read_csv(datafile)
 
     # extract phase mean and variance for stable measurements (don't ask what loc means)
-    phases = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'S21_PHASE']
+    phases = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'S21_PHASE']
     if phases.size == 0:
         return None
     datapoint['phase_mean']=phases.mean()
     datapoint['phase_var']=phases.var()
 
-    magnitudes = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'S21_MAGNITUDE']
+    magnitudes = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'S21_MAGNITUDE']
     datapoint['magnitude_mean'] = magnitudes.mean()
     datapoint['magnitude_var'] = magnitudes.var()
 
-    temperatures = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'READBACK_TEMPERATURE']
+    temperatures = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'READBACK_TEMPERATURE']
     datapoint['temperature_mean'] = temperatures.mean()
     datapoint['temperature_var'] = temperatures.var()
 
-    humidities = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'READBACK_HUMIDITY']
+    humidities = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'READBACK_HUMIDITY']
     datapoint['humidity_mean'] = humidities.mean()
     datapoint['humidity_var'] = humidities.var()
 
@@ -30,55 +31,76 @@ def extract_stable_data(datafile):
 
 
 # sweep_type is either 'temperature' or 'humidity'
-def plot_sweep(temperatures, humidities, basename, sweep_type):
-    x_data = []
-    phases = []
-    phase_vars = []
-    magnitudes = []
-    magnitude_vars = []
+def plot_sweep(temperatures, humidities, basename, sweep_type, measurement_sets):
+    set_data = {}
+    for measurement_set in measurement_sets:
+        set_data[measurement_set] = {'phases': [], 'phase_vars': [], 'magnitudes': [], 'magnitude_vars': [],
+                                     'x_data': []}
+
     for temp, hum in zip(temperatures, humidities):
         datafile = basename+'_'+str(temp)+'deg_'+str(hum)+'rh.csv'
         print(datafile)
-        datapoint = extract_stable_data(datafile)
-        if datapoint is None:
-            continue
-
-        if sweep_type == 'temperature':
-            x_data.append(datapoint['temperature_mean'])
-        elif sweep_type == 'humidity':
-            x_data.append(datapoint['humidity_mean'])
-        else:
-            raise Exception('Unknown sweep_type:'+str(sweep_type))
-
-        phases.append(datapoint['phase_mean'])
-        phase_vars.append(datapoint['phase_var'])
-        magnitudes.append(datapoint['magnitude_mean'])
-        magnitude_vars.append(datapoint['magnitude_var'])
-
-    fig, ax1 = plt.subplots()
+        for measurement_set in measurement_sets:
+            datapoint = extract_stable_data(datafile, measurement_set)
+            if datapoint is None:
+                continue
+
+            data = set_data[measurement_set]
+            if sweep_type == 'temperature':
+                data['x_data'].append(datapoint['temperature_mean'])
+            elif sweep_type == 'humidity':
+                data['x_data'].append(datapoint['humidity_mean'])
+            else:
+                raise Exception('Unknown sweep_type:'+str(sweep_type))
+
+            data['phases'].append(datapoint['phase_mean'])
+            data['phase_vars'].append(datapoint['phase_var'])
+            data['magnitudes'].append(datapoint['magnitude_mean'])
+            data['magnitude_vars'].append(datapoint['magnitude_var'])
+
+    #fig = plt.figure()
+    #gs = fig.add_gridspec(2, 1, hspace=0)
+    #(ax1, ax2) = gs.subplots(sharex=True)
+    fig, (ax1, ax2) = plt.subplots(2, sharex=True, gridspec_kw={'hspace': 0}, figsize=(10, 5))
+    ax1.tick_params(bottom=True, top=True, direction='in')
+    ax2.tick_params(bottom=True, top=True, direction='inout')
 
     if sweep_type == 'temperature':
-        plt.title(basename + ': Temperature sweep at ' + str(humidities[0]) + ' % r.h.')
-        ax1.set_xlabel('temperature [deg C]')
+        fig.suptitle(basename + ': Temperature sweep at ' + str(humidities[0]) + ' % r.h.')
+        ax2.set_xlabel('temperature [deg C]')
     elif sweep_type == 'humidity':
-        plt.title(basename + ': Humidity sweep at ' + str(temperatures[0]) + ' deg C')
-        ax1.set_xlabel('relative humidity [%]')
+        fig.suptitle(basename + ': Humidity sweep at ' + str(temperatures[0]) + ' deg C')
+        ax2.set_xlabel('relative humidity [%]')
     else:
         raise Exception('Unknown sweep_type:'+str(sweep_type))
 
-    ax1.errorbar(x_data, phases, phase_vars, marker='+', linewidth=0)
-    ax1.set_ylabel('S21 phase [deg]', color='blue')
+    for measurement_set in measurement_sets:
+        ax1.errorbar(set_data[measurement_set]['x_data'],
+                     set_data[measurement_set]['phases'], set_data[measurement_set]['phase_vars'],
+                     label=measurement_set, marker='+', linewidth=0)
+    ax1.set_ylabel('S21 phase [deg]')
 
-    ax2 = ax1.twinx()
-    ax2.errorbar(x_data, magnitudes, magnitude_vars, marker='x', color='red', linewidth=0)
-    ax2.set_ylabel('S21 magnitude [dB]', color='red')
-    
-    fig.tight_layout()  # otherwise the right y-label is slightly clipped
+    for measurement_set in measurement_sets:
+        ax2.errorbar(set_data[measurement_set]['x_data'],
+                     set_data[measurement_set]['magnitudes'], set_data[measurement_set]['magnitude_vars'],
+                     label=measurement_set, marker='+', linewidth=0)
+    ax2.set_ylabel('S21 magnitude [dB]')
+
+    # Remove the highest tick label from the lower subplot. It probably overlaps with the lowest one of the upper plot.
+    yticks = ax2.yaxis.get_major_ticks()
+    yticks[-1].set_visible(False)
+    yticks[-2].label1.set_visible(False)  # don't ask me why we have to set the last two tick marks to invisible
+    ax2.legend(loc='center right', bbox_to_anchor=(1.5, 1.0))
+
+    fig.tight_layout()  # otherwise the legend is clipped
+    plt.subplots_adjust(top=0.9)  # avoid overlap with suptitle
 
-    fig.savefig(basename+'_analysis.pdf')
+    fig.savefig(basename+measurement_set+'_analysis.pdf')
     
     plt.show()
 
 
 if __name__ == '__main__':
-   plot_sweep(range(20, 30+1), [40]*11, 'tempsweep1', 'temperature')
+    print('run \'./prototype.py -t first_tempsweep.txt -p -o tempsweep1\' to get the data needed for this plot.' )
+    plot_sweep(np.arange(25., 31.+1.), [35.]*7, 'tempsweep1', 'temperature',
+               ['1.3GHz', '1.0GHz', '3.0GHz', '6.0GHz', '10.0GHz'])
diff --git a/Python_script/climate-lab-gui.py b/Python_script/climate-lab-gui.py
index b77c3da..3f58f3e 100755
--- a/Python_script/climate-lab-gui.py
+++ b/Python_script/climate-lab-gui.py
@@ -85,9 +85,10 @@ class TestStandMainWindow(QMainWindow):
                 for t in temperatures:
                     temp_extensions.append(str(t) + 'deg_' + str(self.fixedParameter.value()) + 'rh')
                 analysis.plot_sweep(temperatures, [self.fixedParameter.value()] * len(temperatures), output_basename,
-                                    'temperature')
+                                    'temperature', meas.dut.get_measurement_set_names())
                 
                 prototype.plot_output(output_basename, temp_extensions, True, config_data, ext_sensor_channels,
+                                      meas.dut.get_measurement_set_names(),
                                       output_basename + ': Temperature sweep ' + str(temperatures[0]) + '--' +
                                       str(temperatures[-1]) + ' degC @ ' + str(self.fixedParameter.value()) + ' % r.h.')
 
@@ -100,9 +101,10 @@ class TestStandMainWindow(QMainWindow):
                 for h in humidities:
                     hum_extensions.append(str(self.fixedParameter.value()) + 'deg_' + str(h) + 'rh')
                 analysis.plot_sweep([self.fixedParameter.value()] * len(humidities), humidities, output_basename,
-                                    'humidity')
+                                    'humidity', meas.dut.get_measurement_set_names())
                                 
                 prototype.plot_output(output_basename, hum_extensions, True, config_data, ext_sensor_channels,
+                                      meas.dut.get_measurement_set_names(),
                                       output_basename + ': Humidity sweep ' + str(humidities[0]) + '--' +
                                       str(humidities[-1]) + ' % r.h. @ ' + str(self.fixedParameter.value()) + ' degC')
 
@@ -110,7 +112,8 @@ class TestStandMainWindow(QMainWindow):
                 try:
                     n_measurements = meas.perform_measurements(os.path.join(self.start_dir,
                                                                             self.measurementFile.text()))
-                    prototype.plot_output(output_basename, range(n_measurements), True, ext_sensor_channels,
+                    prototype.plot_output(output_basename, range(n_measurements), True, config_data, ext_sensor_channels,
+                                          meas.dut.get_measurement_set_names(),
                                           output_basename)
                 except FileNotFoundError as e:
                     QtWidgets.QMessageBox.warning(self, 'Warning', str(e))
diff --git a/Python_script/dut_measurement.py b/Python_script/dut_measurement.py
index bffb95d..b2f2171 100644
--- a/Python_script/dut_measurement.py
+++ b/Python_script/dut_measurement.py
@@ -6,9 +6,10 @@ class DutMeasurement(ABC):
     Interface for DUT measurements
     """
     @abstractmethod
-    def get_dut_measurements(self):
+    def get_dut_measurements(self, set_name):
         """
-        Returns a dictionary with column names a keys and scalar values
+        Performs the measurements according to the set name and
+        returns a dictionary with signal names a keys and scalar values
         """
         pass
 
@@ -31,3 +32,10 @@ class DutMeasurement(ABC):
         """
         Return the maximum deltas of the reference signals
         """
+
+    @abstractmethod
+    def get_measurement_set_names(self):
+        """
+        Names /  string identifiers of the different measurement sets which are performed at each temperature/humidity
+        """
+        pass
diff --git a/Python_script/first_tempsweep.txt b/Python_script/first_tempsweep.txt
index a9dfacd..9dc362c 100644
--- a/Python_script/first_tempsweep.txt
+++ b/Python_script/first_tempsweep.txt
@@ -1,2 +1,2 @@
-25.1 35.1 2.5 35 300 30
+25 31 1 35 100 10
 
diff --git a/Python_script/prototype.py b/Python_script/prototype.py
index 2b708cd..13354cd 100755
--- a/Python_script/prototype.py
+++ b/Python_script/prototype.py
@@ -81,7 +81,7 @@ class Measurements:
         self.postplot_obj = None
         
         self.measurement_plot = MeasurementPlot.MeasurementPlot(trace_subplot5=config_data['trace_subplot5'])
-        self.data_collection = []
+        self.data_collection_for_online_plot = []
 
     def perform_measurements(self, sweep_file):
         with open(sweep_file) as file:
@@ -155,6 +155,11 @@ class Measurements:
         return sweep_values
 
     def perform_single_measurement(self, output, target_temp, target_hum, soaking_time, n_stable_reads):
+        """
+        A "single measurement refers to a single temperature and humidity setting.
+        This is a "chamber measurement point". It consists out of multiple measurements sets. The data for all
+        measurement sets of this chamber point are taken for this measurement.
+        """
         with open(output, mode='w', newline='') as csv_file:
             fieldnames = ['TIMESTAMP', 'TARGET_TEMPERATURE', 'READBACK_TEMPERATURE', 'TARGET_HUMIDITY',
                           'READBACK_HUMIDITY', 'DUT_IDENTIFIER', 'RUN_ID', 'EQUILIBRIUM_INDICATOR', 'TEMP_HEATER',
@@ -165,7 +170,7 @@ class Measurements:
             # csv.dict writer add adda row wise
             writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
             writer.writeheader()
-            self.data_collection = []
+            self.data_collection_for_online_plot = []
             plt.ion()
 
             set_const_response = self.chamber.set_const((target_temp, target_hum))
@@ -184,7 +189,9 @@ class Measurements:
                 dut_signal_queues = [[], []]
 
                 while True:
-                    data = self.read_data()
+                    # Only read the climate chamber data once. The equilibrium indicator is only calculated on the
+                    # primary data set.
+                    data = self.read_data(self.dut.get_measurement_set_names()[0])
                     # if it is not within the target range reset start time
                     self.temperature_stable = False
                     self.humidity_stable = False
@@ -214,7 +221,13 @@ class Measurements:
                           ' °C' + ' | Humid: ' + data.hum + '%'
                           + ' | 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])
+                    writer.writerow(self.data_collection_for_online_plot[-1])
+
+                    # read and store data for all further sets
+                    for set_name in self.dut.get_measurement_set_names()[1:]:
+                        data.dut_data = self.dut.get_dut_measurements(set_name)
+                        writer.writerow(self.create_data_dictionary(target_temp, target_hum, data,
+                                                                    self.cook_up_equi_indicator()))
 
                     if self.temperature_stable and self.humidity_stable and self.dut_signals_stable[0] and\
                             self.dut_signals_stable[1]:
@@ -229,22 +242,33 @@ class Measurements:
 
                 # perform the configured number of measurements and check that they are really stable
                 # It started running after everything become stable
-                supposedly_stable_measurements = []
+                supposedly_stable_measurements = {}
+                for set_name in self.dut.get_measurement_set_names():
+                    supposedly_stable_measurements[set_name] = []
                 all_measurements_stable = True
+
                 for i in range(0, n_stable_reads):
-                    data = self.read_data()
+                    # create the main data set for the first measurement set
+                    data = self.read_data(self.dut.get_measurement_set_names()[0])
                     self.temperature_stable = self.calculate_temperature_stability(target_temp, float(data.temp))
                     self.humidity_stable = self.calculate_humidity_stability(target_hum, float(data.hum))
                     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)
+                            (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])
+                    supposedly_stable_measurements[self.dut.get_measurement_set_names()[0]].append(
+                        self.data_collection_for_online_plot[-1])
+
+                    # read and store data for all further sets
+                    for set_name in self.dut.get_measurement_set_names()[1:]:
+                        data.dut_data = self.dut.get_dut_measurements(set_name)
+                        supposedly_stable_measurements[set_name].append(
+                            self.create_data_dictionary(target_temp, target_hum, data, self.cook_up_equi_indicator()))
                                 
                     if this_measurement_stable:
                         print('Stable measurement ' + str(i+1) + '/' + str(n_stable_reads))
@@ -255,25 +279,26 @@ class Measurements:
                         print('Measurement not stable. Retrying.')
                         break
 
-                for measurement in supposedly_stable_measurements:
-                    if all_measurements_stable:
-                        measurement['EQUILIBRIUM_INDICATOR'] = TEMPERATURE_STABLE | HUMIDITY_STABLE | \
-                                                               DUT_SIGNAL0_STABLE | DUT_SIGNAL1_STABLE | \
-                                                               MEASUREMENT_STABLE
-                        do_another_measurement = False
+                for set_name in self.dut.get_measurement_set_names():
+                    for measurement in supposedly_stable_measurements[set_name]:
+                        if all_measurements_stable:
+                            measurement['EQUILIBRIUM_INDICATOR'] = TEMPERATURE_STABLE | HUMIDITY_STABLE | \
+                                                                   DUT_SIGNAL0_STABLE | DUT_SIGNAL1_STABLE | \
+                                                                   MEASUREMENT_STABLE
+                            do_another_measurement = False
 
-                    writer.writerow(measurement)
+                        writer.writerow(measurement)
 
     def sleep_until(self, wakeup_time):
         remaining_sleep_time = wakeup_time - self.clock.time()
         if remaining_sleep_time > 0:
             self.clock.sleep(remaining_sleep_time)
 
-    def read_data(self):
+    def read_data(self, measurement_set_name):
         [temp, hum, mode, alarms] = self.chamber.read_monitor().split(',')
         perc_temp_heater, perc_hum_heater = self.chamber.get_heater_percentage()
 
-        dut_data = self.dut.get_dut_measurements()
+        dut_data = self.dut.get_dut_measurements(measurement_set_name)
 
         temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room = \
             self.ext_sensors.get_sensor_values()
@@ -286,7 +311,10 @@ class Measurements:
                                hum_dut, hum_room, hum_meas_instr, air_press_room, temp_chamber_meas_instr,
                                hum_chamber_meas_instr)
 
-    def store_and_plot_data(self, target_temp, target_hum, data, equi_indicator):
+    def create_data_dictionary(self, target_temp, target_hum, data, equi_indicator):
+        """
+        Create a dictionary out of the  MeasurementData object and some extra data, as needed by the writer
+        """
         measurement = {
             'TIMESTAMP': data.timestamp,
             'TARGET_TEMPERATURE': target_temp,
@@ -295,7 +323,7 @@ class Measurements:
             'READBACK_HUMIDITY': float(data.hum),
             'EQUILIBRIUM_INDICATOR': equi_indicator,
             'TEMP_HEATER': data.percent_temp_heater,
-             'HUM_HEATER': data.percent_hum_heater,
+            'HUM_HEATER': data.percent_hum_heater,
             'TEMP_DUT': data.temp_dut,
             'TEMP_ROOM': data.temp_room,
             'TEMP_MEAS_INSTR': data.temp_meas_instr,
@@ -307,8 +335,12 @@ class Measurements:
             'READBACK_HUM_MEAS_INSTR': float(data.hum_chamber_meas_instr),
         }
         measurement.update(data.dut_data)
-        self.data_collection.append(measurement)
-        data_frame = pd.DataFrame(self.data_collection)
+        return measurement
+
+    def store_and_plot_data(self, target_temp, target_hum, data, equi_indicator):
+        measurement = self.create_data_dictionary(target_temp, target_hum, data, equi_indicator)
+        self.data_collection_for_online_plot.append(measurement)
+        data_frame = pd.DataFrame(self.data_collection_for_online_plot)
         self.measurement_plot.draw(data_frame)
 
     def current_milli_time(self):
@@ -340,7 +372,8 @@ class Measurements:
                (float(readback_hum) <= target_hum+self.max_delta_hum)
 
 
-def plot_output(output_basename, measurements_appendices, show_blocking_plot, config_data, ext_sens_data, title = ''):
+def plot_output(output_basename, measurements_appendices, show_blocking_plot, config_data, ext_sens_data,
+                measurement_set_names, title=''):
     
     time_unit = str(config_data['time_unit'])
 
@@ -352,13 +385,15 @@ def plot_output(output_basename, measurements_appendices, show_blocking_plot, co
         measurement_name = output_basename+'_'+str(m)
         list_of_frames.append(pd.read_csv(measurement_name+'.csv'))
 
-        post_plot.plot_frame_data(list_of_frames[-1], title, time_unit,)
-        post_plot.save_fig(storepath, measurement_name+'.pdf')
+        for set_name in measurement_set_names:
+            post_plot.plot_frame_data(list_of_frames[-1], title+set_name, time_unit, set_name)
+            post_plot.save_fig(storepath, measurement_name+set_name+'.pdf')
 
     combined_data_frame = pd.concat(list_of_frames, ignore_index=True, sort=False)
     
-    post_plot.plot_frame_data(combined_data_frame, title, time_unit)
-    post_plot.save_fig(storepath, 'FullTransistion.pdf')
+    for set_name in measurement_set_names:
+        post_plot.plot_frame_data(combined_data_frame, title+set_name, time_unit, set_name)
+        post_plot.save_fig(storepath, output_basename+set_name + '.pdf')
     
     if show_blocking_plot:
         plt.ioff()
@@ -431,18 +466,19 @@ if __name__ == '__main__':
         if args.file:
             n_measurements = mes.perform_measurements(args.file)
             plot_output(output_basename, range(n_measurements), args.plot, config_data, ext_sensor_channels,
-                        output_basename)
+                        mes.dut.get_measurement_set_names(), output_basename)
         if args.temperaturesweepfile:
             temperatures, humidity = run_temperature_sweep_from_file(args.temperaturesweepfile, mes)
             # run analysis here
             temp_extensions = []
             for t in temperatures:
                 temp_extensions.append(str(t)+'deg_'+str(humidity)+'rh')
-            analysis.plot_sweep(temperatures, [humidity]*len(temperatures), output_basename, 'temperature')
+            analysis.plot_sweep(temperatures, [humidity]*len(temperatures), output_basename, 'temperature',
+                                mes.dut.get_measurement_set_names())
             plot_output(output_basename, temp_extensions, args.plot, config_data, ext_sensor_channels,
+                        mes.dut.get_measurement_set_names(),
                         output_basename + ': Temperature sweep ' +
                         str(temperatures[0]) + ' degC to ' + str(temperatures[-1]) + ' degC')
-            print(str(temp_extensions))
     finally:
         mes.chamber.close()
         mes.ext_sensors.close()
diff --git a/Python_script/test_stand_parameter.json b/Python_script/test_stand_parameter.json
index 11ddfeb..56a0a24 100644
--- a/Python_script/test_stand_parameter.json
+++ b/Python_script/test_stand_parameter.json
@@ -5,7 +5,7 @@
     "type": "VNA",
     "delta_mag": 0.13,
     "delta_phase": 1.5,
-    "frequency": 1300000000,
+    "frequencies": [1300000000, 1000000000, 3000000000, 6000000000, 10000000000],
     "vna_ip": "mskzna43-lab",
     "vna_config_file": "CalSetup2.znxml"
   },
diff --git a/Python_script/vna_measurement.py b/Python_script/vna_measurement.py
index 8185049..f6be599 100644
--- a/Python_script/vna_measurement.py
+++ b/Python_script/vna_measurement.py
@@ -7,15 +7,6 @@ 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):
@@ -24,21 +15,26 @@ class VnaMeasurement(dut_measurement.DutMeasurement):
 
         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.load_config(config_data['vna_config_file'])
         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.frequencies = {}
+        for f in config_data['frequencies']:
+            self.frequencies[str(f / 1e9) + 'GHz'] = f
+
     def _get_trace_data(self, trace):
         return self.vna.get_list_of_measurement_values(trace, "SDAT")
 
-    def get_dut_measurements(self):
+    def get_dut_measurements(self, set_name):
         # 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()
+                frequency = self.frequencies[set_name]
+                self.vna.set_cw_frequency(frequency)
                 self.vna.do_single_sweep()
                 s11 = self._get_trace_data("Trace1")
                 s12 = self._get_trace_data("Trace2")
@@ -53,7 +49,8 @@ class VnaMeasurement(dut_measurement.DutMeasurement):
                         '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)}
+                        'S22_PHASE': self.calculate_mean_phase(s21),
+                        'SET_NAME': set_name}
 
             # exception for pyvisa error or timeout
             except pyvisa.errors.VisaIOError:
@@ -68,7 +65,7 @@ class VnaMeasurement(dut_measurement.DutMeasurement):
 
     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']
+                'S12_PHASE', 'S21_MAGNITUDE', 'S21_PHASE', 'S22_MAGNITUDE', 'S22_PHASE', 'SET_NAME']
 
     def get_dut_reference_signal_names(self):
         return ['S21_MAGNITUDE', 'S21_PHASE']
@@ -104,3 +101,6 @@ class VnaMeasurement(dut_measurement.DutMeasurement):
     def calculate_mean_phase(self, values_list):
         phases = self.calculate_phases(values_list)
         return numpy.mean(phases)
+
+    def get_measurement_set_names(self):
+        return list(self.frequencies.keys())
-- 
GitLab