From 2beb6f1fa1981e301ab58859fbb7c3cb3a9b1b2e Mon Sep 17 00:00:00 2001 From: SuprajaKumbargeri <93954870+SuprajaKumbargeri@users.noreply.github.com> Date: Sun, 7 May 2023 05:00:13 -0600 Subject: [PATCH 1/3] Experiment Plotter runs without errors TO CHECK: Plots for different types of experiments --- GUI/experimentWindowGui.py | 11 ++++---- GUI/experiment_runner_gui.py | 55 ++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/GUI/experimentWindowGui.py b/GUI/experimentWindowGui.py index 240ff9e..6a85ee5 100644 --- a/GUI/experimentWindowGui.py +++ b/GUI/experimentWindowGui.py @@ -27,8 +27,7 @@ def __init__(self, parent_gui, ics, logger: logging.Logger): lab_experiment_icon = QIcon("../Icons/labExperiment.png") self.setWindowIcon(lab_experiment_icon) - # Experiment runner GUI - self.experiment_runner_gui = ExperimentRunner(self, self.my_logger) + # This is the outermost widget or the "main" widget self.main_widget = QWidget() @@ -317,9 +316,7 @@ def comment_text_changed(self): def experiment_runner_clicked(self): self.get_logger().debug('Experiment Runner clicked') if self.validate_experiment_data(): - print(self.experiment_DTO) self.experiment_DTO = self.construct_DTO() - print(self.experiment_DTO) self.show_experiment_runner_window() else: return @@ -342,13 +339,15 @@ def construct_experiment_menu_bar(self): help_menu = experiment_menu_bar.addMenu("&Help") def show_experiment_runner_window(self): - self.experiment_runner_gui.show() + # Experiment runner GUI + self.experiment_runner_gui = ExperimentRunner(self, self.my_logger) + # self.experiment_runner_gui.show() def exit_experiment_gui(self): """ Closes the experiment GUI """ - print('Exiting ExperimentWindowGui...') + self.get_logger().info('Exiting ExperimentWindowGui...') self.close() diff --git a/GUI/experiment_runner_gui.py b/GUI/experiment_runner_gui.py index 1505c6f..fdbebf5 100644 --- a/GUI/experiment_runner_gui.py +++ b/GUI/experiment_runner_gui.py @@ -46,8 +46,6 @@ class MainExperimentProcedure(Procedure): sequence = {} # Dictionary with key -> (ins, qty), value -> sequence details dict: start, stop, number_of_points, data_type quantities = {} # Dictionary with key -> (ins, qty), value -> QuantitiyManager object - # The columns in the plotter - DATA_COLUMNS = ['step', 'dummy'] # TO FIX: Plotter Widget needs two columns to initialize # Dynamically added column names input_data_names = {} output_data_names = {} @@ -67,14 +65,10 @@ def set_parameters(self, DTO, logger): for level in self.input: for instrument_name, quantity_name in level: input_name = "Input - " + str(instrument_name) + " - " + str(quantity_name) - if input_name not in self.DATA_COLUMNS: - self.DATA_COLUMNS.append(input_name) - self.input_data_names[(instrument_name, quantity_name)] = input_name + self.input_data_names[(instrument_name, quantity_name)] = input_name for instrument_name, quantity_name in self.output: output_name = "Output - " + str(instrument_name) + " - " + str(quantity_name) - if output_name not in self.DATA_COLUMNS: - self.DATA_COLUMNS.append(output_name) - self.output_data_names[instrument_name, quantity_name] = output_name + self.output_data_names[instrument_name, quantity_name] = output_name def set_logger(self, logger: logging.Logger): self.logger = logger @@ -121,7 +115,7 @@ def execute(self): for step_sequence in combined_sequences: # step sequence is a list of tupules [(1 , 'a'), (True, )] # The datapoints we record at each "step": - print(step_sequence) + data = {} for level in range(len(self.input)): for index in range(len(self.input[level])): @@ -154,15 +148,28 @@ class ExperimentRunner(ManagedWindow): def __init__(self, parent_gui, logger: logging.Logger, base_filename='experiment_results'): # Initialize the super class + + # A reference to the invoking GUI + self.parent_gui = parent_gui + self.DTO = self.parent_gui.experiment_DTO + DATA_COLS = ['step'] + + + for level in self.DTO.input_quantities: + for instrument_name, quantity_name in level: + input_name = "Input - " + str(instrument_name) + " - " + str(quantity_name) + DATA_COLS.append(input_name) + + for instrument_name, quantity_name in self.DTO.output_quantities: + output_name = "Output - " + str(instrument_name) + " - " + str(quantity_name) + DATA_COLS.append(output_name) + + MainExperimentProcedure.DATA_COLUMNS = DATA_COLS super().__init__(procedure_class=MainExperimentProcedure, x_axis='step', y_axis='step', - directory_input=True) # Enables directory input widget - # self.set_parameters(params) - - # A reference to the invoking GUI - self.parent_gui = parent_gui + directory_input=True) # Enables directory input widget play_icon = QIcon("../Icons/playButton.png") self.setWindowIcon(play_icon) @@ -187,6 +194,8 @@ def __init__(self, parent_gui, logger: logging.Logger, base_filename='experiment self.main_layout.addWidget(quit_btn) self.main_layout.setAlignment(quit_btn, Qt.AlignmentFlag.AlignRight) + self.show() + def set_parameters(self, parameters): self.logger.info('Setting parameter values') return super().set_parameters(parameters) @@ -197,9 +206,9 @@ def queue(self, procedure=None): # The full path to file where the experiment results will be written to filename = os.path.join(self.directory, self.generate_experiment_file_name(self.base_filename)) self.logger.info(f'Writing results to file {filename}') - - procedure = MainExperimentProcedure() - procedure.set_parameters(self.parent_gui.experiment_DTO, self.logger) + if procedure is None: + procedure = MainExperimentProcedure() + procedure.set_parameters(self.parent_gui.experiment_DTO, self.logger) results = Results(procedure, filename) experiment = self.new_experiment(results) @@ -230,10 +239,20 @@ def construct_menu_bar(self): help_menu = experiment_menu_bar.addMenu("&Help") def exit_gui(self): + self.close() + + def closeEvent(self, event): """ Closes the experiment GUI """ - self.close() + self.logger.info("Exiting the window") + # disconnect log_widget slots manually to avoid + # RuntimeError: wrapped C/C++ object of type QPlainTextEdit has been deleted + self.log_widget.view.disconnect() + self.log_widget.handler.emitter.record.disconnect() + + super().closeEvent(event) + self.destroy() if __name__ == "__main__": From e8faead88455bcbd372c65ff8ee6c8be0531e6af Mon Sep 17 00:00:00 2001 From: SuprajaKumbargeri <93954870+SuprajaKumbargeri@users.noreply.github.com> Date: Sun, 7 May 2023 23:17:07 -0600 Subject: [PATCH 2/3] Update experiment_runner_gui.py --- GUI/experiment_runner_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/experiment_runner_gui.py b/GUI/experiment_runner_gui.py index fdbebf5..759923b 100644 --- a/GUI/experiment_runner_gui.py +++ b/GUI/experiment_runner_gui.py @@ -125,7 +125,7 @@ def execute(self): sleep(self.delay_time) for (ins, qty) in self.output: - data[self.output_data_names[(ins, qty)]] = self.quantities[(ins, qty)].get_value() + data[self.output_data_names[(ins, qty)]] = float(self.quantities[(ins, qty)].get_value()) sleep(self.delay_time) data['step'] = step From 2c5d7cb6a20c284ac045447196e1c83aabb884da Mon Sep 17 00:00:00 2001 From: SuprajaKumbargeri <93954870+SuprajaKumbargeri@users.noreply.github.com> Date: Mon, 8 May 2023 05:12:22 -0600 Subject: [PATCH 3/3] Updated comments in the Experiment Module --- GUI/channels_table.py | 22 +++-- GUI/experimentWindowGui.py | 181 ++++++++++++++++++----------------- GUI/experiment_runner_gui.py | 37 ++++++- GUI/log_channels_table.py | 26 ++++- GUI/sequence_constructor.py | 127 +++++++----------------- GUI/sequence_table.py | 33 +++++-- 6 files changed, 225 insertions(+), 201 deletions(-) diff --git a/GUI/channels_table.py b/GUI/channels_table.py index 7d9034c..25a3a8c 100644 --- a/GUI/channels_table.py +++ b/GUI/channels_table.py @@ -13,8 +13,12 @@ def __init__(self, parent_gui, channels_added: dict(), logger: logging.Logger): self._parent_gui = parent_gui self.logger = logger # dictionary for added channels in the table + # key -> Instrument Name, value -> InstrumentManager object self.channels_added = channels_added # dictionary for quantities of each added channel + # format key -> instrument name + # value -> dictionary of quantity and QuantityManager objects + # example: self.quantities_added[ins_name][qty_name] = QuanttityManager object # TODO: change dictionary layout to (ins, qty): quantity self.quantities_added = dict() @@ -39,6 +43,7 @@ def __init__(self, parent_gui, channels_added: dict(), logger: logging.Logger): self.itemDoubleClicked.connect(self.show_quantity_frame_gui) def add_channel_item(self, instrument_manager): + """Adds a connected instrument and all its quantities to the Channels table""" im = instrument_manager cute_name = im.name model = im.model_name @@ -67,6 +72,7 @@ def add_channel_item(self, instrument_manager): self.parent.setExpanded(True) def get_value(self, quantity): + """Gets the value for the quantity, similar implentation as quantity frames in the instrument manager GUI""" try: value = quantity.get_value() self._handle_quant_value_change(quantity.name, value) @@ -75,12 +81,8 @@ def get_value(self, quantity): self.logger.error(f"Error querying '{quantity.name}': {e}") QtW.QMessageBox.critical(self, f"Error querying '{quantity.name}'", str(e)) - def handle_incoming_value(self, value): - """Handles any value returned by instrument to be properly displayed. Should be implemented by child class""" - pass - # raise NotImplementedError() - def remove_channel(self): + """Removes a instrument and all its quantities from the Channels table""" selected_item = self.currentItem() if selected_item is not None: if selected_item.childCount() > 0: @@ -93,7 +95,8 @@ def remove_channel(self): return cute_name return - def show_quantity_frame_gui(self, selected_item, column=None): + def show_quantity_frame_gui(self, selected_item, column=None): + """Resuses quantity frames to modify the value of a quantitiy""" parent_item = selected_item.parent() if parent_item is not None: quantity_name = selected_item.text(0) @@ -138,8 +141,8 @@ def _handle_quant_value_change(self, quantity_changed, new_value): # This is to avoid having non-visible quantities being present in the Step Sequence and Log Channels table self._parent_gui.remove_experiment_quantities(cute_name) - def channels_table_selection_changed(self): + """Handle selection change in Channels table. Implement if needed.""" selected_item = self.currentItem() if selected_item is not None: if selected_item.childCount() == 0: @@ -148,11 +151,13 @@ def channels_table_selection_changed(self): pass def startDrag(self, supportedActions): + """Drag functionality in initiation. Enable dragging of quantities from the Channel table""" selected_item = self.currentItem() # only a single item is configured to be selected at once parent_item = selected_item.parent() if not parent_item: return data_dict = {'instrument_name': parent_item.text(0), 'quantity_name': selected_item.text(0), 'address': parent_item.text(3)} + # drag data is a dictionary of instrument name, quantity name and instrument address # Serialize the dictionary as JSON mime_data = QMimeData() @@ -163,6 +168,7 @@ def startDrag(self, supportedActions): class AddChannelDialog(QDialog): + """Dialog box to display all connect instruments to be added to the channels table""" def __init__(self, connected_ins: dict): super().__init__() @@ -192,6 +198,7 @@ def __init__(self, connected_ins: dict): self.setLayout(layout) def get_selected_item(self): + """Returns the selected instrument name""" selected_item = self.tree_widget.currentItem() if selected_item is not None: return selected_item.text(0) @@ -199,6 +206,7 @@ def get_selected_item(self): return None class ModifyQuantity(QDialog): + """Uses quantity frame in a dialog box to modify its value""" def __init__(self, quantity, frame): super().__init__() diff --git a/GUI/experimentWindowGui.py b/GUI/experimentWindowGui.py index 6a85ee5..9285813 100644 --- a/GUI/experimentWindowGui.py +++ b/GUI/experimentWindowGui.py @@ -15,20 +15,21 @@ class ExperimentWindowGui(QMainWindow): def __init__(self, parent_gui, ics, logger: logging.Logger): super().__init__() - self.parent_gui = parent_gui + self.parent_gui = parent_gui # The parent GUI object. self.my_logger = logger - self.setWindowTitle('Experiment') + self.setWindowTitle('Create Experiment') self.resize(1000, 800) - self._ics = ics - self._working_instruments = dict() + self._ics = ics # InstrumentConnectionService object from the parent gui - self.experiment_DTO = None + # Dictionary of instruments added for the experiment in the Channels table + # key -> Instrument Name, value -> InstrumentManager object + self._working_instruments = dict() + + self.experiment_DTO = None # the Data Transfer Object with experiment details that the Experiment Runner uses lab_experiment_icon = QIcon("../Icons/labExperiment.png") self.setWindowIcon(lab_experiment_icon) - - # This is the outermost widget or the "main" widget self.main_widget = QWidget() @@ -51,6 +52,22 @@ def __init__(self, parent_gui, ics, logger: logging.Logger): # Make the Menu Bar self.construct_experiment_menu_bar() + def construct_experiment_menu_bar(self): + """ + Constructs the Menu Bar on top + """ + experiment_menu_bar = self.menuBar() + experiment_menu_bar.addSeparator() + file_menu = experiment_menu_bar.addMenu("&File") + + # Defines the action to exit GUI via the menu bar File section + exit_action = QAction("&Exit", self) + exit_action.triggered.connect(self.exit_experiment_gui) + file_menu.addAction(exit_action) + + edit_menu = experiment_menu_bar.addMenu("&Edit") + help_menu = experiment_menu_bar.addMenu("&Help") + def get_logger(self): """Get the application logger""" return self.my_logger @@ -64,6 +81,13 @@ def remove_experiment_quantities(self, instrument_name): self.step_sequence_table.remove_channel(instrument_name) self.log_channels_table.remove_channel(instrument_name) return + + def exit_experiment_gui(self): + """ + Closes the create experiment GUI + """ + self.get_logger().info('Exiting ExperimentWindowGui...') + self.close() #################################################################### # Channel table related implementation @@ -108,9 +132,7 @@ def construct_channels_section(self): self.main_layout.addWidget(self.channels_group) def add_channel(self): - """ - Implements the add instrument functionality to the Channel table - """ + """Implements the add instrument functionality to the Channel table""" dialog = AddChannelDialog(self._ics._connected_instruments) if dialog.exec(): # Get the selected item from the dialog box @@ -120,18 +142,14 @@ def add_channel(self): self.channels_table.add_channel_item(instrument_manager) def remove_channel(self): - """ - Removes an added instrument from channel table - """ + """Removes an added instrument from channel table""" instrument_name = self.channels_table.remove_channel() # returns the deleted instrument's name if instrument_name: self.remove_experiment_quantities(instrument_name) return def edit_channel_quantity(self): - """ - Modifies an existing instrument's quantity - """ + """Modifies an existing instrument's quantity""" if self.channels_table.currentItem() is not None: self.channels_table.show_quantity_frame_gui(self.channels_table.currentItem()) return @@ -163,9 +181,6 @@ def construct_step_sequence_section(self): # This is the table for looping values in the experiment self.step_sequence_table = StepSequenceTreeWidget(self._working_instruments, self.item_valid, self.my_logger) - # Expand all the inner items - self.step_sequence_table.expandAll() # TODO: Remove - step_sequence_main_layout.addWidget(self.step_sequence_table) # Button section for 'edit' and 'remove' options @@ -188,17 +203,13 @@ def construct_step_sequence_section(self): self.right_side_section.addWidget(self.step_sequence_group, 3) def edit_quantity_sequence(self): - """ - Modifies an existing sequence for a quantity - """ + """Modifies an existing sequence for a quantity""" if self.step_sequence_table.currentItem() is not None: self.step_sequence_table.show_sequence_constructor_gui(self.step_sequence_table.currentItem()) return def remove_quantity_sequence(self): - """ - Removes an added quantity from sequence table - """ + """Removes an added quantity from sequence table""" self.step_sequence_table.remove_quantity() return @@ -226,11 +237,6 @@ def construct_logging_section(self): # Button section for 'edit' and 'remove' options button_section_layout = QHBoxLayout() - ''' - edit_btn = QPushButton("Edit...") - button_section_layout.addStretch(1) - button_section_layout.addWidget(edit_btn) - ''' remove_btn = QPushButton("Remove") button_section_layout.addStretch(1) @@ -273,16 +279,16 @@ def construct_logging_section(self): self.delay_time.setMinimum(0) self.delay_time.setMaximum(float('inf')) timing_layout.addRow(QLabel("Delay between step and measure [s]:"), self.delay_time) - # TODO: Connect later + # TODO: Connect if needed # self.delay_time.valueChanged.connect(delay_time_changed) self.estimated_time = QSpinBox() timing_layout.addRow(QLabel("Estimated time per point [s]:"), self.estimated_time) - # TODO: Connect later + # TODO: Connect if needed # self.estimated_time.valueChanged.connect(delay_estimated_time) - # TODO: Connect later to set time needed value - self.time_needed = "0:00:00" # replace with calculated time needed value + # TODO: Connect if needed to set time estimated value + self.time_needed = "0:00:00" # replace with calculated time estimated value timing_layout.addRow(QLabel("Time needed:"), QLabel(self.time_needed)) self.timing_group.setLayout(timing_layout) @@ -300,13 +306,13 @@ def construct_logging_section(self): self.main_layout.addLayout(self.right_side_section) def remove_log_quantity(self): - """ - Removes an added quantity from log table - """ + """Removes an added quantity from log table""" self.log_channels_table.remove_quantity() return def comment_text_changed(self): + """Handles change of comment text""" + # TODO: Implement if needed pass @@ -314,6 +320,10 @@ def comment_text_changed(self): # Experiment Runner related implementation #################################################################### def experiment_runner_clicked(self): + """ + Validates the step sequence, creates an experiment DTO + and calls to shiw the Experiment Runner window for the created DTO + """ self.get_logger().debug('Experiment Runner clicked') if self.validate_experiment_data(): self.experiment_DTO = self.construct_DTO() @@ -321,60 +331,13 @@ def experiment_runner_clicked(self): else: return - - def construct_experiment_menu_bar(self): - """ - Constructs the Menu Bar on top - """ - experiment_menu_bar = self.menuBar() - experiment_menu_bar.addSeparator() - file_menu = experiment_menu_bar.addMenu("&File") - - # Defines the action to exit GUI via the menu bar File section - exit_action = QAction("&Exit", self) - exit_action.triggered.connect(self.exit_experiment_gui) - file_menu.addAction(exit_action) - - edit_menu = experiment_menu_bar.addMenu("&Edit") - help_menu = experiment_menu_bar.addMenu("&Help") - def show_experiment_runner_window(self): - # Experiment runner GUI + """Creates adnd shows the Experiment Runner GUI""" self.experiment_runner_gui = ExperimentRunner(self, self.my_logger) - # self.experiment_runner_gui.show() - - def exit_experiment_gui(self): - """ - Closes the experiment GUI - """ - self.get_logger().info('Exiting ExperimentWindowGui...') - self.close() - - - #################################################################### - # Data validation related implementaion - #################################################################### - def item_valid(self, ins_name: str, qty_name: str): - if self.step_sequence_table.check_item_valid(ins_name, qty_name) and self.log_channels_table.check_item_valid(ins_name, qty_name): - return True - else: - return False - - def validate_experiment_data(self): - - # Ensure no over-lap between step sequence table and log channels table quantities - if set(self.step_sequence_table.quantities) & (set(self.log_channels_table.quantities)): - self.get_logger().error("Duplicate quantities in Step Sequence and Log Channels tables.") - return False - - # Ensure sequences at the same level have the same number of points - if not self.step_sequence_table.validate_sequence(): - return False - - return True - + self.experiment_runner_gui.show() def construct_DTO(self): + """Constructs the DTO for an experiment """ self.get_logger().info("Constructing DTO ...") input_quantities, quantity_sequences = self.step_sequence_table.get_step_sequence_quantities() output_quantities = self.log_channels_table.get_log_table_quantities() @@ -398,11 +361,55 @@ def construct_DTO(self): comments=self.comment_box.toPlainText()) return DTO + #################################################################### + # Data validation related implementaion + #################################################################### + def item_valid(self, ins_name: str, qty_name: str): + """ + Validates if a quantity (represented as (ins, qty)) already exists in either of Step Sequence and Log Channels table + Returns True if the item doesn't already exist in these tables and hence can be added + Returns False if the item already exists + """ + if self.step_sequence_table.check_item_valid(ins_name, qty_name) and self.log_channels_table.check_item_valid(ins_name, qty_name): + return True + else: + return False + + def validate_experiment_data(self): + """Checks if the step sequence data is valid to construct an experiment with""" + # Ensure no over-lap between step sequence table and log channels table quantities + if set(self.step_sequence_table.quantities) & (set(self.log_channels_table.quantities)): + self.get_logger().error("Duplicate quantities in Step Sequence and Log Channels tables.") + return False + + # Ensure sequences at the same level have the same number of points + if not self.step_sequence_table.validate_sequence(): + return False + + return True + #################################################################### # Data Transfer Object Class #################################################################### class ExperimentDTO: + """ + Holds experiment details for the Experiment Runner to use + input_quantities: list containing list of input quantities represented as (ins, qty) at each level + Example: input_quantities[0] contains list of (ins, qty) at level 0 + quantity_sequences: dictionary of dictionaries with sequence data for a quantity represented as (ins, qty) + dictionary format: key -> (ins, qty) + value -> {'datapoints': points, + 'start':start, + 'stop':stop, + 'datatype': data_type} + output_quantities: list of output quantities represented as (ins, qty) + quantitiy_managers: dictionary of quantity managers for all input and output quantities + dictionary format: key -> (int, qty) + value -> QuantityManager object + delay_time: float value representing the delay time set in the timing section + comments: text entered in the comment section + """ def __init__(self, input_quantities: list, quantity_sequences: dict, output_quantities: list, quantitiy_managers: dict, delay_time: float, comments: str): diff --git a/GUI/experiment_runner_gui.py b/GUI/experiment_runner_gui.py index 759923b..1167429 100644 --- a/GUI/experiment_runner_gui.py +++ b/GUI/experiment_runner_gui.py @@ -13,9 +13,21 @@ from itertools import product import numpy as np + +""" +Implementation of Experiment Runner is inspired by PyMeasure's tutorial on ManagedWindow +ref: https://pymeasure.readthedocs.io/en/latest/tutorial/graphical.html#using-the-managedwindow + +MainExperimentProcedure extends PyMeasure's Procedure class and implements experiment according to +Experiment GUI's experiment DTO by creating dynamic loops. This is achieved using python's itertools package. +""" + ################################################################################### # StringParameter ################################################################################### +# Extend the implementation of procedure class using PyMeasure's Parameter rather than using local objects for attributes +# This is an example of defining a StringParameter class extending the Parameter class +# Default classes are already defined in PyMeasure for Integer and Float class StringParameter(Parameter): """ :class:`Parameter` sub-class that uses the string type to store the value. @@ -53,7 +65,7 @@ class MainExperimentProcedure(Procedure): delay_time = 0 # delay time between each command def set_parameters(self, DTO, logger): - + """Sets values to all the above attributes using DTO from the Experiment GUI""" self.set_logger(logger) self.input = DTO.input_quantities self.sequence = DTO.quantity_sequences @@ -71,15 +83,20 @@ def set_parameters(self, DTO, logger): self.output_data_names[instrument_name, quantity_name] = output_name def set_logger(self, logger: logging.Logger): + """Sets logger""" self.logger = logger def startup(self): + """Start up method""" self.logger.info('startup() was called') def generate_sequence(self, seq: dict): + """Generates an array of values for [start, stop] range with given number of points""" if seq['datatype'].upper() == 'DOUBLE': return np.linspace(seq['start'], seq['stop'], seq['datapoints']) else: + # implements the same for non-float or non-int values too, such as boolean or combo + # generates a list of start and stop values of length of given number of points if seq['start'] != seq['stop']: number_of_points = seq['datapoints'] return ([seq['start']] * (number_of_points // 2)) + ([seq['stop']] * (number_of_points - (number_of_points // 2))) @@ -87,10 +104,13 @@ def generate_sequence(self, seq: dict): return [seq['start']] * seq['datapoints'] def execute(self): + """Implements the experiment: creates dynamic loops; sets input values; records measured datapoint""" self.logger.info(f'Starting experiment') datapoints = 1 # number of datapoints individual_sequences = [] + + # generate sequences for input_level in self.input: sequences_in_level = [] for (ins, qty) in input_level: @@ -101,6 +121,7 @@ def execute(self): pass datapoints *= len(sequences_in_level[0]) """ + example: converting [[1, 2, 3], ['a', 'b', 'c']] to [(1, 'a'), (2, 'b'), (3, 'c')] """ sequences_in_order = list(zip(*sequences_in_level)) @@ -152,9 +173,12 @@ def __init__(self, parent_gui, logger: logging.Logger, base_filename='experiment # A reference to the invoking GUI self.parent_gui = parent_gui self.DTO = self.parent_gui.experiment_DTO - DATA_COLS = ['step'] - + # redefining procedure class with the experiment columns received from the Experiment GUI + # This step is performed because ManagedWindow creates PlotWidget during its instantiation + # It only takes the procedure_class's definition so the class definition has to be modified to + # include the new dynamically added columns for the experiment plotting + DATA_COLS = ['step'] for level in self.DTO.input_quantities: for instrument_name, quantity_name in level: input_name = "Input - " + str(instrument_name) + " - " + str(quantity_name) @@ -166,6 +190,7 @@ def __init__(self, parent_gui, logger: logging.Logger, base_filename='experiment MainExperimentProcedure.DATA_COLUMNS = DATA_COLS + # ManagedWindow's constructor super().__init__(procedure_class=MainExperimentProcedure, x_axis='step', y_axis='step', @@ -201,6 +226,9 @@ def set_parameters(self, parameters): return super().set_parameters(parameters) def queue(self, procedure=None): + """ + Overrides ManagedWindow's queue method + """ self.logger.info('Starting measurement procedure') # The full path to file where the experiment results will be written to @@ -239,6 +267,9 @@ def construct_menu_bar(self): help_menu = experiment_menu_bar.addMenu("&Help") def exit_gui(self): + """ + self.close() automatically calls closeEvent + """ self.close() def closeEvent(self, event): diff --git a/GUI/log_channels_table.py b/GUI/log_channels_table.py index 1fed559..da5ffaa 100644 --- a/GUI/log_channels_table.py +++ b/GUI/log_channels_table.py @@ -10,7 +10,8 @@ def __init__(self, channels_added: dict(), item_valid: Callable, logger: logging super().__init__() self.logger = logger - # dictionary for added channels in the table + # dictionary for availabe channels + # key -> Instrument Name, value -> InstrumentManager object self.channels_added = channels_added # dictionary for quantities of each added channel @@ -20,7 +21,7 @@ def __init__(self, channels_added: dict(), item_valid: Callable, logger: logging self.item_valid = item_valid - self.setDragEnabled(True) # TODO: Disable manual rearrangement + self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) # Disabling arrows in the Tree Widget @@ -49,18 +50,26 @@ def quantities(self): return self.quantities_added.keys() def check_item_valid(self, instrument_name, quantity_name): - # Check if the quantity already exists + """Returns False if the quantity already exists in this table so that it cannot be added again or elsewhere""" if (instrument_name, quantity_name) in self.quantities_added.keys(): return False return True def dragEnterEvent(self, event): + """ + Implements drag enter event + Accepts only a json mime data object (dictionary created in Channels Table drag) + """ if event.mimeData().hasFormat('application/json'): event.accept() else: event.ignore() def dragMoveEvent(self, event): + """ + Implements drag move event + Accepts only a json mime data object (dictionary created in Channels Table drag) + """ if event.mimeData().hasFormat('application/json'): byte_data = event.mimeData().data('application/json') json_string = str(byte_data, 'utf-8') @@ -74,6 +83,11 @@ def dragMoveEvent(self, event): event.ignore() def dropEvent(self, event): + """ + Adds dragged Implements drag drop event to add quantity to this table + Accepts only a json mime data object (dictionary created in Channels Table drag) + Adds this quantity to the table + """ if event.mimeData().hasFormat('application/json'): byte_data = event.mimeData().data('application/json') json_string = str(byte_data, 'utf-8') @@ -94,8 +108,8 @@ def dropEvent(self, event): else: event.ignore() - """ Removes a selected quantitiy from the Log Channels Table """ def remove_quantity(self): + """Removes a selected quantitiy from the Log Channels Table""" selected_item = self.currentItem() if selected_item is not None: instrument_name = selected_item.text(1) @@ -103,8 +117,8 @@ def remove_quantity(self): del self.quantities_added[(instrument_name, quantity_name)] self.takeTopLevelItem(self.indexOfTopLevelItem(selected_item)) - """ Removes all quantities related to a instrument from the Log Channels Table """ def remove_channel(self, cute_name): + """Removes all quantities related to a instrument from the Log Channels Table""" if cute_name: # Traverse the tree in reverse to remove the tree widgets that belong to an instrument for index in range(self.topLevelItemCount() -1, -1, -1): @@ -116,9 +130,11 @@ def remove_channel(self, cute_name): def log_channels_table_selection_changed(self): + """Handle selection change in Log Channels table. Implement if needed.""" selected_item = self.currentItem() pass def get_log_table_quantities(self): + """Provides output quantities for the Experiment DTO""" output_quantities = self.quantities_added.keys() return list(output_quantities) \ No newline at end of file diff --git a/GUI/sequence_constructor.py b/GUI/sequence_constructor.py index 4193a6d..6ba6cf0 100644 --- a/GUI/sequence_constructor.py +++ b/GUI/sequence_constructor.py @@ -23,19 +23,29 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): self.logger = logger - ''' value_flag: True if single point is selected. False if start and stop values are given.''' + """ + Quantities can be set to two different types of values + Single point value - quantity is set at a constant value through out the experiment + Start stop values - quantity is varied from start to stop values through a list of points determined by one of two attributes + step value -> divide [start, stop] into a list of points with 'step value' difference between each point + number of points -> divide [start, stop] into a list of 'number of points' points + """ + + """value_flag: True if single point is selected. False if start and stop values are given.""" self._value_flag = True - ''' step_flag: Applies if value_flag is false (a range is provided). True if step size is provided. False if number of steps is provided.''' + """step_flag: Applies if value_flag is false (a range is provided). True if step size is provided. False if number of steps is provided.""" self._step_flag = False - self._level = 1 - # self._single_point_value = self.get_value() # TODO: Error querying quantity + self._level = 1 # default level is set to 1 self._single_point_value = None self._start_value = None self._stop_value = None self._fixed_step = None self._fixed_number_of_steps = 1 + # self._interpolation = "Linear" + # Currently Experiment Runner only creates linear list of points for a range (np.linspace(start, stop, points)) + # This can be expanded to Logarithmic interlopation as well (np.logspace(start, stop, points)) self.setWindowTitle("Step setup") self.lab_experiment_icon = QIcon("../Icons/labExperiment.png") @@ -56,6 +66,7 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): self.group_box_layout.addWidget(separator) self.group_box_layout.addLayout(self.right_section_layout) + # Left section self.value_buttons_group = QButtonGroup() self.single_point_button = QRadioButton('Single point') self.value_buttons_group.addButton(self.single_point_button) @@ -70,6 +81,7 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): self.left_section_layout.addWidget(self.start_stop_button) self.left_section_layout.addLayout(self.start_stop_form_layout) + # Right section self.step_buttons_group = QButtonGroup() self.fixed_step_button = QRadioButton('Fixed step') self.step_buttons_group.addButton(self.fixed_step_button) @@ -84,13 +96,13 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): self.right_section_layout.addWidget(self.steps_number_button) self.right_section_layout.addLayout(self.steps_number_form_layout) + # Radio button connections self.single_point_button.toggled.connect(self.handle_value_checkboxes_toggle) self.start_stop_button.toggled.connect(self.handle_value_checkboxes_toggle) + """single point and start stop radio buttons are grouped together as value checkboxes""" self.fixed_step_button.toggled.connect(self.handle_step_checkboxes_toggle) - self.steps_number_button.toggled.connect(self.handle_step_checkboxes_toggle) - - - # use these in child classes to add widgets + self.steps_number_button.toggled.connect(self.handle_step_checkboxes_toggle) + """fixed step and steps number radio buttons are grouped togetehr as step checkboxes""" # Add level selector level_label = QLabel("Level:") @@ -116,22 +128,6 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): self.layout.addLayout(bottom_layout) self.setLayout(self.layout) - ''' - # Create menu for right click events - # Implemented into widget by subclasses - self.get_action = QAction('Query quantity value') - self.set_def_val_action = QAction('Set default value') - self.menu = QMenu() - self.menu.addAction(self.get_action) - self.menu.addAction(self.set_def_val_action) - ''' - - ####################################################################################################### - @property - def value(self): - """Returns current value of the quantity by the given QWidget. Should be implemented by child class""" - raise NotImplementedError() - @property def instrument_name(self): return self._name @@ -172,20 +168,7 @@ def stop_value(self): def number_of_points(self): if self.value_flag: return 1 - return self._fixed_number_of_steps - - ####################################################################################################### - - - ''' - # Can be modified to change format of the float values - def custom_context_menu_event(self): - action = self.menu.exec(QCursor.pos()) - if action == self.get_action: - self.get_value() - elif action == self.set_def_val_action: - self.set_default_value() - ''' + return self._fixed_number_of_steps def get_value(self): try: @@ -226,8 +209,10 @@ def handle_step_checkboxes_toggle(self): def save_data(self): """Saves the sequence data""" raise NotImplementedError() - +#################################################################### +# Children Classes for different quantity data types +#################################################################### class BooleanConstructor(SequenceConstructor): def __init__(self, quantity: QuantityManager, logger: logging.Logger): super().__init__(quantity, logger) @@ -270,15 +255,8 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): def handle_value_checkboxes_toggle(self): - # Individually disable corresponding widgets + """Individually disable corresponding widgets when value checkboxes are toggled""" if self.single_point_button.isChecked(): - # for i in range(layout.rowCount()): - # item = layout.itemAt(i) - # if item is not None: - # widget_item = item.widget() - # if widget_item is not None: - # widget_item.setEnabled(False) - self.single_point_label.setEnabled(True) self.single_point_combo_box.setEnabled(True) @@ -293,7 +271,6 @@ def handle_value_checkboxes_toggle(self): self.steps_number_button.setEnabled(False) self.steps_number_label.setEnabled(False) self.steps_number_spin_box.setEnabled(False) - # self.label.setText('Option 1 selected') else: self.single_point_label.setEnabled(False) self.single_point_combo_box.setEnabled(False) @@ -304,24 +281,21 @@ def handle_value_checkboxes_toggle(self): self.fixed_step_button.setEnabled(True) self.steps_number_button.setEnabled(True) self.handle_step_checkboxes_toggle() - # self.label.setText('Option 2 selected') def handle_step_checkboxes_toggle(self): - # Individually disable corresponding widgets + """Individually disable corresponding widgets when step checkboxes are toggled""" self.start_stop_button.setChecked(True) if self.fixed_step_button.isChecked(): self.fixed_step_label.setEnabled(True) self.fixed_step_spin_box.setEnabled(True) self.steps_number_label.setEnabled(False) self.steps_number_spin_box.setEnabled(False) - # self.label.setText('Option 1 selected') else: self.steps_number_button.setChecked(True) self.fixed_step_label.setEnabled(False) self.fixed_step_spin_box.setEnabled(False) self.steps_number_label.setEnabled(True) self.steps_number_spin_box.setEnabled(True) - # self.label.setText('Option 2 selected') def save_data(self): """Saves the sequence data""" @@ -348,17 +322,6 @@ def save_data(self): # Save user's data self.accept() - ''' - @SequenceConstructor.value.getter - def value(self): - return self.true_radio_button.isChecked() - - def handle_incoming_value(self, value): - self.true_radio_button.setChecked(value) - self.false_radio_button.setChecked(not value) - self.on_value_change(self.quantity.name, value) - ''' - class ButtonConstructor(SequenceConstructor): def __init__(self, quantity: QuantityManager, logger: logging.Logger): @@ -405,15 +368,8 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): def handle_value_checkboxes_toggle(self): - # Individually disable corresponding widgets + """Individually disable corresponding widgets when value checkboxes are toggled""" if self.single_point_button.isChecked(): - # for i in range(layout.rowCount()): - # item = layout.itemAt(i) - # if item is not None: - # widget_item = item.widget() - # if widget_item is not None: - # widget_item.setEnabled(False) - self.single_point_label.setEnabled(True) self.single_point_combo_box.setEnabled(True) @@ -428,7 +384,6 @@ def handle_value_checkboxes_toggle(self): self.steps_number_button.setEnabled(False) self.steps_number_label.setEnabled(False) self.steps_number_spin_box.setEnabled(False) - # self.label.setText('Option 1 selected') else: self.single_point_label.setEnabled(False) self.single_point_combo_box.setEnabled(False) @@ -439,24 +394,21 @@ def handle_value_checkboxes_toggle(self): self.fixed_step_button.setEnabled(True) self.steps_number_button.setEnabled(True) self.handle_step_checkboxes_toggle() - # self.label.setText('Option 2 selected') def handle_step_checkboxes_toggle(self): - # Individually disable corresponding widgets + """Individually disable corresponding widgets when step checkboxes are toggled""" self.start_stop_button.setChecked(True) if self.fixed_step_button.isChecked(): self.fixed_step_label.setEnabled(True) self.fixed_step_spin_box.setEnabled(True) self.steps_number_label.setEnabled(False) self.steps_number_spin_box.setEnabled(False) - # self.label.setText('Option 1 selected') else: self.steps_number_button.setChecked(True) self.fixed_step_label.setEnabled(False) self.fixed_step_spin_box.setEnabled(False) self.steps_number_label.setEnabled(True) self.steps_number_spin_box.setEnabled(True) - # self.label.setText('Option 2 selected') def save_data(self): """Saves the sequence data""" @@ -542,7 +494,7 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): self.steps_number_spin_box.valueChanged.connect(self.steps_number_changed) def fixed_step_changed(self): - # TODO: Change logic + """Executes when fixed step value is changed. Calculates and sets number of steps for this new fixed step value""" if self.fixed_step_spin_box.value() != 0.0: new_number_of_points = (self.stop_spin_box.value() - self.start_spin_box.value()) / self.fixed_step_spin_box.value() new_number_of_points = round(new_number_of_points) @@ -554,7 +506,7 @@ def fixed_step_changed(self): self.steps_number_spin_box.setValue(0) def steps_number_changed(self): - # TODO: Change logic + """Executes when number of steps is changed. Calculates and sets fixed step value for this new number of points""" if self.steps_number_spin_box.value() >= 2: new_fixed_step = (self.stop_spin_box.value() - self.start_spin_box.value()) / float(self.steps_number_spin_box.value() - 1) self.fixed_step_spin_box.setValue(new_fixed_step) @@ -562,15 +514,8 @@ def steps_number_changed(self): self.fixed_step_spin_box.setValue(0.0) def handle_value_checkboxes_toggle(self): - # Individually disable corresponding widgets + """Individually disable corresponding widgets when value checkboxes are toggled""" if self.single_point_button.isChecked(): - # for i in range(layout.rowCount()): - # item = layout.itemAt(i) - # if item is not None: - # widget_item = item.widget() - # if widget_item is not None: - # widget_item.setEnabled(False) - self.single_point_label.setEnabled(True) self.single_point_spin_box.setEnabled(True) @@ -585,7 +530,6 @@ def handle_value_checkboxes_toggle(self): self.steps_number_button.setEnabled(False) self.steps_number_label.setEnabled(False) self.steps_number_spin_box.setEnabled(False) - # self.label.setText('Option 1 selected') else: self.single_point_label.setEnabled(False) self.single_point_spin_box.setEnabled(False) @@ -596,24 +540,21 @@ def handle_value_checkboxes_toggle(self): self.fixed_step_button.setEnabled(True) self.steps_number_button.setEnabled(True) self.handle_step_checkboxes_toggle() - # self.label.setText('Option 2 selected') def handle_step_checkboxes_toggle(self): - # Individually disable corresponding widgets + """Individually disable corresponding widgets when step checkboxes are toggled""" self.start_stop_button.setChecked(True) if self.fixed_step_button.isChecked(): self.fixed_step_label.setEnabled(True) self.fixed_step_spin_box.setEnabled(True) self.steps_number_label.setEnabled(False) self.steps_number_spin_box.setEnabled(False) - # self.label.setText('Option 1 selected') else: self.steps_number_button.setChecked(True) self.fixed_step_label.setEnabled(False) self.fixed_step_spin_box.setEnabled(False) self.steps_number_label.setEnabled(True) self.steps_number_spin_box.setEnabled(True) - # self.label.setText('Option 2 selected') def save_data(self): """Saves the sequence data""" @@ -662,7 +603,9 @@ def __init__(self, quantity: QuantityManager, logger: logging.Logger): def sequence_constructor_factory(quantity: QuantityManager, logger: logging.Logger): - + """ + Creates a sequence constructor object for quantities of each data type + """ match quantity.data_type.upper(): case 'BOOLEAN': return BooleanConstructor(quantity, logger) diff --git a/GUI/sequence_table.py b/GUI/sequence_table.py index 2adef73..7ee4cfe 100644 --- a/GUI/sequence_table.py +++ b/GUI/sequence_table.py @@ -11,7 +11,8 @@ def __init__(self, channels_added: dict(), item_valid: Callable, logger: logging super().__init__() self.logger = logger - # dictionary for added channels in the table + # dictionary for available channels + # key -> Instrument Name, value -> InstrumentManager object self.channels_added = channels_added # dictionary for quantities of each added channel @@ -19,7 +20,7 @@ def __init__(self, channels_added: dict(), item_valid: Callable, logger: logging # value: SequenceConstructor object self.quantities_added = dict() - self.setDragEnabled(True) # TODO: Disable manual rearrangement + self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) # Disabling arrows in the Tree Widget @@ -52,18 +53,26 @@ def quantities(self): return self.quantities_added.keys() def check_item_valid(self, instrument_name, quantity_name): - # Check if the channel already exists + """Returns False if the quantity already exists in this table so that it cannot be added again or elsewhere""" if (instrument_name, quantity_name) in self.quantities_added.keys(): return False return True def dragEnterEvent(self, event): + """ + Implements drag enter event + Accepts only a json mime data object (dictionary created in Channels Table drag) + """ if event.mimeData().hasFormat('application/json'): event.accept() else: event.ignore() def dragMoveEvent(self, event): + """ + Implements drag move event + Accepts only a json mime data object (dictionary created in Channels Table drag) + """ if event.mimeData().hasFormat('application/json'): byte_data = event.mimeData().data('application/json') json_string = str(byte_data, 'utf-8') @@ -77,6 +86,11 @@ def dragMoveEvent(self, event): event.ignore() def dropEvent(self, event): + """ + Adds dragged Implements drag drop event to add quantity to this table + Accepts only a json mime data object (dictionary created in Channels Table drag) + Adds a sequence for this quantity using SequenceConstructor + """ if event.mimeData().hasFormat('application/json'): byte_data = event.mimeData().data('application/json') json_string = str(byte_data, 'utf-8') @@ -85,7 +99,6 @@ def dropEvent(self, event): instrument_name = data_dict['instrument_name'] quantity_name = data_dict['quantity_name'] - # TODO: Remove if (instrument_name, quantity_name) in self.quantities_added.keys(): event.ignore() return @@ -101,6 +114,7 @@ def dropEvent(self, event): event.ignore() def add_tree_item(self, sequence_constructor): + """Adds a QTreeWidgetItem for a quantity with its step sequence data""" instrument_name = sequence_constructor.instrument_name quantity_name = sequence_constructor.quantity_name level = sequence_constructor.level @@ -118,6 +132,7 @@ def add_tree_item(self, sequence_constructor): QTreeWidgetItem(self, [str(level), instrument_name, quantity_name, str(points), str(start) + unit +' - '+ str(stop) + unit]) def show_sequence_constructor_gui(self, selected_item, column=None): + """Displays a quantity's SequenceConstructor dialog and updates the QTreeWidgetItem with the modified step sequence data""" instrument_name = selected_item.text(1) quantity_name = selected_item.text(2) if (instrument_name, quantity_name) in self.quantities_added.keys(): @@ -142,8 +157,8 @@ def show_sequence_constructor_gui(self, selected_item, column=None): self.update_tree() return - """ Removes a selected quantitiy sequence from the Step Sequence Table """ def remove_quantity(self): + """Removes a selected quantitiy sequence from the Step Sequence Table""" selected_item = self.currentItem() if selected_item is not None: instrument_name = selected_item.text(1) @@ -151,8 +166,8 @@ def remove_quantity(self): del self.quantities_added[(instrument_name, quantity_name)] self.takeTopLevelItem(self.indexOfTopLevelItem(selected_item)) - """ Removes all quantity sequences related to an instrument from the Step Sequence Table """ def remove_channel(self, cute_name): + """Removes all quantity sequences related to an instrument from the Step Sequence Table""" if cute_name: # Traverse the tree in reverse to remove the tree widgets that belong to an instrument for index in range(self.topLevelItemCount() - 1, -1, -1): @@ -163,14 +178,17 @@ def remove_channel(self, cute_name): self.takeTopLevelItem(index) def update_tree(self): - # Sort the tree items based on level value + """Sort the tree items based on level value""" self.sortItems(0, Qt.SortOrder.AscendingOrder) def step_sequence_table_selection_changed(self): + """Handle selection change in Step Sequence table. Implement if needed.""" selected_item = self.currentItem() pass def validate_sequence(self): + """Check if the step sequences in the table are valid for an experiment + Check if quantities at the same level have the same number of points""" current_level = None current_points = None for i in range(self.topLevelItemCount()): @@ -201,6 +219,7 @@ def validate_sequence(self): return True def get_step_sequence_quantities(self): + """Provides input quantities and quantity sequences details for the Experiment DTO""" input_quantities = [] quantity_sequences = {} current_level = None