diff --git a/.gitignore b/.gitignore index f98d02aa85401bc0d782c52d341d614609c45198..a1cdda760b0429734357b4670254935dc2dfceb1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ *.csv *graph.pdf *analysis.pdf +Python_script/measurements/* +Python_script/PostPlots/* __pycache__ diff --git a/Python_script/Equipment_Manuals/VNA/ZNA_user_manual.pdf b/Python_script/Equipment_Manuals/VNA/ZNA_user_manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ddcde3e944607597f55a775ddbd95236c08fc839 Binary files /dev/null and b/Python_script/Equipment_Manuals/VNA/ZNA_user_manual.pdf differ diff --git a/Python_script/Equipment_Manuals/datalogger_env_sensors/cross_sectional_manual_almemo.pdf b/Python_script/Equipment_Manuals/datalogger_env_sensors/cross_sectional_manual_almemo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ccde5bfc22e556776805c4677b08ce1c4e4a8c53 Binary files /dev/null and b/Python_script/Equipment_Manuals/datalogger_env_sensors/cross_sectional_manual_almemo.pdf differ diff --git a/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D6_sensors.pdf b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D6_sensors.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5bec921c00647a76082f02ad28f64ade4d6da561 Binary files /dev/null and b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D6_sensors.pdf differ diff --git a/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D7_sensors.pdf b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D7_sensors.pdf new file mode 100644 index 0000000000000000000000000000000000000000..279ca7cd0cf3a7fd93f1681f05f2d405430b315f Binary files /dev/null and b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D7_sensors.pdf differ diff --git a/Python_script/Equipment_Manuals/env_test_chamber/instrument_manual_PR3J.pdf b/Python_script/Equipment_Manuals/env_test_chamber/instrument_manual_PR3J.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c5873e8bbf907090e49924ae0e40f2df602d7d3b Binary files /dev/null and b/Python_script/Equipment_Manuals/env_test_chamber/instrument_manual_PR3J.pdf differ diff --git a/Python_script/Equipment_Manuals/env_test_chamber/remote_control__manual_PR3J.pdf b/Python_script/Equipment_Manuals/env_test_chamber/remote_control__manual_PR3J.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ca4552ae7e188e2db819086995464e1e6b2c5ab0 Binary files /dev/null and b/Python_script/Equipment_Manuals/env_test_chamber/remote_control__manual_PR3J.pdf differ diff --git a/Python_script/Equipment_Manuals/env_test_chamber/user_manual_PR3J.pdf b/Python_script/Equipment_Manuals/env_test_chamber/user_manual_PR3J.pdf new file mode 100644 index 0000000000000000000000000000000000000000..46f3e9bfa7533b63831287b30a99b6bcab257eeb Binary files /dev/null and b/Python_script/Equipment_Manuals/env_test_chamber/user_manual_PR3J.pdf differ diff --git a/Python_script/MeasurementPlot.py b/Python_script/MeasurementPlot.py index 5e72951d41eba35fd6d9ac60f3255804bbd10745..e61a11be646c48cbc4b5af34a47cfeb5b3c16dea 100644 --- a/Python_script/MeasurementPlot.py +++ b/Python_script/MeasurementPlot.py @@ -3,6 +3,7 @@ import matplotlib.pyplot as plt import numpy as np import time import queue +import sys # Different exceptions can be thrown while plotting, depending on the backend. # We catch them all locally and raise our own exception instead @@ -11,48 +12,137 @@ class PlottingError(Exception): pass class MeasurementPlot: - def __init__(self, title=''): - self.fig, self.ax1 = plt.subplots(2, figsize=(12, 10)) + def __init__(self, reference_signal_names, title='', trace_subplot5 ='', legend_loc = 'upper left',\ + legend_bbox_to_anchor = (1.09, 1)): + self.reference_signal_names = reference_signal_names + + # set python for opening an separate plot window when starting from anaconda + if 'ipykernel' in sys.modules: + from IPython import get_ipython + get_ipython().run_line_magic('matplotlib', 'qt') + + self.trace_subplot5 = trace_subplot5 + + # parameter for legend of subplots + self.legend_loc = legend_loc + self.legend_bbox_to_anchor = legend_bbox_to_anchor + + # Prepare for five subplots + self.fig, self.ax1 = plt.subplots(5, figsize=(25, 20)) + self.fig.subplots_adjust(bottom= 0.1, right=0.8, hspace = 0.4) self.fig.suptitle("Measurement "+title, color="red") - # First plot: Phase and magnitude - self.path_collection_phase = self.ax1[0].scatter([], [], c='red', marker='<', label='Phase') - self.magnitude_axis = self.ax1[0].twinx() - self.path_collection_mag = self.magnitude_axis.scatter([], [], c='#3120E0', marker='4', label='Magnitude') + # First plot: signal0 and signal1 + self.path_collection_signal0 = self.ax1[0].scatter([], [], c='red', marker='<', label=reference_signal_names[0]) + self.path_collection_fit = self.ax1[0].scatter([], [], c='green', marker='.', label = ' ') + self.signal1_axis = self.ax1[0].twinx() + + self.path_collection_signal1 = self.signal1_axis.scatter([], [], c='#3120E0', marker='4', label=reference_signal_names[1]) self.equi_axis0 = self.ax1[0].twinx() - self.equi_axis0.spines['right'].set_position(('outward', 40)) + self.equi_axis0.spines['right'].set_position(('outward', 75)) self.path_collection_equi0 = self.equi_axis0.scatter([], [], c='black', marker=".", label='Equilibrium_Indicator') self.ax1[0].set_xlabel("TIMESTAMP") - self.ax1[0].set_ylabel("PHASE", color='red') - self.magnitude_axis.set_ylabel("MAGNITUDE", color='#3120E0') - self.equi_axis0.set_ylabel("EQUILIBRIUM_INDICATOR", color='black') + self.ax1[0].set_ylabel(reference_signal_names[0], color='red') + self.signal1_axis.set_ylabel(reference_signal_names[1], color='#3120E0') + self.equi_axis0.set_ylabel("INDICATOR VALUE", color='black') # fix range to 0..31 with some extra margin for plotting self.equi_axis0.set_ylim(-1, 32) self.ax1[0].grid(True, linestyle=":") - all_path_collections = [self.path_collection_phase, self.path_collection_mag, self.path_collection_equi0] + all_path_collections = [self.path_collection_signal0, self.path_collection_signal1, + self.path_collection_equi0, self.path_collection_fit] labels = [pc.get_label() for pc in all_path_collections] - self.ax1[0].legend(all_path_collections, labels, loc='lower right') + self.signal0_legend = self.ax1[0].legend(all_path_collections, labels, loc=self.legend_loc, + bbox_to_anchor=self.legend_bbox_to_anchor) + + ax = self.fig.axes + self.annotation = ax[0].annotate('', xy=(0, 1), + xycoords='axes fraction', xytext=(-0.16, 1), + textcoords='axes fraction', fontsize='16', + horizontalalignment='left', verticalalignment='bottom') - # Second plot: Humidity and temperature - self.path_collection_temp = self.ax1[1].scatter([], [], c='blue', marker='p', label="Temperature") + # Second plot: Humidity and temperature of climate chamber requested from internal sensors of chamber + self.path_collection_temp = self.ax1[1].scatter([], [], c='blue', marker='p', label="Chamber Temperature") self.humidity_axis = self.ax1[1].twinx() - self.path_collection_hum = self.humidity_axis.scatter([], [], c='green', marker="*", label="Humidity") + self.path_collection_hum = self.humidity_axis.scatter([], [], c='green', marker="*", label="Chamber Humidity") self.equi_axis1 = self.ax1[1].twinx() - self.equi_axis1.spines['right'].set_position(('outward', 40)) - self.path_collection_equi1 = self.equi_axis1.scatter([], [], c='black', marker=".", label="Equilibrium_Indicator") + self.equi_axis1.spines['right'].set_position(('outward', 75)) + self.path_collection_equi1 = self.equi_axis1.scatter([], [], c='black', marker=".", + label="Equilibrium Indicator") self.ax1[1].set_xlabel("TIMESTAMP") - self.ax1[1].set_ylabel("TEMPERATURE ", color='blue') - self.humidity_axis.set_ylabel("HUMIDITY", color='green') - self.equi_axis1.set_ylabel("EQUILIBRIUM_INDICATOR", color='black') + self.ax1[1].set_ylabel("TEMPERATURE [°C] ", color='blue') + self.humidity_axis.set_ylabel("HUMIDITY [%RH]", color='green') + self.equi_axis1.set_ylabel("INDICATOR VALUE", color='black') self.equi_axis1.set_ylim(-1, 32) self.ax1[1].grid(True, linestyle=":") all_path_collections = [self.path_collection_temp, self.path_collection_hum, self.path_collection_equi1] labels = [pc.get_label() for pc in all_path_collections] - self.ax1[1].legend(all_path_collections, labels, loc='lower right') + self.ax1[1].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor) + + # Third plot: parameter of external sensors DUT temperature, DUT humidity + self.path_collection_temp_dut = self.ax1[2].scatter([], [], c='red', marker='p', label='DUT temperature') + + self.ext_sens_hum_axis = self.ax1[2].twinx() + self.path_collection_hum_dut = self.ext_sens_hum_axis.scatter([], [], c='purple', marker='*', + label='DUT humidity') + + + self.ax1[2].set_xlabel("TIMESTAMP") + self.ax1[2].set_ylabel("TEMPERATURE [°C]", color='red') + self.ext_sens_hum_axis.set_ylabel("HUMIDITY [%RH]", color = 'purple') + + self.ax1[2].grid(True, linestyle=":") + all_path_collections = [self.path_collection_temp_dut, self.path_collection_hum_dut] + labels = [pc.get_label() for pc in all_path_collections] + self.ax1[2].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor) + + # Forth plot: parameter of external sensors: room temperature, room humidity , air pressure room + self.path_collection_temp_room = self.ax1[3].scatter([], [], c='green', marker='*', label='room temperature') + + self.sec_ext_hum_sens_axis = self.ax1[3].twinx() + self.path_collection_hum_room = self.sec_ext_hum_sens_axis.scatter([], [], c='orange', marker='>', + label='room humidity') + + self.press_axis = self.ax1[3].twinx() + self.press_axis.spines['right'].set_position(('outward', 60)) + self.path_collection_air_press_room = self.press_axis.scatter([], [], c='grey', marker='4', + label='air pressure room') + + self.ax1[3].set_xlabel("TIMESTAMP") + self.ax1[3].set_ylabel("TEMPERATURE [°C]", color='green') + self.sec_ext_hum_sens_axis.set_ylabel("HUMIDITY [%RH]", color='orange') + self.press_axis.set_ylabel("AIR PRESSURE [mb]", color='grey') + + self.ax1[3].grid(True, linestyle=":") + all_path_collections = [self.path_collection_temp_room, self.path_collection_hum_room, + self.path_collection_air_press_room] + labels = [pc.get_label() for pc in all_path_collections] + self.ax1[3].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor) + + # Fifth plot: parameter of external sensors: meas instruments temperature, meas instruments humidity + subplot_dict = self.config_fifth_subplot() + + self.path_collection_trace_1 = self.ax1[4].scatter([], [], c='black', marker='p', + label=subplot_dict.get('label_trace_1')) + + self.sec_plot_param_axis = self.ax1[4].twinx() + self.path_collection_trace_2 = self.sec_plot_param_axis.scatter([], [], c='brown', marker="*", + label=subplot_dict.get('label_trace_2')) + + self.ax1[4].set_xlabel("TIMESTAMP") + self.ax1[4].set_ylabel(subplot_dict.get('y_axis'), color='black') + self.sec_plot_param_axis.set_ylabel(subplot_dict.get('sec_y_axis'), color='brown') + + self.ax1[4].grid(True, linestyle=":") + all_path_collections = [self.path_collection_trace_1, self.path_collection_trace_2] + + labels = [pc.get_label() for pc in all_path_collections] + self.ax1[4].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor) + + plt.rcParams.update({'font.size': 16}) self.data_queue = queue.Queue(10) @@ -80,26 +170,76 @@ class MeasurementPlot: minimum, maximum = self.get_extended_min_max(timestamps) self.ax1[0].set_xlim(minimum, maximum) self.ax1[1].set_xlim(minimum, maximum) + self.ax1[2].set_xlim(minimum, maximum) + self.ax1[3].set_xlim(minimum, maximum) + self.ax1[4].set_xlim(minimum, maximum) - phases = data_frame.S21_PHASE - minimum, maximum = self.get_extended_min_max(phases) + # refresh data for signal0 in subplot for signal0 and signal1 + signal0 = data_frame[self.reference_signal_names[0]] + minimum, maximum = self.get_extended_min_max(signal0) self.ax1[0].set_ylim(minimum, maximum) - self.path_collection_phase.set_offsets(np.c_[timestamps, phases]) + self.path_collection_signal0.set_offsets(np.c_[timestamps, signal0]) + self.path_collection_fit.set_offsets(np.c_[[], []]) - magnitudes = data_frame.S21_MAGNITUDE - minimum, maximum = self.get_extended_min_max(magnitudes) - self.magnitude_axis.set_ylim(minimum, maximum) - self.path_collection_mag.set_offsets(np.c_[timestamps, magnitudes]) + # refresh data for signal1 in subplot for signal0 and signal1 + signal1s = data_frame[self.reference_signal_names[1]] + minimum, maximum = self.get_extended_min_max(signal1s) + self.signal1_axis.set_ylim(minimum, maximum) + self.path_collection_signal1.set_offsets(np.c_[timestamps, signal1s]) + # refresh data for chamber temperature in subplot for chamber temperature and humidity temperatures = data_frame.READBACK_TEMPERATURE minimum, maximum = self.get_extended_min_max(temperatures) self.ax1[1].set_ylim(minimum, maximum) self.path_collection_temp.set_offsets(np.c_[timestamps, temperatures]) + # refresh data for chamber humidity in subplot for chamber temperature and humidity humidities = data_frame.READBACK_HUMIDITY minimum, maximum = self.get_extended_min_max(humidities) self.humidity_axis.set_ylim(minimum, maximum) self.path_collection_hum.set_offsets(np.c_[timestamps, humidities]) + + # refresh temperatures for used external sensors in subplots + temp_dut = data_frame.TEMP_DUT + temp_room = data_frame.TEMP_ROOM + + minimum, maximum = self.get_extended_min_max(temp_dut) + self.ax1[2].set_ylim(minimum, maximum) + self.path_collection_temp_dut.set_offsets(np.c_[timestamps, temp_dut]) + + minimum, maximum = self.get_extended_min_max(temp_room) + self.ax1[3].set_ylim(minimum, maximum) + self.path_collection_temp_room.set_offsets(np.c_[timestamps, temp_room]) + + # refresh humidities external sensors in subplots for DUT humidity and room humidity + hum_dut = data_frame.HUM_DUT + hum_room = data_frame.HUM_ROOM + + minimum, maximum = self.get_extended_min_max(hum_dut) + self.ext_sens_hum_axis.set_ylim(minimum, maximum) + + minimum, maximum = self.get_extended_min_max(hum_room) + self.sec_ext_hum_sens_axis.set_ylim(minimum, maximum) + self.path_collection_hum_dut.set_offsets(np.c_[timestamps, hum_dut]) + self.path_collection_hum_room.set_offsets(np.c_[timestamps, hum_room]) + + # refresh air pressure of external sensor in subplot for air pressure room + air_press_room = data_frame.AIR_PRESS_ROOM + minimum, maximum = self.get_extended_min_max(air_press_room) + self.press_axis.set_ylim(minimum, maximum) + self.path_collection_air_press_room.set_offsets(np.c_[timestamps, air_press_room]) + + # refresh temperature and humidity of external sensor in subplot for measurement + # for instrument temperature and measurement instrument humidity + val_trace_1, val_trace_2 = self.refresh_param_fifth_subplot(data_frame) + + minimum, maximum = self.get_extended_min_max(val_trace_1) + self.ax1[4].set_ylim(minimum, maximum) + + minimum, maximum = self.get_extended_min_max(val_trace_2) + self.sec_plot_param_axis.set_ylim(minimum, maximum) + self.path_collection_trace_1.set_offsets(np.c_[timestamps, val_trace_1]) + self.path_collection_trace_2.set_offsets(np.c_[timestamps, val_trace_2]) self.path_collection_equi0.set_offsets(np.c_[timestamps, data_frame.EQUILIBRIUM_INDICATOR]) self.path_collection_equi1.set_offsets(np.c_[timestamps, data_frame.EQUILIBRIUM_INDICATOR]) @@ -114,21 +254,78 @@ class MeasurementPlot: self.fig.canvas.flush_events() except Exception as e: raise PlottingError from e + + def config_fifth_subplot(self): + + # key names for config parameter fifth subplot + keys = ["label_trace_1", "label_trace_2", "y_axis", "sec_y_axis"] + + if self.trace_subplot5 == 'logger_sens': + + # values for plotting evnironmental conditions in measurement instrument chamber + values = ["temperature\nalmemo sensors\nmeas instr chamber", + "humidity\nalmemo sensors\nmeas instr chamber", + "TEMPERATURE [°C]", "HUMIDITY [%RH]"] + + elif self.trace_subplot5 == 'chamber_sens': + # values for plotting environmental conditions in measurement instrument chamber + values = ["temperature\nchamber sensors\nmeas instr chamber", + "humidity\nchamber sensors\nmeas instr chamber", + "TEMPERATURE [°C]", "HUMIDITY [%RH]"] + else: + + # values for plotting heater activity in fifth subplot + values = ["Temp Heater\nDUT chamber", "Humidity Heater\n DUT chamber", + "ACTIVITY [%]","ACTIVITY [%]"] + + # generate dictionary from selection + config_subplot_dict = dict(zip(keys, values)) + + return config_subplot_dict + + def refresh_param_fifth_subplot(self, data_frame): + + if self.trace_subplot5 == 'logger_sens': + + # chose sensor values for refreshing fifth subplot + val_trace_1 = data_frame.TEMP_MEAS_INSTR + val_trace_2 = data_frame.HUM_MEAS_INSTR + + elif self.trace_subplot5 == 'chamber_sens': + + val_trace_1 = data_frame.READBACK_TEMP_MEAS_INSTR + val_trace_2 = data_frame.READBACK_HUM_MEAS_INSTR + + else: + + # chose heater values for refreshing fifth plot + val_trace_1 = data_frame.TEMP_HEATER + val_trace_2 = data_frame.HUM_HEATER + + return val_trace_1, val_trace_2 # add 5 % of the distance between min and max to the range @staticmethod def get_extended_min_max(array): distance = array.max() - array.min() if distance == 0.: - distance = 0.5 + distance = 1 return array.min()-0.05*distance, array.max()+0.05*distance + +# test procedure for measurement plot procedure if __name__ == '__main__': - m = MeasurementPlot() + # possible selections trace subplot 5 + # chamber_sens + # logger_sens + # heater_dut_chamber + + m = MeasurementPlot(['S21_PHASE', 'S21_MAGNITUDE'], trace_subplot5="chamber_sens") plt.ion() measurements = [] #FIXME: The loop should run in a separate thread and use draw_in_other_thread +# generation of datapoints for plot for i in range(20): measurement = { 'TIMESTAMP': i, @@ -136,11 +333,27 @@ if __name__ == '__main__': 'READBACK_HUMIDITY': 10 + 0.1*i, 'EQUILIBRIUM_INDICATOR': i % 4, 'S21_PHASE': 20 - 2*i, - 'S21_MAGNITUDE': 0.3*i - } + 'S21_MAGNITUDE': 0.3*i, + + 'TEMP_HEATER': 10, + 'HUM_HEATER': 3, + 'TEMP_DUT': i, + 'TEMP_ROOM': 25-i, + 'HUM_DUT': 40, + 'HUM_ROOM': 45, + 'AIR_PRESS_ROOM': 1000+10*i, + 'TEMP_MEAS_INSTR': 40-1.5*i, + 'HUM_MEAS_INSTR': 55, + + 'READBACK_TEMP_MEAS_INSTR': 50-2*i, + 'READBACK_HUM_MEAS_INSTR': 39 + } + measurements.append(measurement) my_data_frame = pd.DataFrame(measurements) + # plot of data frame with test data for actual step m.draw_in_this_thread(my_data_frame) + # plot of step number print(str(i)) time.sleep(0.3) diff --git a/Python_script/PostPlot.py b/Python_script/PostPlot.py new file mode 100644 index 0000000000000000000000000000000000000000..5ff91d6bb8f4bf94eed4ecc0f933c2adc954a27d --- /dev/null +++ b/Python_script/PostPlot.py @@ -0,0 +1,191 @@ +import pandas as pd +import sys +from pathlib import Path +import os +from MeasurementPlot import MeasurementPlot +import numpy as np + +class PostPlot: + """ + Generate a plot of all data files from the individual measurements to have one plot of the whole + data taking sequence. + Optionally, correlation and regression can be calculated and plotted. + """ + def __init__(self, reference_signal_names, trace_subplot5='', legend_loc='upper left', legend_bbox_to_anchor=(1.09, 1)): + # set python for opening an separate plot window + if 'ipykernel' in sys.modules: + from IPython import get_ipython + get_ipython().run_line_magic('matplotlib', 'qt') + + self.measplot = MeasurementPlot(reference_signal_names, trace_subplot5=trace_subplot5, + legend_loc=legend_loc, legend_bbox_to_anchor=legend_bbox_to_anchor) + + self.legend_loc = legend_loc + self.legend_bbox_to_anchor = legend_bbox_to_anchor + + # set parameter figure of class to object parameter figure + self.fig = self.measplot.fig + + self.reference_signal_names = reference_signal_names + + # read csv-file and import data to data frame + def import_csv(self, csv_file): + + data_frame = pd.read_csv(csv_file) + + return data_frame + + 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") + + # reset index of data frame + data_frame.reset_index(inplace=True, drop=True) + + # set y-Achlabel in all subplots to Time an in square brackets the selected time unit + for element in self.measplot.ax1: + element.set_xlabel("Time [%s]" %time_unit) + + # make a copy of data_frame in parameter list without changing original during modification + 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] + + # Set scaling of time axis depending on the time unit given in parameter list. + # Default unit is minutes + time_vals = self.scaling_time_axes(time_sec, time_unit) + + # update Timestamps with calculated time values + postplot_data_frame.update(time_vals) + + # refresh subplots with data in data frame + self.measplot.draw(postplot_data_frame, pdf_name='') + + # cal PK2PK values of magnitude and phase + PK2PK = self.calc_pkpk_values(postplot_data_frame) + + self.edit_annotation_in_plot(annotate_string=PK2PK) + + def edit_annotation_in_plot(self, annotate_string='', anno_fontsize=16): + + self.measplot.annotation.set_text(annotate_string) + self.measplot.annotation.set_fontsize(anno_fontsize) + + def calc_pkpk_values(self, data_frame): + + # calc PK2PK values of the two reference signals + delta0 = max(data_frame[self.reference_signal_names[0]]) - min(data_frame[self.reference_signal_names[0]]) + delta1 = max(data_frame[self.reference_signal_names[1]]) - min(data_frame[self.reference_signal_names[1]]) + + # generate text for annotation in first subplot + # FIXME: This used to contain units. We will need some 'pretty printing' values for the nanes + delta_vals_string = ('$\Delta_{PkPk}$'+self.reference_signal_names[0]+': '+str(delta0)+'\n$\Delta_{PkPk}$'+ + self.reference_signal_names[1]+': '+str(delta1)) + + return delta_vals_string + + def add_curvefit_to_plot(self, y_lim=None, xvals=[], yvals=[], trace_color='None', trace_label=''): + if y_lim is not None: + # set axis for phase plot to min, max values + self.measplot.ax1[0].set_ylim(y_lim) + + self.measplot.path_collection_fit.set_color(trace_color) + self.measplot.path_collection_fit.set_label(trace_label) + + # refresh data in meas plot + self.measplot.path_collection_fit.set_offsets(np.c_[xvals, yvals]) + + # get legend handles and labels y-axes from subplot magnitude, phase + handles_phase, labels_phase = self.measplot.ax1[0].get_legend_handles_labels() + handles_mag, labels_mag = self.measplot.magnitude_axis.get_legend_handles_labels() + handles_equi0, labels_eqi0 = self.measplot.equi_axis0.get_legend_handles_labels() + + handles = handles_phase + handles_mag + handles_equi0 + labels = labels_phase + labels_mag + labels_eqi0 + + # update legend subplot phase, magnitude + self.measplot.ax1[0].legend(handles, labels, loc=self.legend_loc, + bbox_to_anchor=self.legend_bbox_to_anchor) + + # refresh plot window + self.measplot.fig.canvas.flush_events() + + # save figure under the given path and file name + def save_fig(self, storepath, filename): + if not os.path.exists(storepath): + os.makedirs(storepath) + + self.fig.savefig(os.path.join(storepath, filename)) + + # scaling time axes depending on the time unit + def scaling_time_axes(self, time_sec, time_unit): + if time_unit == 'min': + time_vals = time_sec/60 + + elif time_unit == 'hours': + time_vals = time_sec/3600 + + elif time_unit == 'sec': + time_vals = time_sec + + else: + time_vals = time_sec/60 + + return time_vals + + +if __name__ == '__main__': + + + # set result path for post plot + Results_Path = r'TestData_JBY240' + + time_unit = 'min' + + storepath = os.path.join(Results_Path, 'PostPlots') + + # search all csv files in results folder + csv_file_list = list(Path(Results_Path).glob("**/*.csv")) + + # selection measurment data should be plotted in subplot5 + # 'logger_sens' : values for temperature and humidity of logger sensor in + # measurement instrument chmaber + # 'chamber_sens' : values for temepratere and humidity readback from chamber sensors + # of chamber for measurement instruments + # 'heater_dut_chamber': activity of temp heater and hum heater readback from DUT chamber + # This is also the default parameter + + + 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() + + # plot results for each csv-file + for index, csv_file in enumerate(csv_file_list): + + data_frame = plot_obj.import_csv(str(csv_file)) + 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) + + plot_obj.plot_frame_data(data_frame, title, time_unit) + + filename = str(csv_file.stem) + '.pdf' + plot_obj.save_fig(storepath, filename) + + # plot of all steps of a sweep is plotted in one diagram + plot_obj.plot_frame_data(concat_data_frame, 'Full Transition', time_unit) + + filename = 'Full_Transition' + '.pdf' + plot_obj.save_fig(storepath, filename) + + \ No newline at end of file diff --git a/Python_script/SoftwareVersion.py b/Python_script/SoftwareVersion.py new file mode 100644 index 0000000000000000000000000000000000000000..b4d870191513050624d7a25245df7af772bfa2ce --- /dev/null +++ b/Python_script/SoftwareVersion.py @@ -0,0 +1,5 @@ +import subprocess + +def software_version(): + return subprocess.check_output(['git', 'describe']).strip().decode() + diff --git a/Python_script/TestStandGUI.bat b/Python_script/TestStandGUI.bat new file mode 100644 index 0000000000000000000000000000000000000000..425416afafb83fa13a5a85efd95963111f827391 --- /dev/null +++ b/Python_script/TestStandGUI.bat @@ -0,0 +1,6 @@ +@echo off +cd.. +cd.. +cd C:\git\climate-lab-test-stand\Python_script & REM path has to be changed to folder with file "climate-lab-gui.py" +python C:\git\climate-lab-test-stand\Python_script\climate-lab-gui.py %* & REM Python_script & REM path has to be changed to folder with file "climate-lab-gui.py" +pause \ No newline at end of file diff --git a/Python_script/VNA.py b/Python_script/VNA.py index aa0f37d93f3589c15bfe6c2b0009d3005c12e47b..0e39b7a8c7813cca19b1a95a591e2f20ea4a519a 100755 --- a/Python_script/VNA.py +++ b/Python_script/VNA.py @@ -17,6 +17,8 @@ class Vna: Implements the basic operation of the VNA """ + self.vna = None + # open connection here # store the credentials so that methods can access them @@ -196,9 +198,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\\ @@ -230,8 +230,13 @@ class Vna: #wait until finished self.vna.query('*OPC?') + # reset status registers and queues of the VNA + def reset_status(self): + self.vna.write('*CLS') + def close(self): - self.vna.close() + if self.vna is not None: + self.vna.close() def __del__(self): self.close() diff --git a/Python_script/VNA_dummy.py b/Python_script/VNA_dummy.py index 69260881b8b9cf1149e6f02f12ea365906f370a3..1429a036ba518a236481669bce39d9b9f16c5a95 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,8 +151,11 @@ 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): pass + + def reset_status(self): + pass diff --git a/Python_script/almemo2490.py b/Python_script/almemo2490.py new file mode 100644 index 0000000000000000000000000000000000000000..5af34a4fbdea8309dbd68ce296e64c29657c0bac --- /dev/null +++ b/Python_script/almemo2490.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 2 11:08:12 2023 + +@author: michael pawelzik +Some parts have been copied from uros mavric and then they have been modified for compatibility. + +#### SYNTAX FOR CALLING CLASS OBJECTS ######################################## + +Look into module play_with_almemo2490.py + +############################################################################## +""" + + +# import pythod modules +import time +from telnetlib import Telnet +import re +from datetime import datetime +import pandas as pd +import numpy as np +import traceback +from functools import wraps + + +# python class for error handling of almemo 2490 +class err_hdl(Exception): + + # function for retry decorate object in case of exception + def do_retry(ExceptionToCheck = Exception, tries=4, delay=3, backoff=2, \ + default_response = None): + """Retry calling the decorated function using an backoff and defaults for return + + original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry + several lines of code have been modified for functionality + + :param ExceptionToCheck: the exception to check. may be a tuple of + exceptions to check + :type ExceptionToCheck: Exception or tuple + :param tries: number of times to try (not retry) before giving up + :type tries: int + :param delay: initial delay between retries in seconds + :type delay: int + :param backoff: backoff multiplier e.g. value of 2 will double the delay + each retry + :param default response: default values to return if all tries fail + : type default response: Value or tuple + + """ + def decorator_retry(function): + + @wraps(function) + def function_retry(*args, **kwargs): + + mtries, mdelay = tries, delay + while mtries > 0: + try: + return function(*args, **kwargs) + except ExceptionToCheck: + print("ALMEO2490: Error occured in %s,'Retrying in %d seconds" %(function.__name__, mdelay)) + time.sleep(mdelay) + mtries -= 1 + mdelay += backoff + raise Exception('maximum number of retries [%d] reached\nfunction: %s\nmodule: %s' \ + % (tries, function.__name__,function.__module__)) + + return function_retry # true decorator + + return decorator_retry + + # function for handle exception + def do_handle_exceptions(ExceptionToCheck = Exception, default_response = None): + + """ error calling the decorated function using defaults for return + original from: + https://www.3resource.com/python-excercises/decorator/python-decorator-exercise-9.php + + several lines of code have been modified for functionality + :param ExceptionToCheck: the exception to check. may be a tuple of + exceptions to check + :type ExceptionToCheck: Exception or tuple + :param default response: default values to return if all tries fail + : type default response: Value or tuple + + """ + def exception_decorator(function): + + def function_handle_exception(*args, **kwargs): + + try: + return function(*args, **kwargs) + + except ExceptionToCheck as e: + + print('ALMEO2490: ' + str(type(e).__name__) + " occured in:\nfunction: %s\nmodule: %s" \ + % (function.__name__,function.__module__)) + print(traceback.print_tb(e.__traceback__)) + + return default_response + + return function_handle_exception + + return exception_decorator + + +class almemo2490: + + + # constructor of class + def __init__(self, ip = '192.168.115.44', timeout = 10): + self.tn = None + + # set class variabel with ip adress given in parameter list of constructor + self.ip_adress = ip + + # set class variable with timeout value given in parameter list of constructor + self.timeout = timeout + + # open telenet connection to ALMEMO 2490 + self.tn = self.open_connection() + + # create data frame for measurment buffer to store parameter and measured + # properties of connected sensors to ALMEMO 2490 + self.meas_buffer = pd.DataFrame(columns = ['meas_date','meas_time', 'sens_port', \ + 'sens_channel', 'meas_val', 'channel_unit', \ + 'channel_name']) + + + + """ + ##### commands are echoed. Will cause conflicts in communication and there it is obmitted #### + # write function to send commands to ALMEMO 2490 that do not send a + # response + def write(self, cmd_str, wait_time = 0.5): + + # send command to ALMEMO 2490 + self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r') + + # wait + time.sleep(wait_time) + """ + @err_hdl.do_retry(tries=5, delay=1, backoff=3) + def open_connection(self): + + return Telnet(self.ip_adress, 10001, self.timeout) + + + # query function to send a command to almemo 2490 and + # return the response to the command + + def query(self, cmd_str, wait_time = 1): + + # send request to ALMEMO 2490 encode UTF8 with CR + self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r') + + # wait + time.sleep(wait_time) + + # read_buffer = self.tn.read_very_eager() + # read buffer until END OF TEXT character has been reach or timeout has occured + read_buffer = self.tn.read_until(b'\x03',self.timeout) + + # decode receivved data with CP437 format + decoded_read_buffer = read_buffer.decode('cp437') + + + # return received data + return decoded_read_buffer + + + # method to set date of ALMEMO 2490 + @err_hdl.do_handle_exceptions() + def set_date(self): + + # request date from operating system + # convert requested date to string + # date format: ddmmyy (d: day, m: month, y: year, last 2 digits ) + date = datetime.strftime(datetime.now(),"%d%m%y") + + # cmd string for setting date of device + cmd_str = 'd' + date + + # send command for setting date to device + self.query(cmd_str) + + + + # method to set time of ALMEMO 2490 + @err_hdl.do_handle_exceptions() + def set_time(self): + + # request date from operating system + #convert requested time to strinf + # time format: hhmmss (h:hour, m: minute, s: second) + time = datetime.strftime(datetime.now(), "%H%M%S") + + # cmd string for setting time of device + cmd_str = 'U' + time + + # send command for setting date to device + self.query(cmd_str) + + + # method to get actual date from ALMEMO 2490 + @err_hdl.do_handle_exceptions(default_response = "01.01.70") + def get_date(self): + + # request date from ALMEMO 2490 + read_buffer = self.query('P13') + + # search date string in reply to command P13 + # date alway has the following format: xx.xx.xx where x is within [0-9] + # get time out of read buffer and strip white space characters + + date = re.search(r'([0-9]{2}\.[0-9]{2}\.[0-9]{2})', read_buffer).group(1) + + return date + + # method to get actual actual time from ALMEMO 2490 + @err_hdl.do_handle_exceptions(default_response = "00:00:00") + def get_time(self): + + # request time from ALMEMO 2490 + read_buffer = self.query('P10') + + # search time string in reply to command P10 + # time alway has the following format: xx:xx:xx where x cis within [0-9] + # get time out of read buffer and strip white space characters + time = re.search(r'([0-9]{2}:[0-9]{2}:[0-9]{2})', read_buffer).group(1) + + return time + + # method to set channel name of selected channel at ALMEMO 2490 + # channel name can be at max 10 characters + # not allowed characters in name are white space charcters and semicolon + # numbmer format: 'xy' where x has to be [0-9] and y has be [0-1] + @err_hdl.do_handle_exceptions() + def set_channel_name(self, channel, channel_name): + + # select channel for modification + self.query('M' + channel) + + # send first part of command for changing channel name + self.query('f2') + + # send second part of command for changing channel name + channel name + CR + self.query('$' + channel_name + '\r') + + # call method for requisting channel name from ALMEMO 2490 + read_buffer = self.get_channel_name(channel) + + # compare requested channel name an channel name from parameter list + # if non equal print error message + if read_buffer.strip() != channel_name: + print('setting of channel name has failed') + + + # method to get channel name from selected channel at ALMEMO 2490 + # numbmer format: 'xy' where x has to be [0-9] and y has be [0-1] + @err_hdl.do_handle_exceptions(default_response = "N/A") + def get_channel_name(self, channel): + + # select channel for request of channel name + self.query('M' + channel) + + # send command for requesting channel name to ALMEMO 2490 + read_buffer = self.query('P00') + + + # search channel name in reply to cammand P00 + # name follows after 2$\ in read_buffer + # get name out of read buffer + + channel_name = re.search(r'([^ ]+ +\r)', read_buffer).group(1) + + return channel_name + + + # method to request the list of all active measurement channels of all sensors + # that are connected to ALMEMO 2490 + # several parameters (channel number, meas_port, channel_unit, channel_name) are + # collected during request an stored in the meas buffer data frame of python class + @err_hdl.do_retry(tries=4, delay=3, backoff=3) + def request_sens_channel_list(self): + + self.set_meas_output_format('N0') + + # send command for requesting the list of all active meas channels of all sensors + # that are connected to ALMEMO 2490 + read_buffer = self.query('P15') + + # find channel entries in read buffer and add them to a list + # a channel entry starts with M\ and ends with CR + channel_entries = re.findall(r'([0-9]{2}:DIGI.+\r)', read_buffer) + + # to get fixed parameter (channel number, measurment name, measurment unit), + # of a channel entry out of the read buffer search patterns are needed to identify them + + search_patterns = {'sens_channel': r'([0-9]{2})', \ + 'channel_unit': r'( [^ ]{2})', 'channel_name': r'([^ ]+ +\r)'} + + # the search pattern contains identification string for the search in front of the + # measurment parameter of interest and has to be removed -> strip pattern + strip_patterns = {'sens_channel': '', 'channel_unit': ' ', \ + 'channel_name': ' \r'} + + # dictonary where parameter of a channel are temporary store before they are added + # to the measurement buffer of the class + channel_params = {'sens_channel': '', 'channel_unit': '', 'channel_name': ' ', 'sens_port': ''} + + + for element in channel_entries: + + for key in search_patterns: + + + # for an channel entry in read buffer parameter is searched in the string, + # and the stip pattern is removed + # result of search and strip is stored in dictonary channel params + channel_params.update({key: re.search(search_patterns[key] ,element).group(1)}) + channel_params.update({key: channel_params[key].strip(strip_patterns[key])}) + + + # with the information of sens_channel the sensor port is determined + + sens_port = channel_params['sens_channel'][1] + channel_params.update({'sens_port': 'M' + sens_port}) + + # add a row in measurement buffer an fill colums in measurement buffer with + # collected parameter + self.meas_buffer = self.meas_buffer.append(channel_params, ignore_index= True) + + self.set_meas_output_format('N2') + + + + # method to request all measument values for all measurement channels of all sensors + # that are connected to ALMEMO 2490 + # if request fails meas_buffer of class is not updated + @err_hdl.do_retry(tries=10, delay=3, backoff=3) + def request_meas_vals_all_channels(self, wait_time = 0.5): + + # request measured value from all measurment channels of all connected sensor + # to ALMEMO 2490 + read_buffer = self.query('S1', wait_time) + + # search for line in measurment buffer that start with a 2 numbers + meas_vals_string = re.search(r'([0-9]{2}[^\s]+)',read_buffer).group(1) + + + # change decimal designator to point if not default + mod_meas_vals_string = meas_vals_string.replace(',','.') + + + # ALMEMO 2490 returns date, time and the measured values for all + # sensor channels of all connceted sensors. + # parameters are separated by semicolon, splitted and stored in list format + meas_param_list = re.findall(r'[^;"]+', mod_meas_vals_string) + + # extraction of date from meas_param_list + date = meas_param_list.pop(0) + date = date.strip('\"') + + # extraction of time from meas_param_list + time = meas_param_list.pop(0) + time = time.strip('\"') + + # check if extracted date and time have correct format + datetime.strptime(date, '%d.%m.%y') + datetime.strptime(time, '%H:%M:%S') + + # store date and time of measurement in meas_buffer of class + self.meas_buffer.loc[:,'meas_date'] = date + self.meas_buffer.loc[:,'meas_time'] = time + + + # after exctraction of date and time meas_vals_list does only contain + # the measured mavules for all sensor channels + # convert the measured values now to float + meas_vals_list = np.array(meas_param_list, dtype = 'float') + + # sore measured values for all sensor channels in measurment buffer of class + self.meas_buffer.loc[:, 'meas_val'] = meas_vals_list + + + + + # method to set the ouput format for the measured data + # default option and the only one at ALMEMO 2490 is + # output in table format separated by semicolon + # S1 for request replies the following format: + # date;time;meas value channel 1;meas vale channel 2; ...;meas value channel N + @err_hdl.do_handle_exceptions() + def set_meas_output_format(self, output_format = 'N2'): + + # send command to set the output format the measured data + self.query (output_format) + + # method to the the conversion rate (Measurements per Second) + @err_hdl.do_handle_exceptions() + def set_conversion_rate(self, rate = 10): + + # create as dictonary with the possible conversion rates and the + # scan time in seconds for the conversion rate + scan_time_dict = dict({10: 0.9, 50: 0.18, 100: 0.09}) + + # send command for setting conversion rate to ALMEMO 2490 + self.query(int(rate)) + + # fetch scan time for selected conversion rate out of dictonary + scan_time = scan_time_dict.get(int(rate)) + + return scan_time + + # method to fetch parameter for selected channel from measrement buffer of class + # selection possible by channel number of sensor channel or channel name + # if more than one row of channel buffer fits entry can be selected by optional parameter item + @err_hdl.do_handle_exceptions(default_response = (pd.Series(), 0)) + def fetch_channel_param_from_meas_buffer(self, pattern = '00', index = 0): + + # filter rows of data frame that match to selection + # results will be a dataframe that contains all matches + sel_channel_data = self.meas_buffer[self.meas_buffer.isin([pattern]).any(axis=1)] + + # if no entry for selection of sensor channel is found in meas buffer of class, + # sensor channel '0.0' will be selected as default parameter + if sel_channel_data.empty == True: + + sel_channel_data = self.meas_buffer[self.meas_buffer.isin(['00']).any(axis=1)] + + # info message is printed that no channel matches to selection + print('value is not in data_frame. Default value (channel 00) is returned') + + # determine number of matches + num_matches = sel_channel_data.shape[0] + + # check if selected entry of filtered measured buffer smaller than number of matches + # max index is always number of matches -1 + if index < num_matches: + meas_channel_data = sel_channel_data.iloc[index,:] + + else: + + # determine the maximum value for index + # max index = number of matches -1 + max_item_val = num_matches - 1 + + # return default value which is the fisrt one in the filtered meas_buffer + # print error message that selected index is too high + meas_channel_data = sel_channel_data.iloc[0,:] + print('Value for index too large. Set Value to Default (index = 0)\n') + print('max index: %d' % max_item_val) + + # data series with selected channel is returned containing all parameter of sensor channel + # if more entires match to selection data series that should be returned can be selected + # by optinal parameter item. By default item is set to 0 + # second parameter that is returned by the method is the number of matches + # that fit to selection criteria + return meas_channel_data, num_matches + + + # close telenet connection + def close(self): + if self.tn is not None: + self.tn.close() + + def __exit__(self): + self.close() + + # destructor of class + def __del__(self): + self.close() + + + + + + + diff --git a/Python_script/almemo710.py b/Python_script/almemo710.py new file mode 100644 index 0000000000000000000000000000000000000000..fd6a7cd6234c8cd46799f418bd5d2a1fd1e068b0 --- /dev/null +++ b/Python_script/almemo710.py @@ -0,0 +1,479 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 2 11:08:12 2023 + +@author: michael pawelzik +Some parts have been copied from uros mavric and then they have been modified for compatibility. + +#### SYNTAX FOR CALLING CLASS OBJECTS ######################################## + +Look into module play_with_almemo710.py + +############################################################################## +""" + + +# import pythod modules +import time +from telnetlib import Telnet +import re +from datetime import datetime +import pandas as pd +import numpy as np +import traceback +from functools import wraps + + +# python class for error handling of almemo 710 +class err_hdl(Exception): + + # function for retry decorate object in case of exception + def do_retry(ExceptionToCheck = Exception, tries=4, delay=3, backoff=2, \ + default_response = None): + """Retry calling the decorated function using an backoff and defaults for return + + original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry + several lines of code have been modified for functionality + + :param ExceptionToCheck: the exception to check. may be a tuple of + exceptions to check + :type ExceptionToCheck: Exception or tuple + :param tries: number of times to try (not retry) before giving up + :type tries: int + :param delay: initial delay between retries in seconds + :type delay: int + :param backoff: backoff multiplier e.g. value of 2 will double the delay + each retry + :param default response: default values to return if all tries fail + : type default response: Value or tuple + + """ + def decorator_retry(function): + + @wraps(function) + def function_retry(*args, **kwargs): + + mtries, mdelay = tries, delay + while mtries > 0: + try: + return function(*args, **kwargs) + except ExceptionToCheck: + print("ALMEO710: Error occured in %s,'Retrying in %d seconds" %(function.__name__, mdelay)) + time.sleep(mdelay) + mtries -= 1 + mdelay += backoff + raise Exception('maximum number of retries [%d] reached\nfunction: %s\nmodule: %s' \ + % (tries, function.__name__,function.__module__)) + + return function_retry # true decorator + + return decorator_retry + + # function for handle exception + def do_handle_exceptions(ExceptionToCheck = Exception, default_response = None): + + """ error calling the decorated function using defaults for return + original from: + https://www.3resource.com/python-excercises/decorator/python-decorator-exercise-9.php + + several lines of code have been modified for functionality + :param ExceptionToCheck: the exception to check. may be a tuple of + exceptions to check + :type ExceptionToCheck: Exception or tuple + :param default response: default values to return if all tries fail + : type default response: Value or tuple + + """ + def exception_decorator(function): + + def function_handle_exception(*args, **kwargs): + + try: + return function(*args, **kwargs) + + except ExceptionToCheck as e: + + print('ALMEO710: ' + str(type(e).__name__) + " occured in:\nfunction: %s\nmodule: %s" \ + % (function.__name__,function.__module__)) + print(traceback.print_tb(e.__traceback__)) + + return default_response + + return function_handle_exception + + return exception_decorator + + + +class almemo710: + + + # constructor of class + def __init__(self, ip = '192.168.115.94', timeout = 10): + self.tn = None + + # set class variabel with ip adress given in parameter list of constructor + self.ip_adress = ip + + # set class variable with timeout value given in parameter list of constructor + self.timeout = timeout + + # open telenet connection to ALMEMO 710 + self.tn = self.open_connection() + + # create data frame for measurment buffer to store parameter and measured + # properties of connected sensors to ALMEMO 710 + self.meas_buffer = pd.DataFrame(columns = ['meas_date','meas_time', 'sens_port', \ + 'sens_channel', 'meas_val', 'channel_unit', \ + 'channel_name']) + + + + """ + ##### commands are echoed. Will cause conflicts in communication and there it is obmitted #### + # write function to send commands to ALMEMO 710 that do not send a + # response + def write(self, cmd_str, wait_time = 0.5): + + # send command to ALMEMO 710 + self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r') + + # wait + time.sleep(wait_time) + """ + + @err_hdl.do_retry(tries=5, delay=1, backoff=3) + def open_connection(self): + return Telnet(self.ip_adress, 10001, self.timeout) + + # query function to send a command to almemo 710 and + # return the response to the command + + def query(self, cmd_str, wait_time = 1): + + # send request to ALMEMO 710 encode UTF8 with CR + self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r') + + # wait + time.sleep(wait_time) + + # read_buffer = self.tn.read_very_eager() + # read buffer until END OF TEXT character has been reach or timeout has occured + read_buffer = self.tn.read_until(b'\x03',self.timeout) + + # decode receivved data with CP437 format + decoded_read_buffer = read_buffer.decode('cp437') + + # return received data + return decoded_read_buffer + + + # method to set date of ALMEMO 710 + @err_hdl.do_handle_exceptions() + def set_date(self): + + # request date from operating system + # convert requested date to string + # date format: ddmmyy (d: day, m: month, y: year, last 2 digits ) + date = datetime.strftime(datetime.now(),"%d%m%y") + + # cmd string for setting date of device + cmd_str = 'd' + date + + # send command for setting date to device + self.query(cmd_str) + + + + # method to set time of ALMEMO 710 + @err_hdl.do_handle_exceptions() + def set_time(self): + + # request date from operating system + #convert requested time to strinf + # time format: hhmmss (h:hour, m: minute, s: second) + time = datetime.strftime(datetime.now(), "%H%M%S") + + # cmd string for setting time of device + cmd_str = 'U' + time + + # send command for setting date to device + self.query(cmd_str) + + + # method to get actual date from ALMEMO 710 + @err_hdl.do_handle_exceptions(default_response = "01.01.1970") + def get_date(self): + + # request date from ALMEMO 710 + read_buffer = self.query('P13') + + # search date string in reply to command P13 + # date alway has the following format: xx.xx.xx where x is within [0-9] + # get time out of read buffer and strip white space characters + + date = re.search(r'([0-9]{2}\.[0-9]{2}\.[0-9]{2})', read_buffer).group() + + return date + + # method to get actual actual time from ALMEMO 710 + @err_hdl.do_handle_exceptions(default_response = "00:00:00") + def get_time(self): + + # request time from ALMEMO 710 + read_buffer = self.query('P10') + + # search time string in reply to command P10 + # time alway has the following format: xx:xx:xx where x cis within [0-9] + # get time out of read buffer and strip white space characters + + time = re.search(r'([0-9]{2}:[0-9]{2}:[0-9]{2})', read_buffer).group() + + return time + + # method to set channel name of selected channel at ALMEMO 710 + # channel name can be at max 10 characters + # not allowed characters in name are white space charcters and semicolon + # channel numbers: string, format 'x.x' where x has to be within [0-9] + @err_hdl.do_handle_exceptions() + def set_channel_name(self, channel, channel_name): + + # select channel for modification + self.query('M' + channel) + + # send first part of command for changing channel name + self.query('f2') + + # send second part of command for changing channel name + channel name + CR + self.query('$' + channel_name + '\r') + + # call method for requisting channel name from ALMEMO 710 + read_buffer = self.get_channel_name(channel) + + # compare requested channel name an channel name from parameter list + # if non equal print error message + if read_buffer.strip() != channel_name: + print('setting of channel name has failed') + + + # method to get channel name from selected channel at ALMEMO 710 + # channel numbers: string, format 'x.x' where x has to be within [0-9] + @err_hdl.do_handle_exceptions(default_response = "N/A") + def get_channel_name(self, channel): + + # select channel for request of channel name + self.query('M' + channel) + + # send command for requesting channel name to ALMEMO 710 + read_buffer = self.query('P00') + + # search channel name in reply to cammand P00 + # name follows after 2$\ in read_buffer + # get name out of read buffer + channel_name = re.search(r'(2\$\\[^;]+)', read_buffer).group() + + # strip white space characters + channel_name = channel_name.strip(r'(2\$\\)') + + return channel_name + + + # method to request the list of all active measurement channels of all sensors + # that are connected to ALMEMO 710 + # several parameters (channel number, meas_port, channel_unit, channel_name) are + # collected during request an stored in the meas buffer data frame of python class + @err_hdl.do_retry(tries=4, delay=3, backoff=3) + def request_sens_channel_list(self): + + # send command for requesting the list of all active meas channels of all sensors + # that are connected to ALMEMO 710 + read_buffer = self.query('P15') + + # find channel entries in read buffer and add them to a list + # a channel entry starts with M\ and ends with CR + channel_entries = re.findall(r'(M\\.+\r)', read_buffer) + + # to get fixed parameter (channel number, measurment name, measurment unit), + # of a channel entry out of the read buffer search patterns are needed to identify them + # pattern for sens_channel: M\x.x where x within [0-9] + # pattern for channel_unit: 1$\xx where x can be every character + # pattern for channel_name: 2$\xx where x can be every character without semicolon + search_patterns = {'sens_channel': r'(M\\[0-9]+\.{1}[0-9]+)', \ + 'channel_unit': r'(1\$\\.{2})', 'channel_name': r'(2\$\\[^;]*)'} + + # the search pattern contains identification string for the search in front of the + # measurment parameter of interest and has to be removed -> strip pattern + strip_patterns = {'sens_channel': r'(M\\)', 'channel_unit': r'(1\$\\)', \ + 'channel_name': r'(2\$\\)'} + + # dictonary where parameter of a channel are temporary store before they are added + # to the measurement buffer of the class + channel_params = {'sens_channel': '', 'channel_unit': '', 'channel_name': '', 'sens_port': ''} + + + for element in channel_entries: + + for key in search_patterns: + + # for an channel entry in read buffer parameter is searched in the string, + # and the stip pattern is removed + # result of search and strip is stored in dictonary channel params + channel_params.update({key: re.search(search_patterns[key] ,element).group(1)}) + channel_params.update({key: channel_params[key].strip(strip_patterns[key])}) + + + # with the information of sens_channel the sensor port is determined + # sens_port = 'M' + numbers in front of the point of the measurment channel + # example: sens_channel = 0.1 -> sens_port = M0 + sens_port, _ = channel_params['sens_channel'].split('.') + channel_params.update({'sens_port': 'M' + sens_port}) + + # add a row in measurement buffer an fill colums in measurement buffer with + # collected parameter + self.meas_buffer = self.meas_buffer.append(channel_params, ignore_index= True) + + + + # method to request all measument values for all measurement channels of all sensors + # that are connected to ALMEMO 710 + # if request fails meas_buffer of class is not updated + + @err_hdl.do_retry(tries=10, delay=3, backoff=3) + def request_meas_vals_all_channels(self, wait_time = 0.5): + + # request measured value from all measurment channels of all connected sensor + # to ALMEMO 710 + read_buffer = self.query('S1', wait_time) + + + # search for line in measurment buffer that starts with digit and ends with digit + meas_vals_string = re.search(r'([0-9].+[0-9])',read_buffer).group() + + # change decimal designator to point if not default + mod_meas_vals_string = meas_vals_string.replace(',','.') + + # ALMEMO 710 returns date, time and the measured values for all + # sensor channels of all connceted sensors. + # parameters are separated by semicolon, splitted and stored in list format + meas_param_list = mod_meas_vals_string.split(';') + + # extraction of date from meas_param_list + date = meas_param_list.pop(0) + + # extraction of time from meas_param_list + time = meas_param_list.pop(0) + + + # check if extracted date and time have correct format + datetime.strptime(date, '%d.%m.%y') + datetime.strptime(time, '%H:%M:%S') + + # store date and time of measurement in meas_buffer of class + self.meas_buffer.loc[:,'meas_date'] = date + self.meas_buffer.loc[:,'meas_time'] = time + + + # after exctraction of date and time meas_vals_list does only contain + # the measured mavules for all sensor channels + # convert the measured values now to float + meas_vals_list = np.array(meas_param_list, dtype = 'float') + + # sore measured values for all sensor channels in measurment buffer of class + self.meas_buffer.loc[:, 'meas_val'] = meas_vals_list + + + + # method to set the ouput format for the measured data + # default option and the only one at ALMEMO 710 is + # output in table format separated by semicolon + # S1 for request replies the following format: + # date;time;meas value channel 1;meas vale channel 2; ...;meas value channel N + @err_hdl.do_handle_exceptions() + def set_meas_output_format(self, output_format = 'N2'): + + # send command to set the output format the measured data + self.query (output_format) + + # method to the the conversion rate (Measurements per Second) + @err_hdl.do_handle_exceptions() + def set_conversion_rate(self, rate = 10): + + # create as dictonary with the possible conversion rates and the + # scan time in seconds for the conversion rate + scan_time_dict = dict({10: 0.9, 50: 0.18, 100: 0.09}) + + # send command for setting conversion rate to ALMEMO 710 + self.query(int(rate)) + + # fetch scan time for selected conversion rate out of dictonary + scan_time = scan_time_dict.get(int(rate)) + + return scan_time + + # method to fetch parameter for selected channel from measrement buffer of class + # selection possible by channel number of sensor channel or channel name + # if more than one row of channel buffer fits entry can be selected by optional parameter item + @err_hdl.do_handle_exceptions(default_response = (pd.Series(), 0)) + def fetch_channel_param_from_meas_buffer(self, pattern = '0.0', index = 0): + + # filter rows of data frame that match to selection + # results will be a dataframe that contains all matches + sel_channel_data = self.meas_buffer[self.meas_buffer.isin([pattern]).any(axis=1)] + + # if no entry for selection of sensor channel is found in meas buffer of class, + # sensor channel '0.0' will be selected as default parameter + if sel_channel_data.empty == True: + + sel_channel_data = self.meas_buffer[self.meas_buffer.isin(['0.0']).any(axis=1)] + + # info message is printed that no channel matches to selection + print('value is not in data_frame. Default value (channel 0.0) is returned') + + # determine number of matches + num_matches = sel_channel_data.shape[0] + + # check if selected entry of filtered measured buffer smaller than number of matches + # max index is always number of matches -1 + if index < num_matches: + meas_channel_data = sel_channel_data.iloc[index,:] + + else: + + # determine the maximum value for index + # max index = number of matches -1 + max_item_val = num_matches - 1 + + # return default value which is the fisrt one in the filtered meas_buffer + # print error message that selected index is too high + meas_channel_data = sel_channel_data.iloc[0,:] + print('Value for index too large. Set Value to Default (index = 0)\n') + print('max index: %d' % max_item_val) + + # data series with selected channel is returned containing all parameter of sensor channel + # if more entires match to selection data series that should be returned can be selected + # by optinal parameter item. By default item is set to 0 + # second parameter that is returned by the method is the number of matches + # that fit to selection criteria + return meas_channel_data, num_matches + + + # close telenet connection + def close(self): + if self.tn is not None: + self.tn.close() + + def __exit__(self): + self.close() + + # destructor of class + def __del__(self): + self.close() + + + + + + + + + diff --git a/Python_script/analysis.py b/Python_script/analysis.py index 5ec2edecbfb68a5b734ba66c239d66ab4ee89d2c..0d20ffeed20010795d88c6f5e9f19e96f5f2fbdf 100644 --- a/Python_script/analysis.py +++ b/Python_script/analysis.py @@ -1,88 +1,205 @@ import pandas as pd import matplotlib.pyplot as plt -import math +import numpy as np +from matplotlib import gridspec +from SoftwareVersion import software_version -def extract_stable_data(datafile): +def extract_stable_data(datafile, measurement_set, reference_signal_names, extra_signal_names): datapoint = {} - #df is a pandas data frame + # 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'] - if phases.size == 0: + # extract phase mean and variance for stable measurements (don't ask what loc means) + signal0 = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), + reference_signal_names[0]] + if signal0.size == 0: return None - datapoint['phase_mean']=phases.mean() - datapoint['phase_var']=phases.var() + datapoint['signal0_mean']=signal0.mean() + datapoint['signal0_var']=signal0.var() - magnitudes = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'S21_MAGNITUDE'] - datapoint['magnitude_mean'] = magnitudes.mean() - datapoint['magnitude_var'] = magnitudes.var() + signal1 = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), + reference_signal_names[1]] + datapoint['signal1_mean'] = signal1.mean() + datapoint['signal1_var'] = signal1.var() - temperatures = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'READBACK_TEMPERATURE'] + temperatures = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'TEMP_DUT'] 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), 'HUM_DUT'] datapoint['humidity_mean'] = humidities.mean() datapoint['humidity_var'] = humidities.var() + for extra_signal_name in extra_signal_names: + extra_signal_values = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), extra_signal_name] + datapoint[extra_signal_name] = extra_signal_values.mean() + return datapoint # 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, reference_signal_names, + analysis_config): + set_data = {} + derivatives = {} + for measurement_set in measurement_sets: + set_data[measurement_set] = {'signal0_means': [], 'signal0_vars': [], 'signal1_means': [], 'signal1_vars': [], + 'x_data': []} + for external_signal_name in analysis_config['extra_signal_names']: + set_data[measurement_set][external_signal_name] = [] + derivatives[measurement_set] = {'signal0_deltas': [], 'signal1_deltas': [], 'x_deltas': []} + 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']) - - magnitudes_db = [20 * math.log10(x) for x in magnitudes ] - # approximate formula for small errors (linear approximation) - magnitude_vars_db = [20 / math.log(10) * dx/x for x, dx in zip(magnitudes, magnitude_vars)] + for measurement_set in measurement_sets: + datapoint = extract_stable_data(datafile, measurement_set, reference_signal_names, + analysis_config['extra_signal_names']) + 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['signal0_means'].append(datapoint['signal0_mean']) + data['signal0_vars'].append(datapoint['signal0_var']) + data['signal1_means'].append(datapoint['signal1_mean']) + data['signal1_vars'].append(datapoint['signal1_var']) + + for external_signal_name in analysis_config['extra_signal_names']: + data[external_signal_name].append(datapoint[external_signal_name]) + + for measurement_set in measurement_sets: + data = set_data[measurement_set] + if analysis_config['normalise'][0]: + data['signal0_means'] -= data['signal0_means'][0] + if analysis_config['normalise'][1]: + data['signal1_means'] -= data['signal1_means'][0] + + fig = plt.figure(figsize=(10, 15)) + gs = gridspec.GridSpec(nrows=6, ncols=1, hspace=0)#, width_ratios=[3, 1], height_ratios=[3, 1]) + + upper_text_block = '\n'.join(['DUT name: '+analysis_config['dut_name'], + 'SW version: '+software_version(), 'Date: '+analysis_config['time_string']]) - fig, ax1 = plt.subplots() + + fig.text(0.1, 0.88, upper_text_block) + + ax1 = fig.add_subplot(gs[1, 0]) + ax2 = fig.add_subplot(gs[2, 0], sharex=ax1) + ax4 = fig.add_subplot(gs[4, 0]) + ax5 = fig.add_subplot(gs[5, 0], sharex=ax4) + + ax1.tick_params(bottom=True, top=True, direction='in') + ax2.tick_params(bottom=True, top=True, direction='inout') + ax4.tick_params(bottom=True, top=True, direction='in') + ax5.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]') + ax5.set_xlabel('temperature [deg C]') + denominator_name = '/ 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 [%]') + ax5.set_xlabel('relative humidity [%]') + denominator_name = '/ % r.h.' 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') - - ax2 = ax1.twinx() - ax2.errorbar(x_data, magnitudes_db, magnitude_vars_db, marker='x', color='red', linewidth=0) - ax2.set_ylabel('S21 magnitude [dB]', color='red') + for measurement_set in measurement_sets: + ax1.errorbar(set_data[measurement_set]['x_data'], + set_data[measurement_set]['signal0_means'], set_data[measurement_set]['signal0_vars'], + label=measurement_set, marker='+', linewidth=0) + ax1.set_ylabel(reference_signal_names[0]) + + for measurement_set in measurement_sets: + ax2.errorbar(set_data[measurement_set]['x_data'], + set_data[measurement_set]['signal1_means'], set_data[measurement_set]['signal1_vars'], + label=measurement_set, marker='+', linewidth=0) + ax2.set_ylabel(reference_signal_names[1]) + + # 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)) + + ####################################################################################################################### + # Derrivatives + ####################################################################################################################### + for measurement_set in measurement_sets: + data = set_data[measurement_set] + deriv_data = derivatives[measurement_set] + for i in range(len(data['signal0_means'])-1): + ylabel = '$\\Delta$ '+reference_signal_names[0]+' '+denominator_name + deriv_data['signal0_deltas'].append((data['signal0_means'][i+1]-data['signal0_means'][i])/ + (data['x_data'][i+1]-data['x_data'][i])) + deriv_data['signal1_deltas'].append((data['signal1_means'][i+1]-data['signal1_means'][i])/ + (data['x_data'][i+1]+data['x_data'][i])) + deriv_data['x_deltas'].append((data['x_data'][i+1]+data['x_data'][i])/2) + + ax4.set_ylabel('$\\Delta$ '+reference_signal_names[0]+' '+denominator_name) + ax5.set_ylabel('$\\Delta$ '+reference_signal_names[1]+' '+denominator_name) + + ####################################################################################################################### + # RF cable post analysis: Normalise to cable length and convert phase to time + ####################################################################################################################### - fig.tight_layout() # otherwise the right y-label is slightly clipped - - fig.savefig(basename+'_analysis.pdf') + if analysis_config['type'] == 'rf_cable': + for measurement_set in measurement_sets: + data = set_data[measurement_set] + deriv_data = derivatives[measurement_set] + phase_to_time = 1e15 / (analysis_config['cable_length'] * data['RF_FREQUENCY'][0] * 360.) + deriv_data['signal0_deltas'][:] = [x * phase_to_time for x in deriv_data['signal0_deltas']] + deriv_data['signal1_deltas'][:] = [x / analysis_config['cable_length'] for x in deriv_data['signal1_deltas']] + + ax4.set_ylabel('$\\Delta$t [fs/m/K]') + ax5.set_ylabel('$\\Delta$A [dB/m/K]') + + + ####################################################################################################################### + # Closeout + ####################################################################################################################### + #manual scaling if y axis. Auto is broken with scatter. + ax4_mins = [] + ax4_maxs = [] + ax5_mins = [] + ax5_maxs = [] + + for measurement_set in measurement_sets: + ax4.scatter(derivatives[measurement_set]['x_deltas'], derivatives[measurement_set]['signal0_deltas'], marker='+') + ax5.scatter(derivatives[measurement_set]['x_deltas'], derivatives[measurement_set]['signal1_deltas'], marker='+') + if not len(derivatives[measurement_set]['signal0_deltas']) == 0: + ax4_mins.append(min(derivatives[measurement_set]['signal0_deltas'])) + ax4_maxs.append(max(derivatives[measurement_set]['signal0_deltas'])) + ax5_mins.append(min(derivatives[measurement_set]['signal1_deltas'])) + ax5_maxs.append(max(derivatives[measurement_set]['signal1_deltas'])) + + # set auto y range which does not work properly for small values in scatter + if not len(ax4_maxs) == 0: + dy4 = (max(ax4_maxs) - min(ax4_mins))*0.1 + ax4.set_ylim(min(ax4_mins)-dy4, max(ax4_maxs)+dy4) + dy5 = (max(ax5_maxs) - min(ax5_mins))*0.1 + ax5.set_ylim(min(ax5_mins)-dy5, max(ax5_maxs)+dy5) + + fig.tight_layout() # otherwise the legend is clipped + + 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(20., 31.+1.), [35.]*12, 'tempsweep1', 'temperature', + ['1.3GHz', '1.0GHz', '3.0GHz', '6.0GHz', '10.0GHz'], ['S21_PHASE', 'S21_MAGNITUDE'], + analysis_config={'type': 'rf_cable', 'normalise': [True, False], 'cable_length': 10, + 'extra_signal_names': ['RF_FREQUENCY'], 'dut_name': 'simulated cable', + 'time_string': '0:0:0'}) diff --git a/Python_script/climate-lab-gui.py b/Python_script/climate-lab-gui.py index 6a9e19205209824f544c181039bb346b028dd525..2d63f4ff2a03b7824814ec99f6bac448caeca90a 100755 --- a/Python_script/climate-lab-gui.py +++ b/Python_script/climate-lab-gui.py @@ -54,6 +54,22 @@ class TestStandMainWindow(QMainWindow): print('changed dir') return True + def get_analysis_config(self, time_string): + analysis_config = {} + analysis_config['dut_name'] = self.dutName.text() + analysis_config['time_string'] = time_string + if self.analysisTypeCableButton.isChecked(): + analysis_config['type'] = 'rf_cable' + analysis_config['extra_signal_names'] = ['RF_FREQUENCY'] + analysis_config['normalise'] = [True, False] + analysis_config['cable_length'] = self.cableLengthSpinBox.value() + else: + analysis_config['type'] = 'default' + analysis_config['extra_signal_names'] = [] + analysis_config['normalise'] = [self.removeOffsetsSignal0.isChecked(), self.removeOffsetsSignal1.isChecked()] + + return analysis_config + def do_measurement(self): self.setEnabled(False) self.qt_app.processEvents(); @@ -61,9 +77,13 @@ class TestStandMainWindow(QMainWindow): os.chdir(self.start_dir) with open('test_stand_parameter.json', 'r') as f: config_data = json.load(f) + + with open('ext_sensor_channels.json', 'r') as f2: + ext_sensor_channels = json.load(f2) + time_string = time.strftime("%Y_%m_%d-%H_%M_%S") if self.autoNameCheckbox.isChecked(): - output_basename = time.strftime("%Y_%m_%d-%H_%M_%S") + "_results" + output_basename = time_string + "_results" else: output_basename = self.baseName.text() @@ -71,9 +91,9 @@ class TestStandMainWindow(QMainWindow): self.setEnabled(True) return - meas = prototype.Measurements(config_data['chamber_ip'], config_data['vna_ip'], output_basename, - False, config_data) + meas = None try: + meas = prototype.Measurements(config_data, output_basename,False, ext_sensor_channels) if self.tempSweepButton.isChecked(): temperatures = meas.perform_sweep(self.startParameter.value(), self.stopParameter.value(), self.stepParameter.value(), self.fixedParameter.value(), @@ -83,9 +103,12 @@ 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') - prototype.plot_output(output_basename, temp_extensions, True, output_basename + - ': Temperature sweep ' + str(temperatures[0]) + '--' + + 'temperature', meas.dut.get_measurement_set_names(), + meas.dut.get_dut_reference_signal_names(), self.get_analysis_config(time_string)) + + prototype.plot_output(output_basename, temp_extensions, True, config_data, ext_sensor_channels, + meas.dut.get_measurement_set_names(), meas.dut.get_dut_reference_signal_names(), + output_basename + ': Temperature sweep ' + str(temperatures[0]) + '--' + str(temperatures[-1]) + ' degC @ ' + str(self.fixedParameter.value()) + ' % r.h.') elif self.humSweepButton.isChecked(): @@ -97,22 +120,34 @@ 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') - prototype.plot_output(output_basename, hum_extensions, True, output_basename + - ': Humidity sweep ' + str(humidities[0]) + '--' + + 'humidity', meas.dut.get_measurement_set_names(), + meas.dut.get_dut_reference_signal_names(), self.get_analysis_config(time_string)) + + prototype.plot_output(output_basename, hum_extensions, True, config_data, ext_sensor_channels, + meas.dut.get_measurement_set_names(), meas.dut.get_dut_reference_signal_names(), + output_basename + ': Humidity sweep ' + str(humidities[0]) + '--' + str(humidities[-1]) + ' % r.h. @ ' + str(self.fixedParameter.value()) + ' degC') elif self.measurementFileButton.isChecked(): try: n_measurements = meas.perform_measurements(os.path.join(self.start_dir, self.measurementFile.text())) - prototype.plot_output(output_basename, range(n_measurements), True, output_basename) + prototype.plot_output(output_basename, range(n_measurements), True, config_data, ext_sensor_channels, + meas.dut.get_measurement_set_names(), + meas.dut.get_dut_reference_signal_names(), + output_basename) except FileNotFoundError as e: QtWidgets.QMessageBox.warning(self, 'Warning', str(e)) + except Exception as e: + print('ERROR: Exception during measurement: '+str(e)) + QtWidgets.QMessageBox.critical(self, 'Error', str(e)) finally: - meas.chamber.close() + if meas is not None: + meas.chamber.close() + meas.ext_sensors.close() + self.setEnabled(True) diff --git a/Python_script/climate-lab-main.ui b/Python_script/climate-lab-main.ui index 098b07eba123626d1333616a987a7d10eedf0522..9f90c667e844b1814fe3f085f162332c05347749 100644 --- a/Python_script/climate-lab-main.ui +++ b/Python_script/climate-lab-main.ui @@ -6,16 +6,16 @@ <rect> <x>0</x> <y>0</y> - <width>618</width> - <height>603</height> + <width>536</width> + <height>786</height> </rect> </property> <property name="windowTitle"> <string>MainWindow</string> </property> <widget class="QWidget" name="centralwidget"> - <layout class="QGridLayout" name="gridLayout_4"> - <item row="0" column="0"> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> <widget class="QFrame" name="measurementTypeFrame"> <property name="frameShape"> <enum>QFrame::StyledPanel</enum> @@ -74,7 +74,7 @@ </layout> </widget> </item> - <item row="1" column="0"> + <item> <spacer name="verticalSpacer_2"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -87,7 +87,7 @@ </property> </spacer> </item> - <item row="2" column="0"> + <item> <widget class="QGroupBox" name="parametersGroupBox"> <property name="title"> <string>Sweep parameters</string> @@ -191,7 +191,7 @@ <number>1</number> </property> <property name="value"> - <double>45.000000000000000</double> + <double>55.000000000000000</double> </property> </widget> </item> @@ -265,7 +265,7 @@ </layout> </widget> </item> - <item row="3" column="0"> + <item> <spacer name="verticalSpacer_3"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -278,10 +278,10 @@ </property> </spacer> </item> - <item row="4" column="0"> + <item> <widget class="QGroupBox" name="nameGroupBox"> <property name="title"> - <string>Name</string> + <string>Run Name</string> </property> <layout class="QVBoxLayout" name="verticalLayout_4"> <item> @@ -307,7 +307,114 @@ </layout> </widget> </item> - <item row="5" column="0"> + <item> + <widget class="QGroupBox" name="analysisGroupBox"> + <property name="title"> + <string>Analysis</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <layout class="QHBoxLayout" name="dutNameLayout"> + <item> + <widget class="QLabel" name="dutNameLabel"> + <property name="text"> + <string>DUT name</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="dutName"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="analysisMiddleLayout"> + <item> + <widget class="QGroupBox" name="analysisTypeGroupBox"> + <property name="title"> + <string>Analysis type</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QRadioButton" name="analysisTypeDefaultButton"> + <property name="text"> + <string>Default</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="analysisTypeCableButton"> + <property name="text"> + <string>RF Cable</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="cableLengthLayout"> + <item> + <widget class="QLabel" name="cableLengthLabel"> + <property name="text"> + <string>Length [m]</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="cableLengthSpinBox"> + <property name="value"> + <double>10.000000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="removeOffsetsGroupBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="title"> + <string>Remove offsets</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QCheckBox" name="removeOffsetsSignal1"> + <property name="text"> + <string>Signal 1</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="removeOffsetsSignal2"> + <property name="text"> + <string>Signal 2</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <layout class="QHBoxLayout" name="startButtonLayout"> <item> <spacer name="horizontalSpacer"> @@ -351,7 +458,7 @@ <rect> <x>0</x> <y>0</y> - <width>618</width> + <width>536</width> <height>30</height> </rect> </property> @@ -383,8 +490,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>122</x> - <y>514</y> + <x>136</x> + <y>504</y> </hint> <hint type="destinationlabel"> <x>138</x> @@ -408,5 +515,37 @@ </hint> </hints> </connection> + <connection> + <sender>analysisTypeCableButton</sender> + <signal>toggled(bool)</signal> + <receiver>removeOffsetsGroupBox</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>139</x> + <y>719</y> + </hint> + <hint type="destinationlabel"> + <x>60</x> + <y>557</y> + </hint> + </hints> + </connection> + <connection> + <sender>analysisTypeCableButton</sender> + <signal>toggled(bool)</signal> + <receiver>removeOffsetsSignal1</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>115</x> + <y>723</y> + </hint> + <hint type="destinationlabel"> + <x>92</x> + <y>586</y> + </hint> + </hints> + </connection> </connections> </ui> diff --git a/Python_script/climate_chamber.py b/Python_script/climate_chamber.py index 3632c7d252c1548a52da7161aa9ade153d76d2ea..3512e434d410d6d710a324dd43cee781b0f9db64 100755 --- a/Python_script/climate_chamber.py +++ b/Python_script/climate_chamber.py @@ -247,6 +247,20 @@ class ClimateChamber: while (not self.is_humidity_reached()) or (not self.is_temperature_reached()): time.sleep(10) break + + def get_heater_percentage(self): + + response = self.chamber.query("%?", delay=MONITOR_COMMAND_DELAY) + + heater_param = response.split(sep=',') + heater_count = int(heater_param[0]) + perc_temp_heater = float(heater_param[1]) + perc_hum_heater = float(heater_param[2]) + + return perc_temp_heater, perc_hum_heater + + + def __del__(self): self.close() diff --git a/Python_script/climate_chamber_dummy.py b/Python_script/climate_chamber_dummy.py index 3b62f3531224aa71e550d525263cb45ad58d9abc..e98f919fecc733297484e86603112b1c1201d2a9 100644 --- a/Python_script/climate_chamber_dummy.py +++ b/Python_script/climate_chamber_dummy.py @@ -161,3 +161,7 @@ class ClimateChamberDummy: def __del__(self): self.close() + + def get_heater_percentage(self): + return 0.0, 0.0 + diff --git a/Python_script/curvefit_and_correlation.py b/Python_script/curvefit_and_correlation.py new file mode 100644 index 0000000000000000000000000000000000000000..0ff057cc07c0c30aedac737c35cf2114dff36ada --- /dev/null +++ b/Python_script/curvefit_and_correlation.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 27 08:53:36 2023 + +@author: pawelzik +""" +import numpy as np +from scipy.optimize import curve_fit +from PostPlot import PostPlot +import pandas as pd +from pathlib import Path + +class reg_and_corr: + + def __init__(self, trace_subplot5 = '', legend_loc = 'upper left', \ + legend_bbox_to_anchor = (1.09, 1)): + + self.postplot = PostPlot(trace_subplot5 = trace_subplot5) + self.K_phases = None + self.phase_t0 = None + self.legend_loc = legend_loc + self.legend_bbox_to_anchor = legend_bbox_to_anchor + + + + def calc_correlation(self, func_1 , func_2 , state = 'False'): + + if state == True: + + # calulate correlation between two parameter from data frame + corr_coeff= np.corrcoef(func_1, func_2)[0][1] + + annotate_string_corr_coeff = "$R_{\phi(S_{21}),Temp_{DUT}}$ = %.3f" % corr_coeff + + self.postplot.edit_annotation_in_plot(annotate_string_corr_coeff) + + def calc_regression_coeff_phase_S21(self, data_frame, time_unit = 'min', state = False): + + if state == True: + phases = data_frame.S21_PHASE.tolist() + self.phase_t0 = phases[0] + + self.K_phases = np.median(data_frame.S21_PHASE) + + # time stamp of index = 0 is the start point of time axis, Michael + # substract all other timestamps with time stamp of index zero to get time scale in + # seconds, Michael + + time_sec = data_frame.TIMESTAMP - data_frame.TIMESTAMP[0] + + # set scaling of time axis depending on the time unit given in parameter list, Michael + # default unit is minutes, Michael + time_vals = self.scaling_time_axes(time_sec, time_unit) + + func, popt = self.choose_fit(time_vals, data_frame.S21_PHASE) + + annotate_string_reg_coeff = self.plot_fitted_func_param(func, *popt, time_unit = time_unit) + + self.postplot.edit_annotation_in_plot(annotate_string_reg_coeff) + + + + + def plot_phase_curve_fit(self, data_frame, time_unit = 'min', state = False): + + if state == True: + + + phases = data_frame.S21_PHASE.tolist() + self.phase_t0 = phases[0] + + self.K_phases = np.median(data_frame.S21_PHASE) + + # time stamp of index = 0 is the start point of time axis, Michael + # substract all other timestamps with time stamp of index zero to get time scale in + # seconds, Michael + + time_sec = data_frame.TIMESTAMP - data_frame.TIMESTAMP[0] + + # set scaling of time axis depending on the time unit given in parameter list, Michael + # default unit is minutes, Michael + time_vals = self.scaling_time_axes(time_sec, time_unit) + + # call chose fit function + func, popt = self.choose_fit(time_vals, data_frame.S21_PHASE) + + # genrate trace for curvefit + fit_phaeses = func(time_vals, *popt) + + # determine max, min of measured phases + min_phases, max_phases = self.get_extended_min_max(data_frame.S21_PHASE) + + # determine max, min of curvefit + min_fit_phases, max_fit_phases = self.get_extended_min_max(fit_phaeses) + + # determine min of measure phases and min of curvefit + minimum = min(min_phases, min_fit_phases) + + # determine max of measured phases and max of curvefit + maximum = max(max_phases, max_fit_phases) + + self.postplot.add_curvefit_to_plot(xvals =time_vals, \ + yvals = func(time_vals, *popt), \ + y_lim =[minimum, maximum], \ + trace_color = 'green', \ + trace_label = 'fitted by\n' + func.__name__) + + else: + self.postplot.add_curvefit_to_plot(xvals =[], \ + yvals = [], \ + y_lim = None, \ + trace_color = 'None', \ + trace_label = '') + + + +# step response of PT1 with initial behavior , Michael + def PT1(self, x, K, T1): + return K * (1 - np.exp(-x/T1)) + self.phase_t0 * np.exp(-x/T1) + + # step response of PT2 (D > 1) with initial behavior + def aperiodicPT2(self, x, K, T1, T2,): + return K + self.phase_t0* np.exp(-x/T1)/(T2-T1) - K *T1* np.exp(-x/T1)/(T2-T1) \ + - self.phase_t0 * np.exp(-x/T2)/(T2-T1) + K* T2* np.exp(-x/T2)/(T2-T1) + + # step response of PT2 (D = 1) with initial behavior, Michael + def aper_borderPT2(self, x, K, T): + return K + self.phase_t0*np.exp(-x/T) - K * np.exp(-x/T) + self.phase_t0*np.exp(-x/T)*x/T + \ + - K * np.exp(-x/T) * x/T + + # step response of PT2 (0 < D < 1) with initial behavior, Michael + def periodicPT2(self, x, D, T): + return self.K_phases + (self.phase_t0 -self.K_phases)* \ + np.exp(-D*x/T)*np.cos(np.sqrt(1-np.square(D))*x/T) + \ + (self.phase_t0 - self.K_phases) * D * \ + np.exp(-D*x/T)*np.sin(np.sqrt(1-np.square(D))*x/T)/np.sqrt(1-np.square(D)) + + # step response series of PT2 ( D = 1) and PT1 with initial behavior, Michael + def aper_borderPT2_PT1(self, x, a, b, c, d, T1, T2): + return a + b*np.exp(-x/T1)+c*x*np.exp(-x/T1) + d*np.exp(-x/T2) + + # method for cuvefit, Michael + # two different algorithms for curvefit are use depending on used step response for fit, Michael + def curvefit(self, func, time, phases): + + if func.__name__ == 'periodicPT2': + popt, pcov = curve_fit(func, time, phases, method = 'trf', \ + bounds = ([0.001, 0.001], [0.99, np.inf])) + else: + popt, pcov = curve_fit(func, time, phases, method = 'lm') + + return popt, pcov + + # method adds the parameter of the fit with the lowest error to annotation string, Michael + + def plot_fitted_func_param(self, func, *popt, time_unit): + + if func.__name__ == 'PT1': + K = popt[0] + T1 = popt[1] + annotate_string = "K: %.2f\n$T_1$: %.3f %s" %(K, T1, time_unit) + elif func.__name__ == 'aperiodicPT2': + K = popt[0] + T1 = popt[1] + T2 = popt[2] + annotate_string = "K: %.2f\n$T_1$: %.3f %s\n$T_2$: %.3f %s" % (K, T1, time_unit, T2, \ + time_unit) + elif func.__name__ == 'aper_borderPT2': + K = popt[0] + T = popt[1] + annotate_string = "K: %.2f\nT: %.3f %s" %(K, T, time_unit) + elif func.__name__ == 'periodicPT2': + K = self.K_phases + D = popt[0] + T = popt[1] + T_2perc = 4*T/D + annotate_string = "K: %.2f\nD: %.3f\nT: %.3f %s\n$T_E$(2%%): %.2f %s" \ + % (K, D, T, time_unit, T_2perc, time_unit) + elif func.__name__ == 'aper_borderPT2_PT1': + T1 = popt[4] + T2 = popt[5] + annotate_string = "$T_1$: %.3f %s\n$T_2$: %.3f %s" % (T1, time_unit, T2, time_unit) + else: + # default value + annotate_string ='' + return annotate_string + + # calculates the fit for all step responses and calulates the lowest error, Michael + # parameter of the plot with the lowest error is returned by this function, Michael + # if curve fit fails for one stept response it takes the next one and plots in command + # window the functions + # which failes, Michael + def choose_fit(self, time, phases): + popt = [] + perr = [] + used_functions = [] + functions = [self.PT1, self.aper_borderPT2, self.aperiodicPT2, self.periodicPT2, \ + self.aper_borderPT2_PT1] + for func in functions: + try: + # call curvefit method and put the fitted parameter for step response function + # to a list, Michael + popt.append(self.curvefit(func, time, phases)[0]) + # calculation of fitting error + perr.append(np.sqrt(np.square(np.subtract(phases, func(time, *popt[-1]))).mean())) + used_functions.append(func) + except RuntimeError: + print('curvefit of %s fails' %func.__name__) + # get position of parameters in list which has the lowest calculated error, Michael + pos = perr.index(min(perr)) + # return function name and parameter with the lowest calculated error during curvefit, Michael + return used_functions[pos], popt[pos] + + + + # scaling time axes depending on the time unit + def scaling_time_axes(self, time_sec, time_unit): + if time_unit == 'min': + time_vals = time_sec/60 + + elif time_unit == 'hours': + time_vals = time_sec/3600 + + elif time_unit == 'sec': + time_vals = time_sec + + else: + time_vals = time_sec/60 + + return time_vals + + # add 5 % of the distance between min and max to the range + @staticmethod + def get_extended_min_max(array): + distance = array.max() - array.min() + if distance == 0.: + distance = 1 + return array.min()-0.05*distance, array.max()+0.05*distance + + +# for manually redo post plot after measurement has finished and insert analysis function, Michael +if __name__ == '__main__': + + + # set result path for post plot + Results_Path = r'C:\git\climate-lab-test-stand\Python_script\TestData_JBY240' + + + # set time unit for post post plot + # default is minutes if entry in parameterlist left empty + # possible entries: 'min' for minutes, 'hours' for hours and 'sec' for seconds + time_unit = 'min' + + # set storepath for the post plots, Michael + storepath = Results_Path + '\\AnalysisPlots' + + # search all csv files in results folder + csv_file_list = list(Path(Results_Path).glob("**/*.csv")) + + # choose annotation option + plot_correlation_coeff = False + plot_regression_coeff = False + + # activate plot curvefitting + + plot_trace_curvefit = True + + # selction measurment data should be plotted in subplot5 + # 'logger sens' : values for temperature and humidity of logger sensor in + # measurement instrument chmaber + # 'chamber_sens' : values for temepratere and humidity readback from chamber sensors + # of chamber for measurement instruments + # 'heater_dut_chamber': activity of temp heater and hum heater readback from DUT chamber + # This is also the default parameter + + trace_selection = "" + + + # create postplot object, Michael + analysisplot_obj = reg_and_corr(trace_subplot5= trace_selection) + + + # empty data frame for concat the data frames from csv import to plot full transistion + concat_data_frame = pd.DataFrame() + + # plot results for each csv-file + for index, csv_file in enumerate(csv_file_list): + + # import csv-data from csv-files in list, Michael + data_frame = analysisplot_obj.postplot.import_csv(str(csv_file)) + + # determine title of plot from csv-filename, Michael + title = csv_file.name + + # concate datesframe for plotting full transistion data + concat_data_frame = pd.concat([concat_data_frame,data_frame],ignore_index=True, sort = False) + + + # plot frame data + analysisplot_obj.postplot.plot_frame_data(data_frame, title, time_unit) + + + # determine correlation coefficient between func 1 and func 2 and plot coefficient + analysisplot_obj.calc_correlation(func_1 = data_frame.S21_PHASE , \ + func_2 = data_frame.TEMP_DUT, state = plot_correlation_coeff ) + + + + + # start curvefit and determine coefficients best fit function and plot coefficients + analysisplot_obj.calc_regression_coeff_phase_S21(data_frame, time_unit = 'min', \ + state = plot_regression_coeff) + + + analysisplot_obj.plot_phase_curve_fit(data_frame = data_frame, time_unit = time_unit, \ + state = plot_trace_curvefit) + + # set filename of post plot, Michael + filename = str(csv_file.stem) + '.pdf' + + # store post plot under the path taht is set in storepath with the earlier defined filename + analysisplot_obj.postplot.save_fig(storepath, filename) + + + # plot of all steps of a sweep is plotted in one diagram, Michael + # title of this plot will be always full transistion, Michel + analysisplot_obj.postplot.plot_frame_data(concat_data_frame, 'Full Transistion', time_unit) + + analysisplot_obj.plot_phase_curve_fit(data_frame = concat_data_frame, time_unit = time_unit, \ + state = False) + + # filename of plot that contains all steps of sweep is set to FullTransistion, Michael + filename = 'Full_Transistion' + '.pdf' + + # plot with the results of all steps is store under the predefined storpath with the + # earlier defined filename, Michael + analysisplot_obj.postplot.save_fig(storepath, filename) + \ No newline at end of file diff --git a/Python_script/delay_stage.map b/Python_script/delay_stage.map new file mode 100644 index 0000000000000000000000000000000000000000..68daa88209fc68311d81df13247e5a4479fd82e9 --- /dev/null +++ b/Python_script/delay_stage.map @@ -0,0 +1,3 @@ +RB_ENCODER 1 0 4 0 +POSITION_SP 1 4 4 0 +SPEED 1 8 4 0 diff --git a/Python_script/deviceaccess_measurement.py b/Python_script/deviceaccess_measurement.py new file mode 100644 index 0000000000000000000000000000000000000000..31493e72263713edf5b62089f1777578a7e61a7c --- /dev/null +++ b/Python_script/deviceaccess_measurement.py @@ -0,0 +1,44 @@ + +import dut_measurement +import deviceaccess +import numpy + + +class DeviceAccessMeasurement(dut_measurement.DutMeasurement): + def __init__(self, config_data): + + self.deltas = config_data['deltas'] + + deviceaccess.setDMapFilePath(config_data['dmap_file']) + self.device = deviceaccess.Device(config_data['device_alias']) + self.device.open() + + self.reference_signal_names = config_data['reference_signals'] + + self.accessors = {} + for signal_name, register_path in config_data['signals'].items(): + self.accessors[signal_name] = self.device.getScalarRegisterAccessor(numpy.double, register_path) + + def get_dut_measurements(self, set_name): + retval = {} + + for name, accessor in self.accessors.items(): + accessor.read() + retval[name] = accessor[0] + retval['SET_NAME'] = 'default' + return retval + + def get_dut_signal_names(self): + retval = list(self.accessors.keys()) + retval.append('SET_NAME') + return retval + + + def get_dut_reference_signal_names(self): + return self.reference_signal_names + + def get_dut_max_delta_signals(self): + return self.deltas + + def get_measurement_set_names(self): + return ['default'] diff --git a/Python_script/devices.dmap b/Python_script/devices.dmap new file mode 100644 index 0000000000000000000000000000000000000000..7ebc8687f439098c472be133e063419448b8c7f1 --- /dev/null +++ b/Python_script/devices.dmap @@ -0,0 +1,3 @@ +@LOAD_LIB /usr/lib/x86_64-linux-gnu/libChimeraTK-DeviceAccess-DoocsBackend.so +delay_stage (sharedMemoryDummy?map=delay_stage.map) +#delay_stage (doocs?/MY_FACILITY/MY_DEVICE/MY_LOCATION) diff --git a/Python_script/dut_measurement.py b/Python_script/dut_measurement.py new file mode 100644 index 0000000000000000000000000000000000000000..b2f217138468f4fe203e1c9d123e9fb24db3a5ec --- /dev/null +++ b/Python_script/dut_measurement.py @@ -0,0 +1,41 @@ +from abc import ABC, abstractmethod + + +class DutMeasurement(ABC): + """ + Interface for DUT measurements + """ + @abstractmethod + def get_dut_measurements(self, set_name): + """ + Performs the measurements according to the set name and + returns a dictionary with signal 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 + """ + + @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/ext_sensor_channels.json b/Python_script/ext_sensor_channels.json new file mode 100644 index 0000000000000000000000000000000000000000..107a681561d5756a52d863f028a34ef7e76fda23 --- /dev/null +++ b/Python_script/ext_sensor_channels.json @@ -0,0 +1 @@ +{"temp_dut": "0.0", "hum_dut": "0.1", "temp_room": "1.0" , "hum_room": "1.1", "air_press_room": "1.3", "temp_meas_instr": "2.0", "hum_meas_instr": "2.1"} diff --git a/Python_script/external_sensors.py b/Python_script/external_sensors.py new file mode 100644 index 0000000000000000000000000000000000000000..b73b0a43e6c42b40c2fd1cc674e5537f8d753f74 --- /dev/null +++ b/Python_script/external_sensors.py @@ -0,0 +1,79 @@ +from almemo710 import almemo710 +from almemo2490 import almemo2490 + +import pandas as pd +import climate_chamber_dummy + + +class ExternalSensorsDummy: + def __init__(self): + self.climate_chamber_dummy = climate_chamber_dummy.get_climate_chamber_dummy([0.2, 0.1]) + + def get_sensor_values(self): + # FIXME: Do we have to run the simulation here or do we piggy-back on the chamber probably also being dummy? + temp = self.climate_chamber_dummy.read_temperature()[0] + humidity = self.climate_chamber_dummy.read_humidity()[0] + # half a degree thermal cycle of 10 minutes from the room's air conditioning + room_temp = 22.97 + abs((self.climate_chamber_dummy.last_simulation_time % 600)-300)/600 + # Use the same DUT chamber values as the simulation, but constant measurement instrument chamber and room + # temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room + return temp, room_temp, 23.0, humidity, 62.2, 45.0, 1013 + + def close(self): + pass + + +def create_sensors(logger_model, logger_address, ext_sensor_channels): + if logger_address == 'localhost': + return ExternalSensorsDummy() + else: + return ExternalSensors(logger_model, logger_address, ext_sensor_channels) + + +class ExternalSensors: + def __init__(self, logger_model, logger_address, ext_sensor_channels): + if logger_model == '710': + self.logger = almemo710(ip=logger_address, timeout=10) + elif logger_model == '2490': + self.logger = almemo2490(ip=logger_address, timeout=10) + else: + raise Exception('Unknown logger type: '+logger_model) + + self.logger_model = logger_model + self.ext_sensor_channels = ext_sensor_channels + # initialise the logger + self.logger.request_sens_channel_list() + + def get_sensor_values(self): + """ + Returns: (temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room) + """ + sens_vals_dict = dict() + self.logger.request_meas_vals_all_channels() + + for key, value in self.ext_sensor_channels.items(): + sens_vals_dict[key] = self.logger.fetch_channel_param_from_meas_buffer(pattern=value)[0].meas_val + + # set values from series to variables + sens_vals_series = pd.Series(sens_vals_dict) + + # set series items for sensor value to matching variable + temp_dut = sens_vals_series.temp_dut + temp_room = sens_vals_series.temp_room + hum_dut = sens_vals_series.hum_dut + hum_room = sens_vals_series.hum_room + air_press_room = sens_vals_series.air_press_room + + if self.logger_model == '710': + temp_meas_instr = sens_vals_series.temp_meas_instr + hum_meas_instr = sens_vals_series.hum_meas_instr + + else: + temp_meas_instr = -30 + hum_meas_instr = 0 + + return temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room + + def close(self): + self.logger.close() + diff --git a/Python_script/first_tempsweep.txt b/Python_script/first_tempsweep.txt index a9dfacdb85f2b2fbbd6ab0892455e61ab9031ac7..fc6d2565583ca39f4fa952043e9ac1fbb1ad6886 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 +20 31 1 35 100 10 diff --git a/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_25.0deg_55.0rh.pdf b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_25.0deg_55.0rh.pdf new file mode 100644 index 0000000000000000000000000000000000000000..242ecc3ec97feeeda571032569746e05eee9a1fd Binary files /dev/null and b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_25.0deg_55.0rh.pdf differ diff --git a/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_30.0deg_55.0rh.pdf b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_30.0deg_55.0rh.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d5d310f82b79d2dff7b41a68060cbf094ef1e05d Binary files /dev/null and b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_30.0deg_55.0rh.pdf differ diff --git a/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/FullTransistion.pdf b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/FullTransistion.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1de26f13c30314d4ca0d09e9983587c84a664321 Binary files /dev/null and b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/FullTransistion.pdf differ diff --git a/Python_script/play_with_almemo2490.py b/Python_script/play_with_almemo2490.py new file mode 100644 index 0000000000000000000000000000000000000000..fecf80911ac46a7a56d696eebf6807bc6df218d4 --- /dev/null +++ b/Python_script/play_with_almemo2490.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 2 16:55:57 2023 + +@author: michael pawelzik +""" + +# import module with python class for ahlborn +import almemo2490 + +# variables for selection in used methods +# numbmer format: 'xy' where x has to be [0-9] and y has be [0-1] +# x is the measurement channel and y is the sensor port +sel_channel_no = '10' +sel_channel_name = 'T,t' +item = 0 + +try: + + # create object for python class of almemo710 + almemo2490_obj = almemo2490.almemo2490(ip='192.168.115.44') + + # set date to catual date + almemo2490_obj.set_date() + + #set time to actual time + almemo2490_obj.set_time() + + # request date from almemo 710 + date = almemo2490_obj.get_date() + + # request time from almemo 710 + time = almemo2490_obj.get_time() + + # set name of measurment channel 0.1 from almemo 710 + almemo2490_obj.set_channel_name(sel_channel_no,sel_channel_name) + + # request name of measurement channel from almemo 710 + channel_name = almemo2490_obj.get_channel_name(sel_channel_no) + + # request list of all sensors channels that are currently active + # store this data in data frame of class_object + almemo2490_obj.request_sens_channel_list() + + # trigger single measurement an request the measured values for all measurment channels + # of almemo 710 + # store measured values in data frame of python class + almemo2490_obj.request_meas_vals_all_channels() + + # get measuerement buffer of class object + meas_buffer = almemo2490_obj.meas_buffer + + # fetch parameter of selected measurement channel from meas_buffer of class object + # input of channel name or channel number is possible + # numbmer format: 'xy' where x has to be [0-9] and y has be [0-1] + meas_channel_data, num_matches = almemo2490_obj. \ + fetch_channel_param_from_meas_buffer(pattern = sel_channel_name, index = item) + + # get measurement date from selected measurement channel + meas_date = meas_channel_data.meas_date + + # get measurement time from selected measurement channel + meas_time = meas_channel_data.meas_time + + # get measurement value from selected measurement channel + meas_value = meas_channel_data.meas_val + + # get sensor unit from selected measurement channel + channel_unit = meas_channel_data.channel_unit + + # get channel name from selected measurement channel + channel_name = meas_channel_data.channel_name + + # get channel number from selected measurement channel + sens_channel = meas_channel_data.sens_channel + +finally: + + # close telnet connection + almemo2490_obj.close() + diff --git a/Python_script/play_with_almemo710.py b/Python_script/play_with_almemo710.py new file mode 100644 index 0000000000000000000000000000000000000000..79c88760e86aecfe312adc66d682e804c5aa79f5 --- /dev/null +++ b/Python_script/play_with_almemo710.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 2 16:55:57 2023 + +@author: michael pawelzik +""" + +# import module with python class for ahlborn +import almemo710 + +# variables for selection in used methods +# numbmer format: 'x.y' where x has to be [0-9] and y has be [0-9] +# x is the sensor port and y is the measurement channel +sel_channel_no = '0.1' +sel_channel_name = 'T,t' +item = 0 + +try: + + # create object for python class of almemo710 + almemo710_obj = almemo710.almemo710(ip='192.168.115.94') + + # set date to catual date + almemo710_obj.set_date() + + #set time to actual time + almemo710_obj.set_time() + + # request date from almemo 710 + date = almemo710_obj.get_date() + + # request time from almemo 710 + time = almemo710_obj.get_time() + + # set name of measurment channel 0.1 from almemo 710 + almemo710_obj.set_channel_name(sel_channel_no,sel_channel_name) + + # request name of measurement channel from almemo 710 + channel_name = almemo710_obj.get_channel_name(sel_channel_no) + + # request list of all sensors channels that are currently active + # store this data in data frame of class_object + almemo710_obj.request_sens_channel_list() + + # trigger single measurement an request the measured values for all measurment channels + # of almemo 710 + # store measured values in data frame of python class + almemo710_obj.request_meas_vals_all_channels() + + # get measuerement buffer of class object + meas_buffer = almemo710_obj.meas_buffer + + # fetch parameter of selected measurement channel from meas_buffer of class object + # input of channel name or channel number is possible + # numbmer format: 'x.x' where is has to be [0-9] + meas_channel_data, num_matches = almemo710_obj. \ + fetch_channel_param_from_meas_buffer(pattern = sel_channel_name, index = item) + + # get measurement date from selected measurement channel + meas_date = meas_channel_data.meas_date + + # get measurement time from selected measurement channel + meas_time = meas_channel_data.meas_time + + # get measurement value from selected measurement channel + meas_value = meas_channel_data.meas_val + + # get sensor unit from selected measurement channel + channel_unit = meas_channel_data.channel_unit + + # get channel name from selected measurement channel + channel_name = meas_channel_data.channel_name + + # get channel number from selected measurement channel + sens_channel = meas_channel_data.sens_channel + +finally: + + # close telnet connection + almemo710_obj.close() + diff --git a/Python_script/prototype.py b/Python_script/prototype.py index cc7a055f9220c13e7aadee5e5b709aa0b3127650..cc3a5280c0bdad75fbe1132b46ffae0de5a15c19 100755 --- a/Python_script/prototype.py +++ b/Python_script/prototype.py @@ -1,70 +1,99 @@ #!/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 deviceaccess_measurement import virtual_time import json import MeasurementPlot import sys import analysis import threading +import external_sensors +import PostPlot +import os +# 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): + + 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.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 + self.temp_chamber_meas_instr=temp_chamber_meas_instr + self.hum_dut = hum_dut + self.hum_room = hum_room + self.air_press_room = air_press_room + self.hum_meas_instr = hum_meas_instr + self.hum_chamber_meas_instr = hum_chamber_meas_instr class Measurements: - def __init__(self, chamber_address, vna_address, output_basename, standby, config_data): + def __init__(self, config_data, output_basename, standby, ext_sensor_channels): + #create all members that need to be closed to be able to write a close function + #which does not throw because of unknown variables + self.chamber = None + self.instr_chamber = None + self.dut = None + self.ext_sensors = None + 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.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.vna = VNA.create_vna(vna_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) + # 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) + elif config_data['dut']['type'] == 'DeviceAccess': + self.dut = deviceaccess_measurement.DeviceAccessMeasurement(config_data['dut']) + 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'], 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.measurement_plot = MeasurementPlot.MeasurementPlot() - self.data_collection = [] + self.postplot_obj = None + + self.measurement_plot = MeasurementPlot.MeasurementPlot(self.dut.get_dut_reference_signal_names(), + trace_subplot5=config_data['trace_subplot5']) + self.data_collection_for_online_plot = [] def perform_measurements(self, sweep_file): with open(sweep_file) as file: @@ -79,15 +108,16 @@ class Measurements: self.measurement_plot.fig.suptitle("Measurement "+str(measurement_number)+': ' + str(next_temp) + ' degC, ' + str(next_hum) + ' rel. hum.', color="red") - self.perform_single_measurement(self.output_basename+'_'+str(measurement_number)+'.csv', next_temp, next_hum, - next_soaking, next_reads) + self.perform_single_measurement(self.output_basename+'_'+str(measurement_number)+'.csv', next_temp, + next_hum, next_soaking, next_reads) measurement_number += 1 except KeyboardInterrupt: pass except MeasurementPlot.PlottingError: # Just the plotting failed, probably because the window was closed - # The measurement was partly done and should be plottet, so the following code has to see the correct number of successful measurements + # The measurement was partly done and should be plotted, so the following code has to see the correct + # number of successful measurements measurement_number += 1 plt.close() @@ -130,10 +160,8 @@ class Measurements: pass except MeasurementPlot.PlottingError: # Remove the remaining measurements from the list. - # One measurement was partly done and should be plottet, so we leave 'val' in the list - del sweep_values[sweep_values.index(val)+1 : ] - - + # One measurement was partly done and should be plotted, so we leave 'val' in the list + del sweep_values[sweep_values.index(val)+1:] plt.close() return sweep_values @@ -141,6 +169,11 @@ class Measurements: # wrapper function which calls the impl in a separate thread and runs the # plotting loop 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. + """ measurement_thread = threading.Thread(target=self.perform_single_measurement_impl, args=(output, target_temp, target_hum, soaking_time, n_stable_reads)) @@ -151,13 +184,15 @@ class Measurements: def perform_single_measurement_impl(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', '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() - self.data_collection = [] + self.data_collection_for_online_plot = [] plt.ion() set_const_response = self.chamber.set_const((target_temp, target_hum)) @@ -168,16 +203,17 @@ class Measurements: number_of_soaking_reads = soaking_time / self.sleep_time + 1 do_another_measurement = True - #next_read_time is the starttime of the second read (i.e. read after this one). The time of - #this read (i.e. the first read in the measurement) is now(). + # next_read_time is the starttime of the second read (i.e. read after this one). The time of + # this read (i.e. the first read in the measurement) is now(). 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() + # 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 @@ -185,45 +221,41 @@ 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(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))) + ' °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]) - - 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] + 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]: + reference_values = [] + for signal_queue in dut_signal_queues: + reference_values.append(signal_queue[-1]) print('SOAKING FINISHED!') break else: @@ -232,24 +264,35 @@ 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)) - mag = self.calculate_mean_magnitude(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]) + 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 (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 @@ -258,14 +301,15 @@ 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 |\ - MAGNITUDE_STABLE | PHASE_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) self.measurement_plot.stop() @@ -274,73 +318,58 @@ class Measurements: 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(',') - 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") + perc_temp_heater, perc_hum_heater = self.chamber.get_heater_percentage() - return MeasurementData(int(self.clock.time()), temp, hum, power, frequency, s11, s21, s12, s22) + dut_data = self.dut.get_dut_measurements(measurement_set_name) - def store_and_plot_data(self, target_temp, target_hum, data, equi_indicator): + 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, 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) + + 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, '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, - 'S11_PHASE': self.calculate_mean_phase(data.s11), - 'S11_MAGNITUDE': self.calculate_mean_magnitude(data.s11), - 'S21_PHASE': self.calculate_mean_phase(data.s21), - 'S21_MAGNITUDE': self.calculate_mean_magnitude(data.s21), - 'S12_PHASE': self.calculate_mean_phase(data.s12), - 'S12_MAGNITUDE': self.calculate_mean_magnitude(data.s12), - 'S22_PHASE': self.calculate_mean_phase(data.s22), - 'S22_MAGNITUDE': self.calculate_mean_magnitude(data.s22) + '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, + 'HUM_DUT': data.hum_dut, + 'HUM_ROOM': data.hum_room, + 'HUM_MEAS_INSTR': data.hum_meas_instr, + '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), } - self.data_collection.append(measurement) - data_frame = pd.DataFrame(self.data_collection) + measurement.update(data.dut_data) + 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_in_other_thread(data_frame) 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_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 @@ -350,11 +379,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 @@ -366,18 +395,50 @@ class Measurements: return (target_hum-self.max_delta_hum <= float(readback_hum)) and \ (float(readback_hum) <= target_hum+self.max_delta_hum) -def plot_output(output_basename, measurements_appendices, show_blocking_plot, title = ''): + #convecinece function to close everything in case an exception escapes + #FIXME: needs to be called by code using the Measurements object + def close(self): + if self.chamber is not None: + self.chamber.close() + if self.instr_chamber is not None: + self.inst_chamber.close() + if self.dut is not None: + self.dut.close() + if self.ext_sensors is not None: + self.ext_sensors.close() + + +def plot_output(output_basename, measurements_appendices, show_blocking_plot, config_data, ext_sens_data, + measurement_set_names, reference_signal_names, title=''): + + time_unit = str(config_data['time_unit']) + list_of_frames = [] - for m in measurements_appendices: + storepath = os.path.join(os.getcwd(), 'PostPlots') + + post_plot = PostPlot.PostPlot(reference_signal_names, trace_subplot5=config_data['trace_subplot5']) + for index, m in enumerate(measurements_appendices): measurement_name = output_basename+'_'+str(m) list_of_frames.append(pd.read_csv(measurement_name+'.csv')) + 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) + + 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() - plot = MeasurementPlot.MeasurementPlot(title) + + plot = MeasurementPlot.MeasurementPlot(reference_signal_names, title = title, trace_subplot5 = \ + config_data['trace_subplot5'] ) plot.draw_in_this_thread(combined_data_frame, output_basename + '_graph.pdf') + def run_temperature_sweep_from_file(temperature_sweep_file, meas): with open(temperature_sweep_file) as file: try: @@ -398,12 +459,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='') @@ -428,34 +483,46 @@ if __name__ == '__main__': print('Argument error: Either \'file\' or \'temperaturesweepfile\' must be specified.') sys.exit(-1) + time_string = time.strftime("%Y_%m_%d-%H_%M_%S") if not args.output: - output_basename = time.strftime("%Y_%m_%d-%H_%M_%S") + "_results" + output_basename = time_string + "_results" 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: config_data = json.load(f) - mes = Measurements(args.chamber, args.vna, output_basename, args.standby, config_data) + with open('ext_sensor_channels.json', 'r') as f2: + ext_sensor_channels = json.load(f2) + + mes = None try: + mes = Measurements(config_data, output_basename, args.standby, ext_sensor_channels) if args.file: n_measurements = mes.perform_measurements(args.file) - plot_output(output_basename, range(n_measurements), args.plot, output_basename) + plot_output(output_basename, range(n_measurements), args.plot, config_data, ext_sensor_channels, + mes.dut.get_measurement_set_names(), mes.dut.get_dut_reference_signal_names(), output_basename) if args.temperaturesweepfile: temperatures, humidity = run_temperature_sweep_from_file(args.temperaturesweepfile, mes) - #run analysis here + # 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') - plot_output(output_basename, temp_extensions, args.plot, output_basename + ': Temperature sweep ' + + analysis.plot_sweep(temperatures, [humidity]*len(temperatures), output_basename, 'temperature', + mes.dut.get_measurement_set_names(), mes.dut.get_dut_reference_signal_names(), + analysis_config={'type': 'default', 'normalise': [False, False], + 'extra_signal_names': [], + 'dut_name': 'measurement from prototype script', + 'time_string': time_string}) + plot_output(output_basename, temp_extensions, args.plot, config_data, ext_sensor_channels, + mes.dut.get_measurement_set_names(), mes.dut.get_dut_reference_signal_names(), + output_basename + ': Temperature sweep ' + str(temperatures[0]) + ' degC to ' + str(temperatures[-1]) + ' degC') - print(str(temp_extensions)) finally: - mes.chamber.close() + if mes is not None: + mes.close() # mes.plot_output(output) - diff --git a/Python_script/rerun_analysis.py b/Python_script/rerun_analysis.py new file mode 100755 index 0000000000000000000000000000000000000000..622383621866435b5eaac2070b5106ac065753b8 --- /dev/null +++ b/Python_script/rerun_analysis.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +import os + +import analysis +import numpy as np +import datetime + +basename = '2023_10_26-17_55_50_results' +temperatures = np.arange(10., 32.+1., 2.) +humidities = [55.]*len(temperatures) + +cable_length = 10 +dut_name = "my test cable" + +time_string = 're-analysis at ' + str(datetime.datetime.now()) +sweep_type = 'temperature' +measurement_sets = ['1.3GHz', '1.0GHz', '3.0GHz', '6.0GHz', '10.0GHz'] +reference_signal_names = ['S21_PHASE', 'S21_MAGNITUDE'] + +data_folder = os.path.join('measurements', basename) +os.chdir(data_folder) + +analysis.plot_sweep(temperatures, humidities, basename, sweep_type, measurement_sets, reference_signal_names, + analysis_config={'type': 'rf_cable', 'normalise': [True, False], 'cable_length': cable_length, + 'extra_signal_names': ['RF_FREQUENCY'], 'dut_name': dut_name, + 'time_string': time_string}) diff --git a/Python_script/test_stand_parameter.json b/Python_script/test_stand_parameter.json index 0ba7d7240650e0f48766a60c67034aad40f822f4..ba4b2b8e58abb56b8046aab8412b7ea3a05c443d 100644 --- a/Python_script/test_stand_parameter.json +++ b/Python_script/test_stand_parameter.json @@ -1 +1,21 @@ -{"delta_temp": 0.1, "delta_hum": 1, "delta_mag": 0.1 , "delta_phase": 0.02, "sleep_time": 10.0, "frequency": 1300000000, "vna_config_file": "climate-lab.znxml","chamber_ip":"localhost", "vna_ip":"localhost", "data_folder":"measurements"} +{ + "delta_temp": 0.1, + "delta_hum": 1, + "dut": { + "type": "VNA", + "delta_mag": 0.13, + "delta_phase": 1.5, + "reference_signals" : ["S21_PHASE", "S21_MAGNITUDE"], + "frequencies": [1300000000, 1000000000, 3000000000, 6000000000, 10000000000], + "vna_ip": "mskzna43-lab", + "vna_config_file": "CALSetup310M.znxml" + }, + "sleep_time": 10, + "chamber_ip": "mskclimate3", + "instr_chamber_ip": "mskclimate2", + "data_folder": "measurements", + "logger_ip": "mskdataloggerts", + "time_unit": "min", + "trace_subplot5": "logger_sens", + "logger_model": "710" +} diff --git a/Python_script/test_stand_parameters_deviceaccess.json b/Python_script/test_stand_parameters_deviceaccess.json new file mode 100644 index 0000000000000000000000000000000000000000..1e43ea3154eafbb0c851099a37ee83d8dc3f5a27 --- /dev/null +++ b/Python_script/test_stand_parameters_deviceaccess.json @@ -0,0 +1,25 @@ +{ + "delta_temp": 0.1, + "delta_hum": 1, + "dut": { + "type": "DeviceAccess", + "deltas": [0.13, 1.5], + "reference_signals" : ["encoder_position","target_position"], + "FIXME_dmap_file" : "FIXME has to be absolute path at the moment FIXME", + "dmap_file" : "/home/killenb/climate-lab-test-stand/Python_script/devices.dmap", + "device_alias": "delay_stage", + "signals" : { + "encoder_position" : "RB_ENCODER", + "target_position" : "POSITION_SP", + "speed": "SPEED" + } + }, + "sleep_time": 10, + "chamber_ip": "localhost", + "instr_chamber_ip": "localhost", + "data_folder": "measurements", + "logger_ip": "localhost", + "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 0000000000000000000000000000000000000000..3a80d112c9d5d324295413b852c6f5117d05e7eb --- /dev/null +++ b/Python_script/vna_measurement.py @@ -0,0 +1,107 @@ +import VNA +import pyvisa +import time +import numpy +import math +import cmath + +import dut_measurement + + +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.reference_signal_names = config_data['reference_signals'] + + self.vna = VNA.create_vna(config_data['vna_ip'], target_accuracy) + + 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, 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.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") + 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), + 'SET_NAME': set_name} + + # 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', 'SET_NAME'] + + def get_dut_reference_signal_names(self): + return self.reference_signal_names + + 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) + + def get_measurement_set_names(self): + return list(self.frequencies.keys())