diff --git a/User_Interface.py b/User_Interface.py index b60490f..07eddd1 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -660,7 +660,7 @@ class ExecuteCSVMode(Frame): g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later # create separate thread to run sequence execution in: - self.csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) + self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller) self.csv_thread.start() # start thread def stop_run(self): diff --git a/cage_func.py b/cage_func.py index 188c4c4..224ed44 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,3 +1,6 @@ +# this file contains all classes and functions directly related to the operation of the helmholtz test stand +# the two main classes are Axis and ArduinoCtrl, see their definitions for details + import numpy as np import serial import traceback @@ -6,24 +9,26 @@ from tkinter import messagebox from User_Interface import ui_print from pyps2000b import PS2000B from Arduino import Arduino -# noinspection PyPep8Naming import config_handling as config import globals as g class Axis: + # Main class representing an axis (x,y,z) of the test stand + # contains static and dynamic status information about this axis and methods to control it + def __init__(self, index, device, PSU_channel, arduino_pin): # static information - self.index = index - self.device = device # power supply object (PS2000B class) - self.channel = PSU_channel # power supply unit channel (1 or 2) + self.index = index # index of this axis, 0->X, 1->Y, 2->Z + self.device = device # power supply object for this axis (PS2000B class) + self.channel = PSU_channel # power supply unit channel (0 or 1) self.ardPin = arduino_pin # output pin on the arduino for switching polarity on this axis - self.name = g.AXIS_NAMES[index] - self.port = g.PORTS[index] + self.name = g.AXIS_NAMES[index] # get name of this axis from list in globals.py (e.g. "X-Axis" + self.port = g.PORTS[index] # get serial port of this axis PSU + # read static information from the configuration object (which has read it from the config file or settings) self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT)) - self.max_watts = float(config.read_from_config(self.name, "max_watts", config.CONFIG_OBJECT)) self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT)) self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT)) diff --git a/csv_logging.py b/csv_logging.py index 3bdf2d5..881b437 100644 --- a/csv_logging.py +++ b/csv_logging.py @@ -9,9 +9,9 @@ from tkinter import filedialog from tkinter import messagebox import User_Interface as ui -log_data = pd.DataFrame() # pandas data frame containing logged data +log_data = pd.DataFrame() # pandas data frame containing the logged data, in-program representation of csv/excel data unsaved_data = False # Bool to indicate if there is unsaved data, set to True each time a datapoint is logged -zero_time = datetime.now() +zero_time = datetime.now() # set reference for timestamps in log file, reset when log_data is cleared and restarted # create dictionary with all value handles that could be logged # Key: String that is displayed in UI and column headers. Also serves as handle to access dictionary elements. @@ -39,20 +39,23 @@ def triple_list(key_list): # creates list with each entry of key_list tripled w return new_list -def log_datapoint(key_list): # ToDo: comments - global log_data - global unsaved_data - date = datetime.now().date() - time = datetime.now().strftime("%H:%M:%S,%f") - t = (datetime.now() - zero_time).total_seconds() - data = [[date, time, t]] - for key in key_list: - for axis in g.AXES: - data[0].append(getattr(axis, axis_data_dict[key])) # get value - column_names = ["Date", "Time", "t (s)", *triple_list(key_list)] - new_row = pd.DataFrame(data, columns=column_names) - log_data = log_data.append(new_row, ignore_index=True) - unsaved_data = True +def log_datapoint(key_list): # logs a single row of data into the log_data DataFrame + # key_list determines what data is logged + global log_data # get global dataframe with logged data + global unsaved_data # get global variable that indicates if there is unsaved data + date = datetime.now().date() # get current date + time = datetime.now().strftime("%H:%M:%S,%f") # get string with current time in correct format + t = (datetime.now() - zero_time).total_seconds() # calculate timestamp relative to the start of the logging + data = [[date, time, t]] # initialize new data row with timestamps + for key in key_list: # go through the list telling us what data to log + for axis in g.AXES: # log this data for each axis + # get relevant value from the correct AXIS object and append to new data row: + data[0].append(getattr(axis, axis_data_dict[key])) + + column_names = ["Date", "Time", "t (s)", *triple_list(key_list)] # create list with the correct column headers + new_row = pd.DataFrame(data, columns=column_names) # create data frame containing the new row + log_data = log_data.append(new_row, ignore_index=True) # append the new data frame to the logged data + unsaved_data = True # tell other program parts that there is now unsaved data def select_file(): # select a file to write logs to @@ -90,4 +93,4 @@ def write_to_file(dataframe, filepath): def clear_logged_data(): # clears all logged data from data frame global log_data # get global variable - log_data = pd.DataFrame() # reset to an empty data frame, i.e. clear all logged data + log_data = pd.DataFrame() # reset to an empty data frame, i.e. clearing all logged data diff --git a/csv_threading.py b/csv_threading.py index cda0a42..cf57adb 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,3 +1,6 @@ +# this file contains code for executing a sequence of magnetic fields from a csv file +# to do this without crashing the UI it has to run in a separate thread using the threading module + import time import pandas from threading import * @@ -11,100 +14,108 @@ import globals as g class ExecCSVThread(Thread): - def __init__(self, threadID, array, parent, controller): + # main class for executing a CSV sequence + # it inherits the threading.Thread class, enabling sequence execution in a separate thread + + def __init__(self, array, parent, controller): Thread.__init__(self) - self.threadID = threadID self.array = array # numpy array containing data from csv to be executed - self.parent = parent # object from which this is called - self.controller = controller # object on which mainloop() is running, usually main window + self.parent = parent # object from which this class is called, here the ExecuteCSVMode object of the UI + self.controller = controller # object on which mainloop() is running, usually the main UI window - self.__stop_event = Event() + self.__stop_event = Event() # event which can be set to stop the thread execution if needed - def run(self): + def run(self): # called to start the execution of the thread ui.ui_print("Starting Sequence Execution...") self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence - # reset buttons on UI: - if not g.exitFlag: # program window is open + # when the sequence has ended, reset buttons on the UI: + if not g.exitFlag: # main window is open self.parent.select_file_button["state"] = "normal" self.parent.execute_button["state"] = "normal" self.parent.stop_button["state"] = "disabled" self.parent.reinit_button["state"] = "normal" - def stop(self): # stop thread execution + def stop(self): # stop thread execution, can be called from another thread to kill this one self.__stop_event.set() - def stopped(self): + def stopped(self): # returns true if the thread has been stopped, used to check if a run should continue return self.__stop_event.is_set() def execute_sequence(self, array, delay, parent, controller): + # main execution method of the class # runs through array with times and desired fields and commands test stand accordingly # array format: [time (s), xField (T), yField (T), zField (T)] - # decimal commas - # all times in seconds - func.power_down_all() # sets outputs to 0 before starting + func.power_down_all() # sets outputs on PSUs to 0 and Arduino pins to LOW before starting t_zero = time.time() # set reference time for start of run # Check if everything is properly connected: - all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), + parent.arduino_override.get()) # True or False depending on devices status, checks for some devices may be overridden by user - i = 0 + i = 0 # index of the current array row while i < len(array) and all_connected and not self.stopped() and not g.exitFlag: # while array is not finished, devices are connected, user has not cancelled and application is running - t = time.time() - t_zero # get relative time + t = time.time() - t_zero # get time relative to start of run if t >= array[i, 0]: # time for this row has come - g.threadLock.acquire() # execute the next few lines before going back to the main thread + g.threadLock.acquire() # execute all lines until threadLock.release() before going back to main thread - field_vec = array[i, 1:4] # extract desired field vector - ui.ui_print("%f s: t = %0.2f s, target field vector = " # ToDo: better printing - % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") - func.set_field(field_vec) # send field vector to test stand - ui.ui_print(time.time() - t_zero) - controller.StatusDisplay.update_labels() # update status display after change + # check if everything is still connected before sending commands: + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), + parent.arduino_override.get()) + if all_connected: + field_vec = array[i, 1:4] # extract desired field vector + ui.ui_print("%0.5f s: t = %0.2f s, target field vector =" + % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") + func.set_field(field_vec) # send field vector to test stand + controller.StatusDisplay.update_labels() # update status display after change - # log change to the log file if user has selected event logging in the Configure Logging window - logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator - if logger.event_logging: # data should be logged when test stand is commanded - logger.log_datapoint() # log data + # log change to the log file if user has selected event logging in the Configure Logging window + logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator + if logger.event_logging: # data should be logged when test stand is commanded + logger.log_datapoint() # log data - i = i + 1 # next row + i = i + 1 # next row g.threadLock.release() # allow going back to main thread now - elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep - pass - else: # sleep to give other threads time to run - time.sleep(delay) - - # check again if everything is connected before starting next loop run: - all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), - parent.arduino_override.get()) + elif t <= array[i, 0] - delay - 0.02: # is there enough time to sleep before the next row? + time.sleep(delay) # sleep to give other threads time to run if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption ui.ui_print("Sequence executed, powering down channels.") elif all_connected: # interrupted by user ui.ui_print("Sequence cancelled, powering down channels.") - elif not self.stopped() and not g.exitFlag: # interrupted by device error + elif not all_connected: # interrupted by device error ui.ui_print("Error with at least one device, sequence aborted.") - messagebox.showinfo("Device Error!", "Error with at least one device, sequence aborted.") + messagebox.showwarning("Device Error!", "Error with at least one device, sequence aborted.") + else: # if this happens there is a mistake in the logic above, it really should not + # tell the user something weird happened: + ui.ui_print("Encountered unexpected sequence end state:" + "\nThread Stopped:", self.stopped(), ", Application Closed:", g.exitFlag, + ", Devices connected:", all_connected) + messagebox.showwarning("Unexpected state", + "Encountered unexpected sequence end state, see console output for details.") + func.power_down_all() # set currents and voltages to 0, set arduino pins to low -def read_csv_to_array(filepath): +def read_csv_to_array(filepath): # convert a given csv file to a numpy array # csv format: time (s); xField (T); yField (T); zField (T) (german excel) # decimal commas - file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file + file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file without column headers array = file.to_numpy() # convert csv to array return array -def check_array_ok(array): # check if any magnetic fields in an array exceed the limits +def check_array_ok(array): + # check if any magnetic fields in an array exceed the test stand limits and if so display a warning message values_ok = True - for i in [0, 1, 2]: # go through axes - max_val = g.AXES[i].max_comp_field[1] # get limits + for i in [0, 1, 2]: # go through axes/columns + max_val = g.AXES[i].max_comp_field[1] # get limits the test stand can do min_val = g.AXES[i].max_comp_field[0] data = array[:, i + 1] # extract data for this axis from array # noinspection PyTypeChecker @@ -115,21 +126,21 @@ def check_array_ok(array): # check if any magnetic fields in an array exceed th "\nSee plot and check values in csv.") -def plot_field_sequence(array, width, height): # create plot of fixed size from array +def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array # ToDo (optional): polar plots, plots of angle... # ToDo (optional): show graphs as steps (as performed by test stand) - fig_dpi = 100 # set figure resolution + fig_dpi = 100 # set figure resolution (dots per inch) px = 1/fig_dpi # get pixel to inch size conversion figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # create figure with correct size # noinspection PyTypeChecker,SpellCheckingInspection axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) # create subplots with shared axes - figure.suptitle("Magnetic Field Sequence") + figure.suptitle("Magnetic Field Sequence") # set figure title t = array[:, 0] # extract time column for i in [0, 1, 2]: # go through all three axes - data = array[:, i + 1] * 1e6 # extract field column of this axis + data = array[:, i + 1] * 1e6 # extract field column of this axis and convert to microtesla max_val = g.AXES[i].max_comp_field[1] * 1e6 # get limits of achievable field min_val = g.AXES[i].max_comp_field[0] * 1e6 plot = axes[i] # get appropriate subplot @@ -143,10 +154,11 @@ def plot_field_sequence(array, width, height): # create plot of fixed size from if any(data < min_val): # same as above plot.axhline(y=min_val, linestyle='dashed', color='r') plot.text(t[-1], min_val, "min", horizontalalignment='center', color='r') + plot.set_title(g.AXIS_NAMES[i], size=10) # set subplot title (e.g. "X-Axis") # set shared axis labels: axes[2].set_xlabel("Time (s)") axes[1].set_ylabel("Magnetic Field (\u03BCT)") - return figure # return the created figure to be inserted somewhere else + return figure # return the created figure to be inserted somewhere