From 70fa24f9058fefabe091fee79c166122dd1ac80e Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Thu, 23 Feb 2023 18:17:01 +0100 Subject: [PATCH] Introduced red status indicator line, minor warning fixes --- src/csv_threading.py | 57 +++++++++++++++++++++++++++---- src/user_interface.py | 79 +++++++++++++++++++------------------------ 2 files changed, 84 insertions(+), 52 deletions(-) diff --git a/src/csv_threading.py b/src/csv_threading.py index 999da1a..3fa05e5 100644 --- a/src/csv_threading.py +++ b/src/csv_threading.py @@ -9,6 +9,7 @@ import numpy as np from threading import * from tkinter import messagebox import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from src.exceptions import DeviceBusy, DeviceAccessError from src.utility import ui_print @@ -79,6 +80,9 @@ class ExecCSVThread(Thread): messagebox.showwarning("Device Error!", "Required devices are not present, sequence aborted.") return + # Initialize plot + figure, avx_lines = display_plot(parent) + i = 0 # index of the current array row while i < len(array): if self.stopped or g.exit_flag: @@ -88,9 +92,17 @@ class ExecCSVThread(Thread): return # while array is not finished, devices are connected, user has not cancelled and application is running - t = time.time() - t_zero # get time relative to start of run target_t = array[i, 0] # Target execution time of data point + + # Update figure + try: + for j in range(4): + avx_lines[j].set_data([t, t], [0, 1]) + parent.plot_canvas.draw() # equivalent to matplotlib.show() + except DeviceAccessError as e: + ui_print("Failed to update figure: ", e) + if t >= target_t: # time for this row has come field_vec = array[i, 1:4] # extract desired field vector ui_print("[{:5.3f}s] B=[{:.1f}, {:.1f}, {:.1f}]\u03BCT for t={:.2f}s".format(t, @@ -106,7 +118,6 @@ class ExecCSVThread(Thread): logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator if logger.event_logging: # data should be logged when test bench is commanded logger.log_datapoint() # log data - i = i + 1 # next row elif t <= target_t - delay - 0.02: # is there enough time to sleep before the next row? @@ -117,7 +128,7 @@ class ExecCSVThread(Thread): 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 or period commas. Do not use these characters as a thousands separator! + # decimal or period commas. Do not use these characters as a thousand separator! with open(filepath, 'r') as csv_file: # Normalize separators csv_string = csv_file.read() @@ -153,6 +164,38 @@ def check_array_ok(array): messagebox.showwarning("Value Limits Warning!", warning_msg) +def display_plot(parent): # create plot of fixed size (pixels) from array + # calculate available height for plot (in pixels): + height_others = 0 # initialize variable to calculate height of other widgets + for element in parent.row_elements: # go through all rows in the widget except the plot frame + height_others += element.winfo_height() # add up heights + + # calculate available plot height: + height = parent.parent.winfo_height() - height_others - 50 # height of parent frame - other widgets - margin + width = min(parent.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 + + # Create plot + figure = plot_field_sequence(parent.sequence_array, width, height) # create figure to be displayed + # Clear previous plots first + try: + if parent.plot_canvas is not None: + parent.plot_canvas.get_tk_widget().destroy() + except Exception as e: + ui_print("Something went wrong while plotting csv data!", e) + messagebox.showerror("Error!", "Something went wrong while plotting csv data: \n%s" % e) + pass + + axes = figure.axes + avx_lines = [axes[0].axvline(x=0, color="r"), axes[1].axvline(x=0, color="r"), + axes[2].axvline(x=0, color="r"), axes[3].axvline(x=0, color="r")] + # Show new plot + parent.plot_canvas = FigureCanvasTkAgg(figure, parent.plot_frame) # create canvas to draw figure on + parent.plot_canvas.draw() # equivalent to matplotlib.show() + parent.plot_canvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in the UI + + return figure, avx_lines + + def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array # ToDo (optional): polar plots, plots of angle... fig_dpi = 100 # set figure resolution (dots per inch) @@ -160,12 +203,12 @@ def plot_field_sequence(array, width, height): # create plot of fixed size (pix figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # create figure with correct size # noinspection PyTypeChecker,SpellCheckingInspection - axes = figure.subplots(4, sharex=True, sharey=False, gridspec_kw={'hspace': 0.4}) # create subplots with shared axes + axes = figure.subplots(4, sharex=True, sharey=False, gridspec_kw={'hspace': 0.4}) # make subplots with shared axes figure.suptitle("Magnetic Field Sequence") # set figure title # modify data to show instantaneous jumps in field to reflect test bench operation - new_array = np.array([[0, 0, 0, 0, 0]], dtype=float) # initialize modified array, zeros to show start from no fields + new_array = np.array([[0, 0, 0, 0, 0]], dtype=float) # initialize modified array, zeros to show start from no field last_values = [0, 0, 0, 0] # [x,y,z, rr] field values / rot rate from last data point (zero here) for row in array[:, 0:5]: # go through each row in the original array @@ -200,8 +243,8 @@ def plot_field_sequence(array, width, height): # create plot of fixed size (pix ylim_mag = ([min(axes[0].get_ylim()), max(axes[0].get_ylim()), min(axes[1].get_ylim()), max(axes[1].get_ylim()), min(axes[2].get_ylim()), max(axes[2].get_ylim())]) - for i in range(3): axes[0].set_ylim(min(ylim_mag),max(ylim_mag)) - + for i in range(3): + axes[0].set_ylim(min(ylim_mag), max(ylim_mag)) # set shared axis labels: axes[2].set_xlabel("Time [s])") diff --git a/src/user_interface.py b/src/user_interface.py index cbc4b5d..f210cf3 100644 --- a/src/user_interface.py +++ b/src/user_interface.py @@ -20,7 +20,6 @@ import os.path from datetime import datetime from math import pi import warnings -warnings.filterwarnings("error") # import other project files: import src.globals as g @@ -33,6 +32,9 @@ from src.exceptions import DeviceAccessError, MagFieldOutOfBounds from src.utility import ui_print, save_dict_list_to_csv, save_dict_list_to_csv2, load_dict_list_from_csv import src.helmholtz_cage_device as helmholtz_cage_device +# Filter warning messages +warnings.filterwarnings("error") + # Define global font styles: screen_height_limit1 = 1100 # Limit after which font size gets reduced screen_height_limit2 = 900 # Limit after which font size gets reduced @@ -311,11 +313,11 @@ class ManualMode(Frame): else: # this really should never happen field = [0, 0] ui_print("Unexpected value encountered: compensate =", compensate) - messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate)) + messagebox.showerror("Unexpected Value! Unexpected value encountered: compensate = ", str(compensate)) var.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) # update the label text with the new values def switch_to_current_mode(self): # called when switching to the input current mode - self.compensate_checkbox.config(state=DISABLED) # disable the compensate ambient field checkbox + self.compensate_checkbox.config(state=DISABLED) # disable the compensation of the ambient field checkbox # update the labels showing the min/max achievable values for i, var in enumerate(self.max_value_vars): # go through the max value labels for each axis @@ -374,8 +376,8 @@ class ManualMode(Frame): pass else: # this really should never happen ui_print("Unexpected value encountered: compensate =", compensate) - messagebox.showerror("Unexpected Value!", - ("Unexpected value encountered: compensate =", compensate)) + messagebox.showerror("Unexpected Value! Unexpected value encountered: compensate = ", + str(compensate)) except helmholtz_cage_device.DeviceBusy: ui_print("Error: Could not acquire control. Is the HW already in use?") @@ -522,7 +524,7 @@ class ExecuteCSVMode(Frame): self.generate_load_csv_button.grid(row=0, column=1, padx=5, pady=5) row_counter += 1 info_text = Label(self, - text="Generate data of a circular, counter clockwise motion around an arbitrary axis and strength.", + text="Generate data of a circular, counter clockwise motion around an arbitrary axis.", padx=100, pady=3) info_text.grid(row=row_counter, column=col_counter, padx=0, sticky=W) row_counter += 1 @@ -692,7 +694,6 @@ class ExecuteCSVMode(Frame): self.plot_canvas = FigureCanvasTkAgg(figure, self.plot_frame) # create canvas to draw figure on self.plot_canvas.draw() # equivalent to matplotlib.show() self.plot_canvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in the UI - def export_csv_sequence(self): # Generate and export csv sequence # Generate rotation data @@ -701,7 +702,7 @@ class ExecuteCSVMode(Frame): rot_sequence = dict(enumerate(t)) for i in range(len(t)): rot_sequence[i] = {'Time [s]]': t[i], 'xField [T]': x[i], 'yField [T]': y[i], 'zField [T]': z[i], - 'Rotation Rate [deg/s]': rr[i] } + 'Rotation Rate [deg/s]': rr[i]} # Save dictionary to disk save_dict_list_to_csv2('test_sequence_rotation.csv', rot_sequence, query_path=True) ui_print("Saved test sequence to disc.") @@ -798,11 +799,11 @@ class ExecuteCSVMode(Frame): self.rot_cycle_number_vars.set("{:d}".format(rot_cycle_number)) # Variable rotation rate # Solve integral of r(t) = r_0 + r_1 * t + r_2 * t ^ 2 + r3 * sin(r_4 * t + r_5) from 0 to tau - R = 360 * rot_cycle_number # [deg] Cycle number multiples of full circle + angle = 360 * rot_cycle_number # [deg] Cycle number multiples of full circle func = lambda tau2: r[0] + r[1] * tau2 + r[2] * tau2 ** 2 + r[3] * (np.sin(r[4] * tau2 + r[5])) - func_integral = lambda tau: R - (r[0] * tau + r[1] / 2 * tau ** 2 + r[2] / 3 * tau ** 3 - - r[3] * (np.cos(r[4] * tau + r[5]) + np.cos(r[5])) - + r[3] * r[4] * (np.sin(r[4] * tau + r[5]) - np.sin(r[5]))) + func_integral = lambda tau: angle - (r[0] * tau + r[1] / 2 * tau ** 2 + r[2] / 3 * tau ** 3 + - r[3] * (np.cos(r[4] * tau + r[5]) + np.cos(r[5])) + + r[3] * r[4] * (np.sin(r[4] * tau + r[5]) - np.sin(r[5]))) tau_solution = -1 i = 0 # iterator and guess variable [s] while tau_solution < 0 or abs(func_integral(tau_solution)) > 0.5: @@ -1129,7 +1130,7 @@ class CalibrateAmbientField(Frame): if cmd == 'finished': self.reactivate_buttons() elif cmd == 'failed': - messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg)) + messagebox.showerror("Calibration error", "Error occurred during calibration:\n{}".format(arg)) self.reactivate_buttons() elif cmd == 'progress': self.calibration_procedure_progress_var.set(min(int(arg * 100), 100)) @@ -1518,7 +1519,8 @@ class CalibrateMagnetometerSimple(Frame): # Notes on the calibration method calibration_method_notes_frame = LabelFrame(self.right_column, text="Calibration method notes:") calibration_method_notes_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=20, sticky="nw") - label = "-Implementation according to Zikmund et al. [DOI: 10.1109/I2MTC.2014.6860790]\n-Points created by Fibonacci sphere\n-Only accounts for hard-iron offset and MGM scaling errors!" + label = "-Implementation according to Zikmund et al. [DOI: 10.1109/I2MTC.2014.6860790]\n" \ + "-Points created by Fibonacci sphere\n-Only accounts for hard-iron offset and MGM scaling errors!" calibration_method_notes = Label(calibration_method_notes_frame, anchor='w', justify='left', text=label) calibration_method_notes.grid(row=3, column=0, padx=5, pady=5, sticky="nw") @@ -1624,7 +1626,7 @@ class CalibrateMagnetometerSimple(Frame): if cmd == 'finished': self.reactivate_buttons() elif cmd == 'failed': - messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg)) + messagebox.showerror("Calibration error", "Error occurred during calibration:\n{}".format(arg)) self.reactivate_buttons() elif cmd == 'progress': self.calibration_procedure_progress_var.set(min(int(arg * 100), 100)) @@ -1686,7 +1688,7 @@ class CalibrateMagnetometerSimple(Frame): try: calibration_points = self.calibration_points_var.get() calibration_interval = self.calibration_interval_var.get() - compensated_field = self.self.compensated_field_var.get() + compensated_field = self.compensated_field_var.get() calibration_mag_field = self.mag_field_magnitude_var.get() * 1e-6 # converted to micro Tesla if calibration_mag_field <= 0 or calibration_mag_field > g.MAG_MAG_FIELD: raise MagFieldOutOfBounds @@ -1918,7 +1920,7 @@ class CalibrateMagnetometerComplete(Frame): calibration_point_nr_unit = Label(controls_frame, text="s (> 2 s)") calibration_point_nr_unit.grid(row=2, column=2, pady=5, sticky="nw") # Oversampling - calibration_point_nr_label = Label(controls_frame, text="Oversampling (samples/setpoint)") + calibration_point_nr_label = Label(controls_frame, text="Oversampling (samples/set point)") calibration_point_nr_label.grid(row=3, column=0, pady=5, sticky="nw") calibration_point_nr_entry = Entry(controls_frame, textvariable=self.calibration_oversampling_var) calibration_point_nr_entry.grid(row=3, column=1, pady=5, sticky="nw") @@ -2040,19 +2042,7 @@ class CalibrateMagnetometerComplete(Frame): results_label_unit = Label(calibration_results_frame, text="-") results_label_unit.grid(row=row_counter + row, column=1 + 3, padx=5, pady=5, sticky="nw") row_counter += 3 - """ - # A_mat - results_label_a_mat_inv = Label(calibration_results_frame, text="A =") - results_label_a_mat_inv.grid(row=row_counter+1, column=0, padx=5, pady=5, sticky="nw") - for row in range(3): - for column in range(3): - axis_data = Entry(calibration_results_frame, - textvariable=self.a_mat_result_vars[row][column], - width=15, - state='readonly') - axis_data.grid(row=row_counter+row, column=1 + column, padx=5, pady=5, sticky="nw") - row_counter += 3 - """ + # b results_label_a_mat_inv = Label(calibration_results_frame, text="b^T =") results_label_a_mat_inv.grid(row=row_counter, column=0, padx=5, pady=5, sticky="nw") @@ -2090,11 +2080,15 @@ class CalibrateMagnetometerComplete(Frame): # Notes on the calibration method calibration_method_notes_frame = LabelFrame(self.right_column, text="Calibration method notes:") calibration_method_notes_frame.grid(row=0, column=0, padx=(100, 0), pady=20, sticky="nw") - label = "-Implementation of calibration according to Kok et al. [ISBN: 978-0-9824438-5-9]\n-Implementation of ellipsoid fit according to Li et al. [DOI: 10.1109/GMAP.2004.1290055]\n-Points created by Fibonacci sphere\n-Accounts for soft-iron (A matrix) and hard-iron (b offset vector) effects!\n-Measured to calibrated field function: h=A^-1 (h_m-b)" + label = "-Implementation of calibration according to Kok et al. [ISBN: 978-0-9824438-5-9]\n" \ + "-Implementation of ellipsoid fit according to Li et al. [DOI: 10.1109/GMAP.2004.1290055]\n" \ + "-Points created by Fibonacci sphere\n" \ + "-Accounts for soft-iron (A matrix) and hard-iron (b offset vector) effects!\n" \ + "-Measured to calibrated field function: h=A^-1 (h_m-b)" calibration_method_notes = Label(calibration_method_notes_frame, anchor='w', justify='left', text=label) calibration_method_notes.grid(row=0, column=0, padx=5, pady=5, sticky="nw") - # Plot frame (overwrite plotframe) + # Plot frame plot_frame = LabelFrame(self.right_column, text="Result plots:") plot_frame.grid(row=1, column=0, padx=(100, 0), pady=20, sticky="nw") fig1 = plt.figure('MGM_cal_complete_left', figsize=(2.5, 3), dpi=100) @@ -2152,7 +2146,7 @@ class CalibrateMagnetometerComplete(Frame): if cmd == 'finished': self.reactivate_buttons() elif cmd == 'failed': - messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg)) + messagebox.showerror("Calibration error", "Error occurred during calibration:\n{}".format(arg)) self.reactivate_buttons() elif cmd == 'progress': self.calibration_procedure_progress_var.set(min(int(arg * 100), 100)) @@ -2411,7 +2405,7 @@ class HardwareConfiguration(Frame): self.file_select_frame.grid_columnconfigure(ALL, weight=1) self.file_select_frame.grid(row=row_counter, column=0, sticky=W, padx=20) - # Create and place buttons + # Create and place buttons in frame # button to load a config file: load_file_button = Button(self.file_select_frame, text="Load config file...", command=self.load_config, pady=5, padx=5, font=SMALL_BUTTON_FONT) @@ -2451,10 +2445,7 @@ class HardwareConfiguration(Frame): info_label.grid(row=row, column=2, sticky=W) row += 1 - row_counter += 1 - - # Label(self, text="", pady=0).grid(row=row_counter, column=0) # add spacer - row_counter += 1 + row_counter += 2 # setup main entry field for operational constants # setup frame: @@ -2506,7 +2497,6 @@ class HardwareConfiguration(Frame): self.update_fields() # set current values from config file - # Label(self, text="", pady=3).grid(row=row_counter, column=0) # add spacer row_counter += 1 # Setup buttons to implement and restore defaults @@ -2516,7 +2506,7 @@ class HardwareConfiguration(Frame): self.buttons_frame.grid_columnconfigure(ALL, weight=1) self.buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20) - # Create and place buttons + # Create and place buttons in frame # button to read the values from all fields, update the config and reinitialize the test bench implement_button = Button(self.buttons_frame, text="Update and Reinitialize", command=self.implement, pady=5, padx=5, font=BIG_BUTTON_FONT) @@ -2527,7 +2517,6 @@ class HardwareConfiguration(Frame): restore_button.grid(row=0, column=1, padx=5) row_counter += 1 - # Label(self, text="", pady=3).grid(row=row_counter, column=0) # add spacer def page_switch(self): # function that is called when switching to this window self.update_fields() # update values in the entry fields from config @@ -2649,7 +2638,7 @@ class HardwareConfiguration(Frame): # open file selection dialogue and save path of selected file filename = filedialog.asksaveasfilename(initialdir=directory, title="Save config to file", filetypes=([("Config File", "*.ini*")]), - defaultextension=[("Config File", "*.ini*")]) + defaultextension="*.ini*") if filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not save config.") @@ -2835,8 +2824,8 @@ class ConfigureLogging(Frame): def stop_logging(self): # stop the data logging, called by "Stop Logging" button ui_print("Stopped data logging. Remember to save data to file!") - self.regular_logging = False # tell everything its time to stop periodic logging - self.event_logging = False # tell everything its time to stop logging on test bench commands + self.regular_logging = False # tell everything it is time to stop periodic logging + self.event_logging = False # tell everything it is time to stop logging on test bench commands self.write_to_file_button["state"] = "normal" # enable write to file button self.stop_logging_button["state"] = "disabled" # disable stop logging button self.start_logging_button["state"] = "normal" # enable start logging button @@ -2852,7 +2841,7 @@ class ConfigureLogging(Frame): else: # a valid filename was selected log.write_to_file(filepath) # write logged data to the file - def clear_data(self): # called on button press, asks user if he want to save logged data and then deletes it + def clear_data(self): # called on button press, asks user if logged data should be saved and then deletes it if log.unsaved_data: # there is logged data that has not been written to a file yet # open pop-up to ask user if he wants to save the data or cancel clearing it: save_log = messagebox.askyesnocancel("Save log data?", "There seems to be unsaved logging data. "