def __init__(self, ctr_client: si_tt.Client, ui='count_monitor', logger_client=None, server_port=None, combined_channel=False, config=None): """ Constructor for CountMonitor script :param ctr_client: instance of hardware client for counter :param gui_client: (optional) instance of client of desired output GUI :param logger_client: (obj) instance of logger client. :param server_port: (int) port number of script server :combined_channel: (bool) If true, show additional trace with summed counts. """ self._ctr = ctr_client self.log = logger_client self.combined_channel = combined_channel self._bin_width = None self._n_bins = None self._ch_list = None self._plot_list = None # List of channels to assign to each plot (e.g. [[1,2], [3,4]]) self._plots_assigned = [ ] # List of plots on the GUI that have been assigned if self.combined_channel: ui = 'count_monitor_combined' else: ui = 'count_monitor' # Instantiate GUI window self.gui = Window(gui_template=ui, host=get_ip(), port=server_port) # Setup stylesheet. self.gui.apply_stylesheet() if self.combined_channel: num_plots = 3 else: num_plots = 2 # Get all GUI widgets self.widgets = get_gui_widgets(self.gui, graph_widget=num_plots, number_label=8, event_button=num_plots, legend_widget=num_plots) # Load config self.config = {} if config is not None: self.config = load_script_config(script='monitor_counts', config=config, logger=self.logger_client) if not 'name' in self.config: self.config.update({'name': f'monitor{np.random.randint(1000)}'})
def __init__(self, nanopos_client: smaract_mcs2.Client, attocube_client: attocube_anc300.Client, gui='positioner_control_mixed', log_client=None, config=None, port=None): """ Instantiates the controller :param nanopos_client: (pylabnet.network.client_server.smaract_mcs2.Client) :param gui: name of .ui file ot use :param log_client: (pylabnet.utils.logging.logger.LogClient) :param config: (str) name of config file, optional :param port: (int) port number for update/script server """ self.pos = nanopos_client self.attocube = attocube_client self.log = LogHandler(logger=log_client) self.gui = Window(gui_template=gui, host=get_ip(), port=port) self.gui.apply_stylesheet() self.widgets = get_gui_widgets(self.gui, **self.WIDGET_DICT) self.save_params = generate_widgets( dict(n_steps=self.NUM_CHANNELS, amplitude=self.NUM_CHANNELS, frequency=self.NUM_CHANNELS, velocity=self.NUM_CHANNELS)) # Additional attributes self.prev_amplitude = [50] * self.NUM_CHANNELS self.prev_frequency = [30] * self.NUM_CHANNELS self.prev_velocity = [100] * self.NUM_CHANNELS self.prev_voltage = [50] * self.NUM_CHANNELS self.voltage_override = False self.config = config self.lock_status = [False] * int(self.NUM_CHANNELS / 3) self.released = [False] * self.NUM_CHANNELS self.gui.config_label.setText(self.config) # Configure all button and parameter updates self._setup_gui() # Setup shortcut to use keyboard to step fiber self.press_right = QShortcut(QKeySequence('Right'), self.gui) self.press_left = QShortcut(QKeySequence('Left'), self.gui) self.press_up = QShortcut(QKeySequence('Up'), self.gui) self.press_down = QShortcut(QKeySequence('Down'), self.gui) self.press_up_z = QShortcut(QKeySequence('PgUp'), self.gui) self.press_down_z = QShortcut(QKeySequence('PgDown'), self.gui) self.widgets['keyboard_change_combo'].currentIndexChanged.connect( self._bind_arrow_keys)
def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) # Configure dataset menu for name, obj in inspect.getmembers(datasets): if inspect.isclass(obj) and issubclass(obj, datasets.Dataset): self.gui.dataset.addItem(name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet()
def __init__(self, filterwheel1, filterwheel2, gui='toptica_filterwheels', logger=None, port=None): self.filterwheel1 = filterwheel1 self.filterwheel2 = filterwheel2 self.log = LogHandler(logger) # Setup GUI self.gui = Window(gui_template=gui, host=get_ip(), port=port) # Get Widgets self.widgets = get_gui_widgets(self.gui, comboBox_filter1=1, comboBox_filter2=1, nd_label=1) # Retrieve filter dicts. filters1 = filterwheel1.get_filter_dict() filters2 = filterwheel2.get_filter_dict() # Fill comboboxes. self.widgets['comboBox_filter1'].addItems(filters1.values()) self.widgets['comboBox_filter2'].addItems(filters2.values()) # Get current fitler positions self.current_pos_1 = filterwheel1.get_pos() self.current_pos_2 = filterwheel2.get_pos() # Set comboboxes to current filter positions. self.widgets['comboBox_filter1'].setCurrentIndex( int(self.current_pos_1) - 1) self.widgets['comboBox_filter2'].setCurrentIndex( int(self.current_pos_2) - 1) # Connect change events self.widgets['comboBox_filter1'].currentTextChanged.connect( lambda: self.change_filter(filter_index=1)) self.widgets['comboBox_filter2'].currentTextChanged.connect( lambda: self.change_filter(filter_index=2)) # Update OD reading self.update_od() # Setup stylesheet. self.gui.apply_stylesheet()
def __init__(self, dlc: toptica_dl_pro.Client, gui='toptica_control', logger=None, port=None): """ Initializes toptica specific parameters :param dlc: DLC client for the Toptica laser :param gui: .ui file to use :param logger: LogClient for logging purposes :param port: port number of script server """ self.log = LogHandler(logger) # Setup GUI self.gui = Window(gui_template=gui, host=get_ip(), port=port) self.widgets = get_gui_widgets(gui=self.gui, on_off=2, temperature=2, temperature_actual=2, current=2, current_actual=2, offset=2, amplitude=2, frequency=2, scan=2, update_temp=2, update_current=2, update_params=1) self.dlc = dlc self.offset = 65 self.amplitude = 100 self.scan = [False, False] self.emission = [False, False] # Setup stylesheet. self.gui.apply_stylesheet() self._setup_GUI()
def __init__(self, pm_client, gui='fiber_coupling', logger=None, calibration=None, name=None, port=None): """ Instantiates a monitor for 2-ch power meter with GUI :param pm_clients: (client, list of clients) clients of power meter :param gui_client: client of monitor GUI :param logger: instance of LogClient :calibration: (float) Calibration value for power meter. :name: (str) Humand-readable name of the power meter. """ self.log = LogHandler(logger) self.gui = Window(gui_template=gui, host=get_ip(), port=port) self.gui.apply_stylesheet() self.wavelength = [] self.calibration = calibration self.name = name self.ir_index, self.rr_index = [], [] self.pm = pm_client self.running = False self.num_plots = 3 # Get all GUI widgets self.widgets = get_gui_widgets(self.gui, graph_widget=self.num_plots, number_widget=4, label_widget=2, name_label=1, combo_widget=2) self._initialize_gui()
class TopticaFilterWheelController: """ Class for controlling Toptica scan and laser properties """ def __init__(self, filterwheel1, filterwheel2, gui='toptica_filterwheels', logger=None, port=None): self.filterwheel1 = filterwheel1 self.filterwheel2 = filterwheel2 self.log = LogHandler(logger) # Setup GUI self.gui = Window(gui_template=gui, host=get_ip(), port=port) # Get Widgets self.widgets = get_gui_widgets(self.gui, comboBox_filter1=1, comboBox_filter2=1, nd_label=1) # Retrieve filter dicts. filters1 = filterwheel1.get_filter_dict() filters2 = filterwheel2.get_filter_dict() # Fill comboboxes. self.widgets['comboBox_filter1'].addItems(filters1.values()) self.widgets['comboBox_filter2'].addItems(filters2.values()) # Get current fitler positions self.current_pos_1 = filterwheel1.get_pos() self.current_pos_2 = filterwheel2.get_pos() # Set comboboxes to current filter positions. self.widgets['comboBox_filter1'].setCurrentIndex( int(self.current_pos_1) - 1) self.widgets['comboBox_filter2'].setCurrentIndex( int(self.current_pos_2) - 1) # Connect change events self.widgets['comboBox_filter1'].currentTextChanged.connect( lambda: self.change_filter(filter_index=1)) self.widgets['comboBox_filter2'].currentTextChanged.connect( lambda: self.change_filter(filter_index=2)) # Update OD reading self.update_od() # Setup stylesheet. self.gui.apply_stylesheet() def change_filter(self, filter_index): ''' Read in index from combobox and change filter accordingly.''' # Read in new filter. new_pos = int( self.widgets[f'comboBox_filter{filter_index}'].currentIndex()) + 1 if filter_index == 1: filterwheel = self.filterwheel1 elif filter_index == 2: filterwheel = self.filterwheel2 # Disable comboboxes self.widgets['comboBox_filter1'].setEnabled(False) self.widgets['comboBox_filter2'].setEnabled(False) # Change position successful_changed = filterwheel.change_filter(new_pos) if not successful_changed: # Read in new position to verify changed_position = filterwheel.get_pos() # Set combobox to new position self.widgets[f'comboBox_filter{filter_index}'].setCurrentIndex( int(changed_position) - 1) # Update OD reading self.update_od() # Enable combobos self.widgets['comboBox_filter1'].setEnabled(True) self.widgets['comboBox_filter2'].setEnabled(True) def update_od(self): filter_pos1 = int(self.widgets['comboBox_filter1'].currentIndex()) + 1 filter_string1 = self.filterwheel1.get_filter_dict()[str(filter_pos1)] filter1_od = float(filter_string1.replace(' OD', "")) filter_pos2 = int(self.widgets['comboBox_filter2'].currentIndex()) + 1 filter_string2 = self.filterwheel2.get_filter_dict()[str(filter_pos2)] filter2_od = float(filter_string2.replace(' OD', "")) new_od_string = f"{filter1_od+filter2_od} OD" self.widgets['nd_label'].setText(new_od_string) def run(self, check_vals=False): """ Runs an iteration of checks for updates and implements
class CountMonitor: # Generate all widget instances for the .ui to use # _plot_widgets, _legend_widgets, _number_widgets = generate_widgets() def __init__(self, ctr_client: si_tt.Client, ui='count_monitor', logger_client=None, server_port=None, combined_channel=False, config=None): """ Constructor for CountMonitor script :param ctr_client: instance of hardware client for counter :param gui_client: (optional) instance of client of desired output GUI :param logger_client: (obj) instance of logger client. :param server_port: (int) port number of script server :combined_channel: (bool) If true, show additional trace with summed counts. """ self._ctr = ctr_client self.log = logger_client self.combined_channel = combined_channel self._bin_width = None self._n_bins = None self._ch_list = None self._plot_list = None # List of channels to assign to each plot (e.g. [[1,2], [3,4]]) self._plots_assigned = [ ] # List of plots on the GUI that have been assigned if self.combined_channel: ui = 'count_monitor_combined' else: ui = 'count_monitor' # Instantiate GUI window self.gui = Window(gui_template=ui, host=get_ip(), port=server_port) # Setup stylesheet. self.gui.apply_stylesheet() if self.combined_channel: num_plots = 3 else: num_plots = 2 # Get all GUI widgets self.widgets = get_gui_widgets(self.gui, graph_widget=num_plots, number_label=8, event_button=num_plots, legend_widget=num_plots) # Load config self.config = {} if config is not None: self.config = load_script_config(script='monitor_counts', config=config, logger=self.logger_client) if not 'name' in self.config: self.config.update({'name': f'monitor{np.random.randint(1000)}'}) def set_hardware(self, ctr): """ Sets hardware client for this script :param ctr: instance of count monitor hardware client """ # Initialize counter instance self._ctr = ctr def set_params(self, bin_width=1e9, n_bins=1e4, ch_list=[1], plot_list=None): """ Sets counter parameters :param bin_width: bin width in ps :param n_bins: number of bins to display on graph :param ch_list: (list) channels to record :param plot_list: list of channels to assign to each plot (e.g. [[1,2], [3,4]]) """ # Save params to internal variables self._bin_width = int(bin_width) self._n_bins = int(n_bins) self._ch_list = ch_list self._plot_list = plot_list def run(self): """ Runs the counter from scratch""" try: # Start the counter with desired parameters self._initialize_display() # Give time to initialize # time.sleep(0.05) self._is_running = True self._ctr.start_trace(name=self.config['name'], ch_list=self._ch_list, bin_width=self._bin_width, n_bins=self._n_bins) # Continuously update data until paused while self._is_running: self._update_output() self.gui.force_update() except Exception as exc_obj: self._is_running = False raise exc_obj def pause(self): """ Pauses the counter""" self._is_running = False def resume(self): """ Resumes the counter. To be used to resume after the counter has been paused. """ try: self._is_running = True # Clear counter and resume plotting self._ctr.clear_ctr(name=self.config['name']) while self._is_running: self._update_output() except Exception as exc_obj: self._is_running = False raise exc_obj # Technical methods def _initialize_display(self): """ Initializes the display (configures all plots) """ plot_index = 0 for index in range(len(self.widgets['graph_widget'])): # Configure and return legend widgets self.widgets['legend_widget'][ index] = get_legend_from_graphics_view( self.widgets['legend_widget'][index]) for color, channel in enumerate(self._ch_list): # Figure out which plot to assign to if self._plot_list is not None: for index, channel_set in enumerate(self._plot_list): if channel in channel_set: plot_index = index break # If we have not assigned this plot yet, assign it # if plot_index not in self._plots_assigned: # self.gui_handler.assign_plot( # plot_widget=self._plot_widgets[plot_index], # plot_label='Counter Monitor {}'.format(plot_index + 1), # legend_widget=self._legend_widgets[plot_index] # ) # self._plots_assigned.append(plot_index) # Now assign this curve # self.gui_handler.assign_curve( # plot_label='Counter Monitor {}'.format(plot_index + 1), # curve_label='Channel {}'.format(channel), # error=True # ) # Create a curve and store the widget in our dictionary self.widgets[f'curve_{channel}'] = self.widgets['graph_widget'][ plot_index].plot(pen=pg.mkPen( color=self.gui.COLOR_LIST[color])) self.widgets['legend_widget'][plot_index].addItem( self.widgets[f'curve_{channel}'], ' - ' + f'Channel {channel}') # Assign scalar # self.gui_handler.assign_label( # label_widget=self._number_widgets[channel - 1], # label_label='Channel {}'.format(channel) # ) # Handle button pressing from functools import partial for plot_index, clear_button in enumerate( self.widgets['event_button']): clear_button.clicked.connect( partial(lambda plot_index: self._clear_plot(plot_index), plot_index=plot_index)) if self.combined_channel: self.widgets['curve_combo'] = self.widgets['graph_widget'][ index + 1].plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[color + 1])) self.widgets['legend_widget'][index + 1].addItem( self.widgets['curve_combo'], ' - ' + 'Combined Counts') def _clear_plot(self, plot_index): """ Clears the curves on a particular plot :param plot_index: (int) index of plot to clear """ # First, handle case where combined count channel is clears (very ugly). if self.combined_channel and plot_index == len(self._plot_list): channel = 'combo' # Set the curve to constant with last point for all entries self.widgets[f'curve_{channel}'].setData( np.ones(self._n_bins) * self.widgets[f'curve_{channel}'].yData[-1]) else: # Find all curves in this plot for channel in self._plot_list[plot_index]: # Set the curve to constant with last point for all entries self.widgets[f'curve_{channel}'].setData( np.ones(self._n_bins) * self.widgets[f'curve_{channel}'].yData[-1]) self._ctr.clear_ctr(name=self.config['name']) def _update_output(self): """ Updates the output to all current values""" # Update all active channels # x_axis = self._ctr.get_x_axis()/1e12 counts = self._ctr.get_counts(name=self.config['name']) counts_per_sec = counts * (1e12 / self._bin_width) # noise = np.sqrt(counts)*(1e12/self._bin_width) # plot_index = 0 summed_counts = np.sum(counts_per_sec, axis=0) for index, count_array in enumerate(counts_per_sec): # Figure out which plot to assign to channel = self._ch_list[index] # if self._plot_list is not None: # for index_plot, channel_set in enumerate(self._plot_list): # if channel in channel_set: # plot_index = index_plot # break # Update GUI data # self.gui_handler.set_curve_data( # data=count_array, # error=noise[index], # plot_label='Counter Monitor {}'.format(plot_index + 1), # curve_label='Channel {}'.format(channel) # ) # self.gui_handler.set_label( # text='{:.4e}'.format(count_array[-1]), # label_label='Channel {}'.format(channel) # ) self.widgets[f'curve_{channel}'].setData(count_array) self.widgets[f'number_label'][channel - 1].setText( str(count_array[-1])) if self.combined_channel: self.widgets['curve_combo'].setData(summed_counts)
def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Configure list of missing clients self.missing_clients = {} # Setup Autosave # First check whether autosave is specified in config file if 'auto_save' in self.config: if self.config['auto_save']: self.gui.autosave.setChecked(True) # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) if (client == None): self.missing_clients[f"{client_type}_{client_config}"] = [ client_type, client_config ] else: self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) for client_name, client_config in self.missing_clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setForeground(Qt.gray) self.gui.clients.addItem(client_item) self.log.error("Datataker missing client: " + client_name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.clearData.clicked.connect(self.clear_data) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet()
class DataTaker: def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Configure list of missing clients self.missing_clients = {} # Setup Autosave # First check whether autosave is specified in config file if 'auto_save' in self.config: if self.config['auto_save']: self.gui.autosave.setChecked(True) # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) if (client == None): self.missing_clients[f"{client_type}_{client_config}"] = [ client_type, client_config ] else: self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) for client_name, client_config in self.missing_clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setForeground(Qt.gray) self.gui.clients.addItem(client_item) self.log.error("Datataker missing client: " + client_name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.clearData.clicked.connect(self.clear_data) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet() def update_experiment_list(self): """ Updates list of experiments """ self.gui.exp.clear() for filename in os.listdir(self.exp_path): if filename.endswith('.py'): self.gui.exp.addItem(filename[:-3]) self.gui.exp.itemClicked.connect(self.display_experiment) def display_experiment(self, item): """ Displays the currently clicked experiment in the text browser :param item: (QlistWidgetItem) with label of name of experiment to display """ with open(os.path.join(self.exp_path, f'{item.text()}.py'), 'r') as exp_file: exp_content = exp_file.read() self.gui.exp_preview.setText(exp_content) self.gui.exp_preview.setStyleSheet('font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(0, 0, 0);') self.log.update_metadata(experiment_file=exp_content) def configure(self): """ Configures the currently selected experiment + dataset """ # If the experiment is running, do nothing try: if self.experiment_thread.isRunning(): self.log.warn('Did not configure experiment, since it ' 'is still in progress') return except: pass # Load the config self.reload_config() # Set all experiments to normal state and highlight configured expt for item_no in range(self.gui.exp.count()): self.gui.exp.item(item_no).setBackground( QtGui.QBrush(QtGui.QColor('black'))) self.gui.exp.currentItem().setBackground( QtGui.QBrush(QtGui.QColor('darkRed'))) exp_name = self.gui.exp.currentItem().text() self.module = importlib.import_module(exp_name) self.module = importlib.reload(self.module) # Clear graph area and set up new or cleaned up dataset for index in reversed(range(self.gui.graph_layout.count())): try: self.gui.graph_layout.itemAt(index).widget().deleteLater() except AttributeError: try: self.gui.graph_layout.itemAt(index).layout().deleteLater() except AttributeError: pass self.gui.windows = {} # If we're not setting up a new measurement type, just clear the data # We are reading in the required base-dataset by looking at the define_dataset() as defined in the experiment script. try: classname = self.module.define_dataset() except AttributeError: error_msg = "No 'define_dataset' method found in experiment script." self.log.error( "No 'define_dataset' method found in experiment script.") return try: self.dataset = getattr(datasets, classname)(gui=self.gui, log=self.log, config=self.config) except AttributeError: error_msg = f"Dataset name {classname} as provided in 'define_dataset' method in experiment script is not valid." self.log.error(error_msg) return # Run any pre-experiment configuration try: self.module.configure(dataset=self.dataset, **self.clients) except AttributeError: pass self.experiment = self.module.experiment self.log.info(f'Experiment {exp_name} configured') self.gui.exp_preview.setStyleSheet( 'font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(50, 50, 50);') def clear_data(self): """ Clears all data from curves""" self.log.info("Clearing data") self.dataset.clear_all_data() def run(self): """ Runs/stops the experiment """ # Run experiment if self.gui.run.text() == 'Run': self.gui.run.setStyleSheet('background-color: red') self.gui.run.setText('Stop') self.log.info('Experiment started') # Run update thread self.update_thread = UpdateThread( autosave=self.gui.autosave.isChecked(), save_time=self.gui.autosave_interval.value()) self.update_thread.data_updated.connect(self.dataset.update) self.update_thread.save_flag.connect(self.save) self.gui.autosave.toggled.connect( self.update_thread.update_autosave) self.gui.autosave_interval.valueChanged.connect( self.update_thread.update_autosave_interval) ''' # Step 2: Create a QThread object self.thread = QThread() # Step 3: Create a worker object self.worker = Worker() # Step 4: Move worker to the thread self.worker.moveToThread(self.thread) # Step 5: Connect signals and slots self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) # Step 6: Start the thread self.thread.start() # Final resets self.longRunningBtn.setEnabled(False) self.thread.finished.connect( lambda: self.longRunningBtn.setEnabled(True) ) self.thread.finished.connect( lambda: self.stepLabel.setText("Long-Running Step: 0") ) ''' self.experiment_thread = ExperimentThread(self.experiment, dataset=self.dataset, gui=self.gui, **self.clients) self.experiment_thread.status_flag.connect( self.dataset.interpret_status) self.experiment_thread.finished.connect(self.stop) self.log.update_metadata( exp_start_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Stop experiment else: self.experiment_thread.running = False def stop(self): """ Stops the experiment""" self.gui.run.setStyleSheet('background-color: green') self.gui.run.setText('Run') self.log.info('Experiment stopped') self.update_thread.running = False self.log.update_metadata( exp_stop_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Autosave if relevant if self.gui.autosave.isChecked(): self.save() def save(self): """ Saves data """ self.log.update_metadata(notes=self.gui.notes.toPlainText()) filename = self.gui.save_name.text() directory = self.config['save_path'] self.dataset.save(filename=filename, directory=directory, date_dir=True) save_metadata(self.log, filename, directory, True) self.log.info('Data saved') def reload_config(self): """ Loads a new config file """ self.config = load_script_config(script='data_taker', config=self.gui.config.text(), logger=self.log)
class Controller: """ A script class for controlling MCS2 positioners + interfacing with GUI""" NUM_CHANNELS = 9 WIDGET_DICT = dict(step_left=NUM_CHANNELS, step_right=NUM_CHANNELS, walk_left=NUM_CHANNELS, walk_right=NUM_CHANNELS, n_steps=NUM_CHANNELS, is_moving=NUM_CHANNELS, amplitude=NUM_CHANNELS, frequency=NUM_CHANNELS, velocity=NUM_CHANNELS, voltage=NUM_CHANNELS, lock_button=int(NUM_CHANNELS / 3), keyboard_change_combo=1) DC_TOLERANCE = 0.1 AXIS_ORDER = [[4, 3, 7], [6, 1, 5], [8, 0, 2]] def __init__(self, nanopos_client: smaract_mcs2.Client, gui='positioner_control', log_client=None, config=None, port=None): """ Instantiates the controller :param nanopos_client: (pylabnet.network.client_server.smaract_mcs2.Client) :param gui: name of .ui file ot use :param log_client: (pylabnet.utils.logging.logger.LogClient) :param config: (str) name of config file, optional :param port: (int) port number for update/script server """ self.pos = nanopos_client self.log = LogHandler(logger=log_client) self.gui = Window(gui_template=gui, host=get_ip(), port=port) self.gui.apply_stylesheet() self.widgets = get_gui_widgets(self.gui, **self.WIDGET_DICT) self.save_params = generate_widgets( dict(n_steps=self.NUM_CHANNELS, amplitude=self.NUM_CHANNELS, frequency=self.NUM_CHANNELS, velocity=self.NUM_CHANNELS)) # Additional attributes self.prev_amplitude = [50] * self.NUM_CHANNELS self.prev_frequency = [30] * self.NUM_CHANNELS self.prev_velocity = [100] * self.NUM_CHANNELS self.prev_voltage = [50] * self.NUM_CHANNELS self.voltage_override = False self.config = config self.lock_status = [False] * int(self.NUM_CHANNELS / 3) self.released = [False] * self.NUM_CHANNELS self.gui.config_label.setText(self.config) # Configure all button and parameter updates self._setup_gui() # Setup shortcut to use keyboard to step fiber self.press_right = QShortcut(QKeySequence('Right'), self.gui) self.press_left = QShortcut(QKeySequence('Left'), self.gui) self.press_up = QShortcut(QKeySequence('Up'), self.gui) self.press_down = QShortcut(QKeySequence('Down'), self.gui) self.press_up_z = QShortcut(QKeySequence('PgUp'), self.gui) self.press_down_z = QShortcut(QKeySequence('PgDown'), self.gui) self.widgets['keyboard_change_combo'].currentIndexChanged.connect( self._bind_arrow_keys) def _disconnect_arrow_keys(self): """ Unbinds the arrow, up/down keys from any actions.""" self.press_right.activated.disconnect() self.press_left.activated.disconnect() self.press_up.activated.disconnect() self.press_down.activated.disconnect() self.press_up_z.activated.disconnect() self.press_down_z.activated.disconnect() def _bind_arrow_keys(self): """ Binds arroy keys on keyboard to step around front fiber.""" try: self._disconnect_arrow_keys() except TypeError: self.log.info('Initial call of arrowkey binding.') binding_index = self.widgets['keyboard_change_combo'].currentIndex() front_names = [ 'step_left', 'step_right', 'step_left', 'step_right', 'step_right', 'step_left' ] front_index = [6, 6, 1, 1, 5, 5] rear_names = [ 'step_right', 'step_left', 'step_right', 'step_left', 'step_right', 'step_left' ] rear_index = [8, 8, 0, 0, 2, 2] if binding_index == 0: return if binding_index == 1: names = front_names index = front_index elif binding_index == 2: names = rear_names index = rear_index self.press_right.activated.connect( lambda: self.widgets[names[0]][index[0]].animateClick()) self.press_left.activated.connect( lambda: self.widgets[names[1]][index[1]].animateClick()) self.press_up.activated.connect( lambda: self.widgets[names[2]][index[2]].animateClick()) self.press_down.activated.connect( lambda: self.widgets[names[3]][index[3]].animateClick()) self.press_up_z.activated.connect( lambda: self.widgets[names[4]][index[4]].animateClick()) self.press_down_z.activated.connect( lambda: self.widgets[names[5]][index[5]].animateClick()) def initialize_parameters(self, channel, params): """ Initializes all parameters to values given by params, except for DC voltage :param channel: (int) channel index (from 0) :param params: (tuple) params in order n_steps, is_moving, amplitude, frequency, velocity, voltage """ self.pos.set_parameters(channel, amplitude=params[2]) self.pos.set_parameters(channel, frequency=params[3]) self.pos.set_parameters(channel, dc_vel=params[4]) # Measure DC voltage and set it in the GUI self._set_voltage_display(channel) def get_GUI_parameters(self, channel): """ Gets the current GUI parameters for a given channel :param channel: (int) channel index (from 0) :return: (tuple) params in order n_steps, is_moving, amplitude, frequency, velocity, voltage """ return (self.widgets['n_steps'][channel].value(), self.widgets['is_moving'][channel].isChecked(), self.widgets['amplitude'][channel].value(), self.widgets['frequency'][channel].value(), self.widgets['velocity'][channel].value(), self.widgets['voltage'][channel].value()) def stop_all(self): """ Stops all channels """ for channel in range(self.NUM_CHANNELS): self.pos.stop(channel) def load_settings(self): """ Loads settings from configuration """ self.gui.load_gui("mcs2_control", self.gui.config_label.text(), logger=self.log) # Technical methods def _set_voltage_display(self, channel): """ Sets the voltage on the GUI to the current value measured by the controller :param channel: (int) channel index (from 0) """ voltage = self.pos.get_voltage(channel) self.prev_voltage[channel] = voltage self.widgets['voltage'][channel].setValue(voltage) self.voltage_override = True def save(self): """Saves or loads settings if relevant""" scalars = [] for save_item in self.save_params: scalars += save_item self.gui.save_gui(self.gui.get_text('config_label'), logger=self.log, scalars=scalars) def _lock_stack(self, stack: int): """ Locks/unlocks a particular stack""" if self.lock_status[stack]: self.lock_status[stack] = False self.widgets['lock_button'][stack].setStyleSheet( 'background-color:rgb(100,0,0)') self.widgets['lock_button'][stack].setText('Lock') else: self.lock_status[stack] = True self.widgets['lock_button'][stack].setStyleSheet( 'background-color:rgb(0,100,0)') self.widgets['lock_button'][stack].setText('Unlock') def _is_axis_locked(self, channel: int): """ Checks if an axis is locked and returns boolean :param channel: (int) axis to check :return: (bool) whether or not the axis is currently locked """ for index, ordering in enumerate(self.AXIS_ORDER): if channel in ordering: locked = self.lock_status[index] return locked def _step_left(self, channel: int): """ Steps a particular channel if unlocked :param channel: (int) channel to step """ if not self._is_axis_locked(channel): self.pos.n_steps(channel=channel, n=-self.widgets['n_steps'][channel].value()) self.widgets['step_left'][channel].setStyleSheet( 'background-color:red') if self.pos.is_moving(channel): while self.pos.is_moving(channel): self.widgets['is_moving'][channel].setCheckable(True) self.widgets['is_moving'][channel].setChecked(True) self.gui.force_update() self.widgets['is_moving'][channel].setChecked(False) self.widgets['is_moving'][channel].setCheckable(False) self.widgets['step_left'][channel].setStyleSheet( 'background-color:black') # Ugly hack to set DC voltage of Front X to 0 self.log.info(f"Channel {channel}") if channel in BROKEN_CHANNELS: self._update_voltage(channel, 0) self._set_voltage_display(channel) else: self.log.info("LOCKED") def _step_right(self, channel: int): """ Steps a particular channel if unlocked :param channel: (int) channel to step """ if not self._is_axis_locked(channel): self.pos.n_steps(channel=channel, n=self.widgets['n_steps'][channel].value()) self.widgets['step_right'][channel].setStyleSheet( 'background-color:red') if self.pos.is_moving(channel): while self.pos.is_moving(channel): self.widgets['is_moving'][channel].setCheckable(True) self.widgets['is_moving'][channel].setChecked(True) self.gui.force_update() self.widgets['is_moving'][channel].setChecked(False) self.widgets['is_moving'][channel].setCheckable(False) self.widgets['step_right'][channel].setStyleSheet( 'background-color:black') # Ugly hack to set DC voltage of Front X to 0 if channel in BROKEN_CHANNELS: self._update_voltage(channel, 0) self._set_voltage_display(channel) def _walk_left(self, channel: int): if not self._is_axis_locked(channel): if not self.pos.is_moving(channel): self.widgets['walk_left'][channel].setStyleSheet( 'background-color:red') self.pos.move(channel, backward=True) while self.pos.is_moving(channel): self.widgets['is_moving'][channel].setCheckable(True) self.widgets['is_moving'][channel].setChecked(True) self.gui.force_update() self.widgets['is_moving'][channel].setChecked(False) self.widgets['is_moving'][channel].setCheckable(False) if not self.widgets['walk_left'][channel].isDown(): self.widgets['walk_left'][channel].setStyleSheet( 'background-color:black') self._set_voltage_display(channel) def _walk_right(self, channel: int): if not self._is_axis_locked(channel): if not self.pos.is_moving(channel): self.widgets['walk_right'][channel].setStyleSheet( 'background-color:red') self.pos.move(channel, backward=False) while self.pos.is_moving(channel): self.widgets['is_moving'][channel].setCheckable(True) self.widgets['is_moving'][channel].setChecked(True) self.gui.force_update() self.widgets['is_moving'][channel].setChecked(False) self.widgets['is_moving'][channel].setCheckable(False) if not self.widgets['walk_right'][channel].isDown(): self.widgets['walk_right'][channel].setStyleSheet( 'background-color:black') self._set_voltage_display(channel) def _update_voltage(self, channel: int, voltage: float): """ Updates the channels DC voltage :param channel: (int) channel to update :param voltage: (float) value of voltage to update ot """ # If locked, get the current voltage and reset the GUI value to it if self._is_axis_locked(channel): self.widgets['voltage'][channel].setValue( self.pos.get_voltage(channel)) # Otherwise set the DC voltage else: self.pos.set_voltage(channel, voltage) if self.pos.is_moving(channel): while self.pos.is_moving(channel): self.widgets['is_moving'][channel].setCheckable(True) self.widgets['is_moving'][channel].setChecked(True) self.gui.force_update() self.widgets['is_moving'][channel].setChecked(False) self.widgets['is_moving'][channel].setCheckable(False) def _setup_gui(self): """ Configures what all buttons do """ self.gui.load_button.clicked.connect(self.load_settings) self.gui.save_button.clicked.connect(self.save) self.gui.emergency_button.clicked.connect(self.stop_all) # Stack based items (common to 3 axes) for stack in range(int(self.NUM_CHANNELS / 3)): stack_no = copy.deepcopy(stack) # Lock button self.widgets['lock_button'][stack].pressed.connect( lambda stack=stack_no: self._lock_stack(stack)) for channel in range(self.NUM_CHANNELS): channel_no = copy.deepcopy(channel) # Step buttons self.widgets['step_left'][channel_no].pressed.connect( lambda channel=channel_no: self._step_left(channel)) self.widgets['step_right'][channel_no].pressed.connect( lambda channel=channel_no: self._step_right(channel)) # Walk buttons self.widgets['walk_left'][channel_no].pressed.connect( lambda channel=channel_no: self._walk_left(channel)) self.widgets['walk_left'][channel_no].released.connect( lambda channel=channel_no: self.pos.stop(channel)) self.widgets['walk_right'][channel_no].pressed.connect( lambda channel=channel_no: self._walk_right(channel)) self.widgets['walk_right'][channel_no].released.connect( lambda channel=channel_no: self.pos.stop(channel)) # Parameters self.widgets['voltage'][channel_no].valueChanged.connect( lambda state, channel=channel_no: self._update_voltage( channel=channel, voltage=state)) self.widgets['amplitude'][channel_no].valueChanged.connect( lambda state, channel=channel_no: self.pos.set_parameters( channel=channel, amplitude=state)) self.widgets['frequency'][channel_no].valueChanged.connect( lambda state, channel=channel_no: self.pos.set_parameters( channel=channel, frequency=state)) self.widgets['velocity'][channel_no].valueChanged.connect( lambda state, channel=channel_no: self.pos.set_parameters( channel=channel, dc_vel=state))
class Controller(MultiChSweep1D): def __init__(self, logger=None, channels=['Channel 1'], clients={}, config=None, fast=True): """ Instantiates controller (only 1D data supported so far) :param logger: instance of LogClient :param channels: (list) list of channel names :param config: (str) name of config file :param fast: (bool) whether to operate in fast mode fast mode only updates heat maps at the end of each scan (this speeds things up) """ self.config = load_script_config('scan1d', config, logger=logger) if 'sweep_type' in self.config.keys(): self.sweep_type = self.config['sweep_type'] else: self.sweep_type = 'triangle' super().__init__(logger, channels, sweep_type=self.sweep_type) self.module = None # Instantiate GUI self.gui = Window(gui_template='scan_1d', host=get_ip(), max=True) self.widgets = get_gui_widgets(self.gui, p_min=1, p_max=1, pts=1, config=1, graph=2, legend=2, clients=1, exp=1, exp_preview=1, configure=1, run=1, autosave=1, save_name=1, save=1, reps=1, rep_tracker=1, avg=2) # Configure default parameters self.min = self.widgets['p_min'].value() self.max = self.widgets['p_max'].value() self.pts = self.widgets['pts'].value() self.reps = self.widgets['reps'].value() self.data_fwd = [] self.data_fwd_2nd_reading = [] ### ADDED self.data_bwd = [] self.avg_fwd = [] self.avg_fwd_2nd_reading = [] ### ADDED self.avg_bwd = [] self.fit_popup = None self.p0_fwd = None self.p0_bwd = None self.x_fwd = self._generate_x_axis() if self.sweep_type != 'sawtooth': self.x_bwd = self._generate_x_axis(backward=True) else: self.x_bwd = self._generate_x_axis() self.fast = fast # Configure list of experiments self.widgets['config'].setText(config) self.exp_path = self.config['exp_path'] model = QtWidgets.QFileSystemModel() model.setRootPath(self.exp_path) model.setNameFilterDisables(False) model.setNameFilters(['*.py']) self.widgets['exp'].setModel(model) self.widgets['exp'].setRootIndex(model.index(self.exp_path)) self.widgets['exp'].hideColumn(1) self.widgets['exp'].hideColumn(2) self.widgets['exp'].hideColumn(3) self.widgets['exp'].clicked.connect(self.display_experiment) # Configure list of clients self.clients = clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=self.clients, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) if (client == None): client_name_concat = client_type client_item = QtWidgets.QListWidgetItem(client_name_concat) client_item.setForeground(Qt.gray) client_item.setToolTip(str("Disconnected")) self.widgets['clients'].addItem(client_item) else: self.log.info(client) client_name_concat = f"{client_type}_{client_config}" client_item = QtWidgets.QListWidgetItem(client_name_concat) client_item.setToolTip(str(client)) self.widgets['clients'].addItem(client_item) #Checking for any missing clients, and adding them as greyed out on the list of clients # Manually add logger to client self.clients['logger'] = logger # Configure button self.widgets['configure'].clicked.connect(self.configure_experiment) self.widgets['run'].clicked.connect(self.run_pressed) self.widgets['autosave'].toggled.connect(self._update_autosave) self.widgets['save'].pressed.connect( lambda: self.save(filename=self.widgets['save_name'].text(), directory=self.config['save_path'])) self.widgets['reps'].valueChanged.connect(self.set_reps) self.widgets['avg'][0].clicked.connect( lambda: self._clear_show_trace(0)) self.widgets['avg'][1].clicked.connect( lambda: self._clear_show_trace(1)) self.gui.fit.clicked.connect(self.fit_config) # Create legends self.widgets['curve'] = [] for index in range(len(self.widgets['graph'])): self.widgets['legend'][index] = get_legend_from_graphics_view( self.widgets['legend'][index]) # Configure hmaps self.widgets['hmap'] = [] for index in range(2): # Configure Hmap to work the way we want hmap = pg.ImageView(view=pg.PlotItem()) self.gui.graph_layout.insertWidget(2 * index + 1, hmap) hmap.setPredefinedGradient('inferno') hmap.show() hmap.view.setAspectLocked(False) hmap.view.invertY(False) self.widgets['hmap'].append(hmap) # Setup stylesheet. self.gui.apply_stylesheet() def display_experiment(self, index): """ Displays the currently clicked experiment in the text browser :param index: (QModelIndex) index of QTreeView for selected file """ filepath = self.widgets['exp'].model().filePath(index) if not os.path.isdir(filepath): with open(filepath, 'r') as exp_file: exp_content = exp_file.read() self.widgets['exp_preview'].setText(exp_content) self.widgets['exp_preview'].setStyleSheet( 'font: 12pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: #19232D;') self.cur_path = self.widgets['exp'].model().filePath( self.widgets['exp'].currentIndex()) self.exp_name = os.path.split(os.path.basename(self.cur_path))[0] def configure_experiment(self): """ Configures experiment to be the currently selected item """ spec = importlib.util.spec_from_file_location(self.exp_name, self.cur_path) self.module = importlib.util.module_from_spec(spec) spec.loader.exec_module(self.module) self.experiment = self.module.experiment self.min = self.widgets['p_min'].value() self.max = self.widgets['p_max'].value() self.pts = self.widgets['pts'].value() self.x_fwd = self._generate_x_axis() if self.sweep_type != 'sawtooth': self.x_bwd = self._generate_x_axis(backward=True) else: self.x_bwd = self._generate_x_axis() # Run any pre-experiment configuration try: self.module.configure(self) except AttributeError: pass self.log.info(f'Experiment {self.exp_name} configured') self.widgets['exp_preview'].setStyleSheet( 'font: 12pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(50, 50, 50);') def run_pressed(self): """ Handles button pressing for run and stop """ if self.widgets['run'].text() == 'Run': self.widgets['run'].setStyleSheet('background-color: red') self.widgets['run'].setText('Stop') self.log.info('Sweep experiment started') self.widgets['rep_tracker'].setValue(1) # set min and max self.min = self.widgets['p_min'].value() self.max = self.widgets['p_max'].value() self.pts = self.widgets['pts'].value() self.run() self.widgets['rep_tracker'].setValue(0) self.widgets['reps'].setValue(0) self.widgets['run'].setStyleSheet('background-color: green') self.widgets['run'].setText('Run') for button in self.widgets['avg']: button.setText('Avg only') self.log.info('Sweep experiment stopped') else: self.widgets['rep_tracker'].setValue(0) self.widgets['reps'].setValue(0) for button in self.widgets['avg']: button.setText('Avg only') self.widgets['run'].setStyleSheet('background-color: green') self.widgets['run'].setText('Run') self.stop() self.log.info('Sweep experiment stopped') def save(self, filename=None, directory=None, date_dir=True): """ Saves the dataset :param filename: (str) name of file identifier :param directory: (str) filepath to save to :param date_dir: (bool) whether or not to store in date-specific sub-directory """ if filename is None: filename = self.widgets['save_name'].text() if directory is None: directory = self.config['save_path'] # Save heatmap generic_save(data=fill_2dlist(self.data_fwd), filename=f'{filename}_fwd_scans', directory=directory, date_dir=date_dir) pyqtgraph_save(widget=self.widgets['hmap'][0].getView(), filename=f'{filename}_fwd_scans', directory=directory, date_dir=date_dir) # Save average generic_save(data=np.vstack((self.x_fwd, np.array([self.avg_fwd]))), filename=f'{filename}_fwd_avg', directory=directory, date_dir=date_dir) pyqtgraph_save(widget=self.widgets['graph'][0].getPlotItem(), filename=f'{filename}_fwd_avg', directory=directory, date_dir=date_dir) if self.sweep_type != 'sawtooth': # Save heatmap generic_save(data=fill_2dlist(self.data_bwd), filename=f'{filename}_bwd_scans', directory=directory, date_dir=date_dir) pyqtgraph_save(widget=self.widgets['hmap'][1].getView(), filename=f'{filename}_bwd_scans', directory=directory, date_dir=date_dir) # Save average generic_save(data=np.vstack( (self.x_bwd, np.array([self.avg_bwd]))), filename=f'{filename}_bwd_avg', directory=directory, date_dir=date_dir) pyqtgraph_save(widget=self.widgets['graph'][1].getPlotItem(), filename=f'{filename}_bwd_avg', directory=directory, date_dir=date_dir) '''else: if len(self.data_bwd) > 0: # Save heatmap generic_save( data=fill_2dlist(self.data_bwd), filename=f'{filename}_bwd_scans', directory=directory, date_dir=date_dir ) pyqtgraph_save( widget=self.widgets['hmap'][1].getView(), filename=f'{filename}_bwd_scans', directory=directory, date_dir=date_dir ) # Save average generic_save( data = np.vstack((self.x_bwd, np.array([self.avg_bwd]))), filename=f'{filename}_bwd_avg', directory=directory, date_dir=date_dir ) pyqtgraph_save( widget=self.widgets['graph'][1].getPlotItem(), filename=f'{filename}_bwd_avg', directory=directory, date_dir=date_dir )''' def fit_config(self, status: bool): """ Configures fitting add-on :param status: (bool) whether or not fit button is checked """ # If box is newly checked, instantiate popup if status: self.fit_popup = FitPopup(ui='fit_popup', x_fwd=self.x_fwd, data_fwd=self.avg_fwd, x_bwd=self.x_bwd, data_bwd=self.avg_bwd, p0_fwd=None, p0_bwd=None, config=self.config, log=self.log) self.fit_popup.model_type.activated.connect( self.fit_popup.fit_selection) ''' if len(self.avg_fwd) != 0: self.fit_popup = FitPopup(ui='fit_popup', data = self.avg_fwd, log=self.log) self.fit_popup.model_type.activated.connect(self.fit_popup.fit_selection) else: self.fit_error = Popup(ui = 'fit_error') ''' # If box isn't checked, remove popup else: self.fit_popup = None def _configure_plots(self, plot=True): """ Configures the plots """ # Clear plots if len(self.widgets['curve']) > 0: # self.widgets['curve'][0].clear() # self.widgets['curve'][1].clear() self.widgets['graph'][0].getPlotItem().clear() self.widgets['graph'][1].getPlotItem().clear() self.widgets['hmap'][0].clear() self.widgets['hmap'][1].clear() self.widgets['legend'][0].clear() self.widgets['legend'][1].clear() self.widgets['curve_avg'][0].clear() self.widgets['curve_avg'][1].clear() self.widgets['fit_avg'][0].clear() self.widgets['fit_avg'][1].clear() self.data_fwd = [] self.data_bwd = [] self.avg_fwd = [] self.avg_bwd = [] self.fit_fwd = [] self.fit_bwd = [] self.p0_fwd = None self.p0_bwd = None self.x_fwd = self._generate_x_axis() if self.sweep_type != 'sawtooth': self.x_bwd = self._generate_x_axis(backward=True) else: self.x_bwd = self._generate_x_axis() self.widgets['curve'] = [] self.widgets['curve_avg'] = [] self.widgets['fit_avg'] = [] for index, graph in enumerate(self.widgets['graph']): if self.sweep_type != 'sawtooth': self.widgets['curve'].append( graph.plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[6]))) add_to_legend(self.widgets['legend'][index], self.widgets['curve'][index], f'{"Fwd" if index==0 else "Bwd"} trace') self.widgets['curve_avg'].append( graph.plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(self.widgets['legend'][index], self.widgets['curve_avg'][index], f'{"Fwd" if index==0 else "Bwd"} avg') self.widgets['fit_avg'].append( graph.plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(self.widgets['legend'][index], self.widgets['fit_avg'][index], f'{"Fwd" if index==0 else "Bwd"} fit avg') else: self.widgets['curve'].append( graph.plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[6]))) add_to_legend(self.widgets['legend'][index], self.widgets['curve'][index], f'{"1st" if index==0 else "2nd"} trace') self.widgets['curve_avg'].append( graph.plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(self.widgets['legend'][index], self.widgets['curve_avg'][index], f'{"1st" if index==0 else "2nd"} avg') self.widgets['fit_avg'].append( graph.plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(self.widgets['legend'][index], self.widgets['fit_avg'][index], f'{"1st" if index==0 else "2nd"} fit avg') for hmap in self.widgets['hmap']: hmap.view.setLimits(xMin=self.min, xMax=self.max) def _reset_plots(self): """ Resets things after a rep """ self.data_bwd.append([]) self.data_fwd.append([]) def _run_and_plot(self, x_value, backward=False): if self.sweep_type != 'sawtooth': if backward: # Single trace self.data_bwd[-1].append( self.experiment(x_value, self, gui=self.gui)) cur_ind = len(self.data_bwd[-1]) self.widgets['curve'][1].setData(self.x_bwd[:cur_ind], self.data_bwd[-1]) # Update average and plot try: cur_rep = len(self.data_bwd) self.avg_bwd[cur_ind - 1] = ( (cur_rep - 1) * self.avg_bwd[cur_ind - 1] + self.data_bwd[-1][-1]) / cur_rep self.widgets['curve_avg'][1].setData( self.x_bwd, self.avg_bwd) # If it is the first run, just add the data except IndexError: self.avg_bwd.append(self.data_bwd[-1][-1]) # Heat map if not self.fast: self.widgets['hmap'][1].setImage( img=np.transpose(np.fliplr(fill_2dlist( self.data_bwd))), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) else: self.data_fwd[-1].append( self.experiment(x_value, self, gui=self.gui)) cur_ind = len(self.data_fwd[-1]) self.widgets['curve'][0].setData(self.x_fwd[:cur_ind], self.data_fwd[-1]) # Update average and plot try: cur_rep = len(self.data_fwd) self.avg_fwd[cur_ind - 1] = ( (cur_rep - 1) * self.avg_fwd[cur_ind - 1] + self.data_fwd[-1][-1]) / cur_rep self.widgets['curve_avg'][0].setData( self.x_fwd, self.avg_fwd) # If it is the first run, just add the data except IndexError: self.avg_fwd.append(self.data_fwd[-1][-1]) # Heat map if not self.fast: self.widgets['hmap'][0].setImage( img=np.transpose(fill_2dlist(self.data_fwd)), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) else: reading = self.experiment(x_value, self, gui=self.gui) try: n_readings = len(reading) self.data_fwd[-1].append(reading[0]) cur_ind = len(self.data_fwd[-1]) self.widgets['curve'][0].setData(self.x_fwd[:cur_ind], self.data_fwd[-1]) # Update average and plot try: cur_rep = len(self.data_fwd) self.avg_fwd[cur_ind - 1] = ( (cur_rep - 1) * self.avg_fwd[cur_ind - 1] + self.data_fwd[-1][-1]) / cur_rep self.widgets['curve_avg'][0].setData( self.x_fwd, self.avg_fwd) # If it is the first run, just add the data except IndexError: self.avg_fwd.append(self.data_fwd[-1][-1]) # Heat map if not self.fast: self.widgets['hmap'][0].setImage( img=np.transpose(fill_2dlist(self.data_fwd)), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) self.widgets['hmap'][1].setImage( img=np.transpose(fill_2dlist(self.data_bwd)), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) self.data_bwd[-1].append(reading[1]) cur_ind = len(self.data_bwd[-1]) self.widgets['curve'][1].setData(self.x_bwd[:cur_ind], self.data_bwd[-1]) # Update average and plot try: cur_rep = len(self.data_bwd) self.avg_bwd[cur_ind - 1] = ( (cur_rep - 1) * self.avg_bwd[cur_ind - 1] + self.data_bwd[-1][-1]) / cur_rep self.widgets['curve_avg'][1].setData( self.x_bwd, self.avg_bwd) # If it is the first run, just add the data except IndexError: self.avg_bwd.append(self.data_bwd[-1][-1]) # Heat map if not self.fast: self.widgets['hmap'][1].setImage( img=np.transpose(np.fliplr(fill_2dlist( self.data_bwd))), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) except TypeError: self.data_fwd[-1].append(reading) cur_ind = len(self.data_fwd[-1]) self.widgets['curve'][0].setData(self.x_fwd[:cur_ind], self.data_fwd[-1]) # Update average and plot try: cur_rep = len(self.data_fwd) self.avg_fwd[cur_ind - 1] = ( (cur_rep - 1) * self.avg_fwd[cur_ind - 1] + self.data_fwd[-1][-1]) / cur_rep self.widgets['curve_avg'][0].setData( self.x_fwd, self.avg_fwd) # If it is the first run, just add the data except IndexError: self.avg_fwd.append(self.data_fwd[-1][-1]) # Heat map if not self.fast: self.widgets['hmap'][0].setImage( img=np.transpose(fill_2dlist(self.data_fwd)), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) self.gui.force_update() def _update_hmaps(self, reps_done): """ Updates hmap if in fast mode """ if self.fast: if self.sweep_type == 'triangle': self.widgets['hmap'][1].setImage( img=np.transpose(np.fliplr(fill_2dlist(self.data_bwd))), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) self.widgets['hmap'][0].setImage( img=np.transpose(fill_2dlist(self.data_fwd)), pos=(self.min, 0), scale=((self.max - self.min) / self.pts, 1), autoRange=False) def _update_integrated(self, reps_done): """ Update repetition counter """ self.widgets['rep_tracker'].setValue(reps_done + 1) self._update_fits() def _update_fits(self): """ Updates fits """ if len(self.avg_fwd) != 0 and self.fit_popup is not None: if self.fit_popup.mod is not None and\ self.fit_popup.mod.init_params is not None: self.fit_popup.data_fwd = np.array(self.avg_fwd) self.fit_popup.data_bwd = np.array(self.avg_bwd) if self.p0_fwd is not None and self.p0_bwd is not None: self.fit_popup.p0_fwd = self.p0_fwd self.fit_popup.p0_bwd = self.p0_bwd #method = getattr(self.fit_popup, self.fit_popup.fit_method) self.fit_fwd, self.fit_bwd, self.p0_fwd, self.p0_bwd = self.fit_popup.fit_mod( ) if self.fit_popup.fit_suc: self.widgets['fit_avg'][0].setData(self.x_fwd, self.fit_fwd) self.widgets['fit_avg'][1].setData(self.x_bwd, self.fit_bwd) #print(self.avg_fwd) else: pass #print(self.config) def _update_autosave(self): """ Updates autosave status """ self.autosave = self.widgets['autosave'].isChecked() def _clear_show_trace(self, index): """ Clears or shows the single scan trace of a graph :param index: (int) index of graph """ # Check status of button try: if self.widgets['avg'][index].text() == 'Avg only': self.widgets['avg'][index].setText('Show trace') self.widgets['graph'][index].removeItem( self.widgets['curve'][index]) else: self.widgets['avg'][index].setText('Avg only') self.widgets['graph'][index].addItem( self.widgets['curve'][index]) except KeyError: pass
class WlmMonitor: """ A script class for monitoring and locking lasers based on the wavemeter """ def __init__(self, wlm_client, logger_client, gui='wavemeter_monitor', ao_clients=None, display_pts=5000, threshold=0.0002, port=None, params=None, three_lasers=False): """ Instantiates WlmMonitor script object for monitoring wavemeter :param wlm_client: (obj) instance of wavemeter client :param gui_client: (obj) instance of GUI client. :param logger_client: (obj) instance of logger client. :param ao_clients: (dict, optional) dictionary of ao client objects with keys to identify. Exmaple: {'ni_usb_1': nidaqmx_usb_client_1, 'ni_usb_2': nidaqmx_usb_client_2, 'ni_pxi_multi': nidaqmx_pxi_client} :param display_pts: (int, optional) number of points to display on plot :param threshold: (float, optional) threshold in THz for lock error signal :param port: (int) port number for update server :param params: (dict) see set_parameters below for details :three_lasers: (bool) If three lasers are in use (instead of 2) """ self.channels = [] if three_lasers: gui = 'wavemeter_monitor_3lasers' self.wlm_client = wlm_client self.ao_clients = ao_clients self.display_pts = display_pts self.threshold = threshold self.log = LogHandler(logger_client) # Instantiate gui self.gui = Window( gui_template=gui, host=get_ip(), port=port, ) # Setup stylesheet. self.gui.apply_stylesheet() if three_lasers: self.widgets = get_gui_widgets(gui=self.gui, freq=3, sp=3, rs=3, lock=3, error_status=3, graph=6, legend=6, clear=6, zero=6, voltage=3, error=3) else: self.widgets = get_gui_widgets(gui=self.gui, freq=2, sp=2, rs=2, lock=2, error_status=2, graph=4, legend=4, clear=4, zero=4, voltage=2, error=2) # Set parameters self.set_parameters(**params) # Configure plots # Get actual legend widgets self.widgets['legend'] = [ get_legend_from_graphics_view(legend) for legend in self.widgets['legend'] ] self.widgets['curve'] = [] self.initialize_channels() for channel in self.channels: self.update_parameters( dict(channel=channel.number, setpoint=channel.data[-1])) def set_parameters(self, channel_params): """ Instantiates new channel objects with given parameters and assigns them to the WlmMonitor Note: parameters for a channel that has already been assigned can be set or updated later using the update_parameters() method via an update client in a separate python process. :param channel_params: (list) of dictionaries containing all parameters. Example of full parameter set: {'channel': 1, 'name': 'Velocity', 'setpoint': 406.7, 'lock':True, 'memory': 20, 'pid': {'p': 1, 'i': 0.1, 'd': 0}, 'ao': {'client':'nidaqmx_client', 'channel': 'ao1'}} In more detail: - 'channel': should be from 1-8 for the High-Finesse Wavemeter (with switch) and should ALWAYS be provided, as a reference so that we know which channel to assign all the other parameters to - 'name': a string that can just be provided once and is used as a user-friendly name for the channel. Initializes to 'Channel X' where X is a random integer if not provided - 'setpoint': setpoint for this channel. - 'lock': boolean that tells us whether or not to turn on the lock. Ignored if setpoint is None. Default is False. - 'memory': Number of points for integral memory of pid (history of the integral). Default is 20. - 'pid': dict containing pid parameters. Uses the pylabnet.scripts.pid module. By default instantiates the default PID() object. - 'ao': dict containing two elements: 'client' which is a string that is the name of the ao client to use for locking. This should match up with a key in self.ao_clients. 'channel'is an identifier for which analog output to use for this channel. By default it is set to None and no active locking is performed """ # Check if it is only a single channel if type(channel_params) is dict: channel_params = [channel_params] # Initialize each channel individually for channel_param_set in channel_params: self.channels.append( Channel(channel_param_set, self.ao_clients, log=self.log)) def update_parameters(self, parameters): """ Updates only the parameters given. Can be used in the middle of the script operation via an update client. :param parameters: (list) list of dictionaries, see set_parameters() for details """ if not isinstance(parameters, list): parameters = [parameters] for parameter in parameters: # Make sure a channel is given if 'channel' in parameter: # Check if it is a channel that is already logged channel_list = self._get_channels() if parameter['channel'] in channel_list: # Find index of the desired channel index = channel_list.index(parameter['channel']) channel = self.channels[channel_list.index( parameter['channel'])] # Set all other parameters for this channel if 'name' in parameter: channel.name = parameter['name'] if 'setpoint' in parameter: # self.widgets['sp'][index].setValue(parameter['setpoint']) # channel.setpoint = parameter['setpoint'] # Mark that we should override GUI setpoint, since it has been updated by the script channel.setpoint_override = parameter['setpoint'] if 'lock' in parameter: self.widgets['lock'][index].setChecked( parameter['lock']) # channel.lock = parameter['lock'] # Mark that we should override the GUI lock since it has been updated by the script # channel.lock_override = True if 'memory' in parameter: channel.memory = parameter['memory'] if 'pid' in parameter: channel.pid.set_parameters( p=parameter['pid']['p'], i=parameter['pid']['i'], d=parameter['pid']['d'], ) # Ignore ao requests if clients have not been assigned if 'ao' in parameter and self.ao_clients is not None: # Convert ao from string to object using lookup try: channel.ao = { 'client': self.ao_clients[parameter['ao']['client']], 'channel': parameter['ao']['channel'] } # Handle case where the ao client does not exist except KeyError: channel.ao = None # Otherwise, it is a new channel so we should add it else: self.channels.append(Channel(parameter)) self._initialize_channel(index=len(self.channels) - 1, channel=self.channels[-1]) def initialize_channels(self): """Initializes all channels and outputs to the GUI""" for index, channel in enumerate(self.channels): self._initialize_channel(index, channel) def clear_channel(self, channel): """ Clears the plot output for this channel :param channel: Channel object to clear """ try: channel.initialize(channel.data[-1]) # If the channel isn't monitored except: self.log.warn('Could not clear channel') def clear_all(self): """ Clears all channels """ for channel in self.channels: self.clear_channel(channel) def run(self): """Runs the WlmMonitor Can be stopped using the pause() method """ self._get_gui_data() self._update_channels() self.gui.force_update() def zero_voltage(self, channel): """ Zeros the output voltage for this channel :param channel: Channel object to zero voltage of """ try: channel.zero_voltage() self.log.info(f'Voltage centered for channel {channel.name}') # If the channel isn't monitored except: self.log.warn('Failed to zero voltage') def go_to(self, channel, value, step_size, hold_time): """ Sends laser to a setpoint value gradually :param channel: (int) channel number on wavemeter :param value: (float) value to set laser frequency to :param step_size: (float) step size in THz for laser freq steps :param hold_time: (float) time in seconds to wait between steps """ # Index of channel physical_channel = self.channels[self._get_channels().index(channel)] # Generate array of points to go to traverse = np.linspace( physical_channel.setpoint, value, int((value - physical_channel.setpoint) / step_size)) for frequency in traverse: self.set_parameters([dict(channel=channel, setpoint=frequency)]) time.sleep(hold_time) # Technical methods def _initialize_channel(self, index, channel): """Initializes a channel and outputs to the GUI Should only be called in the beginning of channel use to assign physical GUI widgets """ # Get wavelength and initialize data arrays channel.initialize(wavelength=self.wlm_client.get_wavelength( channel.number), display_pts=self.display_pts) # Create curves # frequency self.widgets['curve'].append(self.widgets['graph'][2 * index].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(legend=self.widgets['legend'][2 * index], curve=self.widgets['curve'][4 * index], curve_name=channel.curve_name) # Setpoint self.widgets['curve'].append(self.widgets['graph'][2 * index].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(legend=self.widgets['legend'][2 * index], curve=self.widgets['curve'][4 * index + 1], curve_name=channel.setpoint_name) # Clear data self.widgets['clear'][2 * index].clicked.connect( lambda: self.clear_channel(channel)) self.widgets['clear'][2 * index + 1].clicked.connect( lambda: self.clear_channel(channel)) # Setpoint reset self.widgets['rs'][index].clicked.connect( lambda: self.update_parameters( dict(channel=channel.number, setpoint=channel.data[-1]))) # Voltage self.widgets['curve'].append(self.widgets['graph'][2 * index + 1].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(legend=self.widgets['legend'][2 * index + 1], curve=self.widgets['curve'][4 * index + 2], curve_name=channel.voltage_curve) # Error self.widgets['curve'].append(self.widgets['graph'][2 * index + 1].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(legend=self.widgets['legend'][2 * index + 1], curve=self.widgets['curve'][4 * index + 3], curve_name=channel.error_curve) # zero self.widgets['zero'][2 * index].clicked.connect( lambda: self.zero_voltage(channel)) self.widgets['zero'][2 * index + 1].clicked.connect( lambda: self.zero_voltage(channel)) def _update_channels(self): """ Updates all channels + displays Called continuously inside run() method to refresh WLM data and output on GUI """ for index, channel in enumerate(self.channels): # Check for override if channel.setpoint_override: self.widgets['sp'][index].setValue(channel.setpoint_override) channel.setpoint_override = 0 # Update data with the new wavelength channel.update(self.wlm_client.get_wavelength(channel.number)) # Update frequency self.widgets['curve'][4 * index].setData(channel.data) self.widgets['freq'][index].setValue(channel.data[-1]) # Update setpoints self.widgets['curve'][4 * index + 1].setData(channel.sp_data) # Update the setpoint to GUI directly if it has been changed # if channel.setpoint_override: # # Tell GUI to pull data provided by script and overwrite direct GUI input # self.widgets['sp'][index].setValue(channel.setpoint) # If the lock has been updated, override the GUI # if channel.lock_override: # self.widgets['lock'][index].setChecked(channel.lock) # Set the error boolean (true if the lock is active and we are outside the error threshold) if channel.lock and np.abs(channel.data[-1] - channel.setpoint) > self.threshold: self.widgets['error_status'][index].setChecked(True) else: self.widgets['error_status'][index].setChecked(False) # Now update lock + voltage plots self.widgets['curve'][4 * index + 2].setData(channel.voltage) self.widgets['voltage'][index].setValue(channel.voltage[-1]) self.widgets['curve'][4 * index + 3].setData(channel.error) self.widgets['error'][index].setValue(channel.error[-1]) def _get_gui_data(self): """ Updates setpoint and lock parameters with data pulled from GUI Does not overwrite the script setpoints and locks, but stores the GUI values for comparison based on context. See Channel.update() method for behavior on how script chooses whether to use internal values or GUI values """ for index, channel in enumerate(self.channels): # Pull the current value from the GUI channel.gui_setpoint = self.widgets['sp'][index].value() channel.gui_lock = self.widgets['lock'][index].isChecked() def _get_channels(self): """ Returns all active channel numbers Usually used for checking whether a newly input channel has already been assigned to the script :return: (list) all active channel numbers """ channel_list = [] for channel in self.channels: channel_list.append(channel.number) return channel_list def get_wavelength(self, channel): # Index of channel physical_channel = self.channels[self._get_channels().index(channel)] return self.wlm_client.get_wavelength(physical_channel.number)
class TimeTraceGui(TimeTrace): """ Same as TimeTrace but with a dedicated GUI for display and parameter setting""" STYLESHEET = 'color: rgb(255, 255, 255); font: 25 12pt "Calibri Light";' def __init__(self, ctr: si_tt.Client, log: LogClient, config, ui='histogram', **kwargs): """ Instantiates TimeTrace measurement :param ctr: (si_tt.Client) client to timetagger hardware :param log: (LogClient) instance of logclient for logging :param config: (str) name of config file :param ui: (str) name of ui file :param **kwargs: additional keyword arguments TODO: in future, can implement multiple histograms if useful """ # Setup GUI self.gui = Window( gui_template='histogram', host=get_ip() ) # Setup stylesheet. self.gui.apply_stylesheet() # Store config self.config = config self.correlation = False if 'type' in self.config: if self.config['type'] == 'correlation': self.correlation = True super().__init__( ctr=ctr, log=log, click_ch=self.config['click_ch'], start_ch=self.config['start_ch'], binwidth=int(self._get_binwidth()), n_bins=self.gui.n_bins.value(), update_interval=0, correlation=self.correlation ) if not type(self.config['click_ch']) == int: combined_click_channel = f"{self.config['click_ch'][0]}+{self.config['click_ch'][1]}" ctr.create_combined_channel( channel_name=combined_click_channel, channel_list=self.config['click_ch'] ) self.config['click_ch'] = [combined_click_channel] self.gates = {} if 'gate_ch' in self.config: # Handle singular input if not isinstance(self.config['gate_ch'], list): self.config['gate_ch'] = [self.config['gate_ch']] # Update GUI to handle gates self._configure_gui_gates() # Setup gated channels for gate_ch in self.config['gate_ch']: ch_name = f'Gated histogram channel {gate_ch}' ctr.create_gated_channel( ch_name, self.config['click_ch'], gate_ch, delay=self.delays[ch_name].value() ) self.gates[ch_name] = TimeTrace( ctr=ctr, log=log, click_ch=ch_name, start_ch=self.config['start_ch'], binwidth=int(self._get_binwidth()), n_bins=self.gui.n_bins.value(), update_interval=0, correlation=self.correlation ) # Configure clicks self.gui.configure.clicked.connect(lambda: self.update_parameters( binwidth=int(self._get_binwidth()), n_bins=self.gui.n_bins.value() )) self.gui.clear.clicked.connect(self.clear_all) # Need Lambda to force it to use default args # https://stackoverflow.com/questions/60001583/pyqt5-slot-function-does-not-take-default-argument self.gui.save.clicked.connect(lambda: self.save()) self.gui.run.clicked.connect(self.run) #### CHANGED CHANGED CHANGED self.gui.fit.clicked.connect(self.fit_config) self._configure_delay_updates() # Configure window length preview self.gui.n_bins.valueChanged.connect(self.update_window_length_label) self.gui.binwidth.valueChanged.connect(self.update_window_length_label) # Configure window length preview self.update_window_length_label() # Initialize plot info self.curve = self.gui.graph.plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]) ) self.gui.graph.getPlotItem().setLabel('bottom', 'Time (s)') self.legend = get_legend_from_graphics_view(self.gui.legend) add_to_legend(self.legend, self.curve, 'Histogram') self.gate_curves = {} index = 1 for gate in self.gates: self.gate_curves[gate] = self.gui.graph.plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[index]) ) index += 1 add_to_legend(self.legend, self.gate_curves[gate], gate) self.gui.apply_stylesheet() self.fitting = False def clear_all(self): """ Clears all plots """ self.clear() for gate in self.gates.values(): gate.clear() def update_window_length_label(self): """ Update label previewing the total window length""" binwidth = int(self._get_binwidth()), n_bins = self.gui.n_bins.value() window_length = binwidth[0] * n_bins # in ps self.gui.window_length.setText(f'{window_length/1000} ns') def update_parameters(self, binwidth, n_bins): """ Updates parameters of all histograms :param binwidth: (float) binwidth in ps :param n_Bins: (int) total number of bins """ self.set_parameters(binwidth, n_bins) for gate in self.gates.values(): gate.set_parameters(binwidth, n_bins) def fit_config(self, status: bool): """ Configures fitting add-on :param status: (bool) whether or not fit button is checked """ # If box is newly checked, instantiate popup if status: self.fit_popup = FitPopup(ui='fit_popup_hist', x=np.array(range(self.gui.n_bins.value())) * int(self._get_binwidth()) / 1e12, #dummy values, not actually used None, #self.ctr.get_x_axis(self.hist)/1e12, data=np.zeros(self.gui.n_bins.value()), #dummy values, not actually used #None, #self.ctr.get_counts(self.hist)[0], p0=None, config=self.config, log=self.log) self.fit_popup.model_type.activated.connect(self.fit_popup.fit_selection) self.fit_curve = self.gui.graph.plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[5]) ) add_to_legend(self.legend, self.fit_curve, 'Histogram Fit') self.fitting = True self.p0 = None # If box isn't checked, remove popup else: self.fit_popup = None self.fitting = False def run(self): """ Handles run button click """ if self.gui.run.text() == 'Run': self.gui.run.setText('Stop') self.gui.run.setStyleSheet('background-color: red') self.log.info('Running histogram') self.go() else: self.gui.run.setText('Run') self.gui.run.setStyleSheet('background-color: green') self.log.info('Stopped histogram') if self.gui.autosave.isChecked(): self.save() self.pause() for gate in self.gates.values(): gate.pause() def go(self): """ Runs counter from scratch """ self.start_acquisition() for gate in self.gates.values(): gate.start_acquisition() self.init_plot() self.is_paused = False last_save = time.time() last_clear = last_save while not self.is_paused: if self.gui.autosave.isChecked(): current_time = time.time() if current_time - last_save > self.gui.save_time.value(): self.save() last_save = current_time if self.gui.auto_clear.isChecked(): current_time = time.time() if current_time - last_clear > self.gui.clear_time.value(): self.clear_all() last_clear = current_time self._update_data() self.gui.force_update() def init_plot(self): """ Initializes the plot """ # Clear existing data self.curve.clear() self.curve.setData( self.ctr.get_x_axis(self.hist) / 1e12, self.ctr.get_counts(self.hist)[0] ) if self.fitting: self.fit_curve.clear() self._update_fit() for gate_name, gate_curve in self.gate_curves.items(): gate_curve.clear() gate_curve.setData( self.gates[gate_name].ctr.get_x_axis(self.gates[gate_name].hist) / 1e12, self.gates[gate_name].ctr.get_counts(self.gates[gate_name].hist)[0] ) def _update_data(self): """ Adds latest data to the plot """ self.curve.setData( self.ctr.get_x_axis(self.hist) / 1e12, self.ctr.get_counts(self.hist)[0] ) if self.fitting: self._update_fit() for gate_name, gate_curve in self.gate_curves.items(): gate_curve.setData( self.gates[gate_name].ctr.get_x_axis(self.gates[gate_name].hist) / 1e12, self.gates[gate_name].ctr.get_counts(self.gates[gate_name].hist)[0] ) def _update_fit(self): """ Updates fits """ if self.fit_popup.mod is not None and self.fit_popup.mod.init_params is not None: self.fit_popup.data = np.array(self.ctr.get_counts(self.hist)[0]) self.fit_popup.x = np.array(self.ctr.get_x_axis(self.hist) / 1e12) if self.p0 is not None: self.fit_popup.p0 = self.p0 self.fit, self.p0 = self.fit_popup.fit_mod() if self.fit_popup.fit_suc: self.fit_curve.setData( self.ctr.get_x_axis(self.hist) / 1e12, self.fit ) def _get_binwidth(self): """ Gets the binwidth using the unit combo box :return: (float) binwidth in ps """ val = self.gui.binwidth.value() unit_index = self.gui.units.currentIndex() if unit_index == 0: return val elif unit_index == 1: return val * 1e3 elif unit_index == 2: return val * 1e6 elif unit_index == 3: return val * 1e9 def _configure_gui_gates(self): """ Configures the gates part of the GUI """ # Configure base layout for all gates gate_box = QtWidgets.QGroupBox("Gates") gate_box.setStyleSheet(self.STYLESHEET) vbox = QtWidgets.QVBoxLayout() # Now add widgets for each gate self.delays = {} for index, gate_ch in enumerate(self.config['gate_ch']): # Configure widgets ch_name = f'Gated histogram channel {gate_ch}' hbox = QtWidgets.QHBoxLayout() hbox.addWidget(QtWidgets.QLabel(text=f'Gate {gate_ch}')) hbox.addWidget(QtWidgets.QLabel(text='Delay: ')) self.delays[ch_name] = QtWidgets.QDoubleSpinBox() self.delays[ch_name].setMaximum(1e12) self.delays[ch_name].setSuffix(' ps') self.delays[ch_name].setButtonSymbols(2) hbox.addWidget(self.delays[ch_name]) # Check for preconfigured delay and set the value if 'delays' in self.config: try: self.delays[ch_name].setValue(self.config['delays'][index]) except IndexError: pass # Add to vertical layout vbox.addLayout(hbox) # Configure layout to a group box and add to GUI in layout gate_box.setLayout(vbox) self.gui.graph_layout.addWidget(gate_box) def _configure_delay_updates(self): """ Configures delay updates when a value is changed """ for channel_name, delay in self.delays.items(): delay.valueChanged.connect(lambda state, x=channel_name: self.gates[channel_name].ctr.update_delay( channel_name=x, delay=state )) def save(self, filename=None, directory=None): """ Saves the current data """ if filename is None: filename = self.gui.save_name.text() if directory is None: directory = self.config['save_path'] generic_save( data=np.array([ self.ctr.get_x_axis(self.hist) / 1e12, self.ctr.get_counts(self.hist)[0] ]), filename=filename, directory=directory, date_dir=True ) for gate_name, gate in self.gates.items(): generic_save( data=np.array([ gate.ctr.get_x_axis(gate.hist) / 1e12, gate.ctr.get_counts(gate.hist)[0] ]), filename=gate_name, directory=directory, date_dir=True ) pyqtgraph_save( widget=self.gui.graph.getPlotItem(), filename=filename, directory=directory, date_dir=True ) self.log.info('Saved histogram data')
def __init__(self, logger=None, channels=['Channel 1'], clients={}, config=None, fast=True): """ Instantiates controller (only 1D data supported so far) :param logger: instance of LogClient :param channels: (list) list of channel names :param config: (str) name of config file :param fast: (bool) whether to operate in fast mode fast mode only updates heat maps at the end of each scan (this speeds things up) """ self.config = load_script_config('scan1d', config, logger=logger) if 'sweep_type' in self.config.keys(): self.sweep_type = self.config['sweep_type'] else: self.sweep_type = 'triangle' super().__init__(logger, channels, sweep_type=self.sweep_type) self.module = None # Instantiate GUI self.gui = Window(gui_template='scan_1d', host=get_ip(), max=True) self.widgets = get_gui_widgets(self.gui, p_min=1, p_max=1, pts=1, config=1, graph=2, legend=2, clients=1, exp=1, exp_preview=1, configure=1, run=1, autosave=1, save_name=1, save=1, reps=1, rep_tracker=1, avg=2) # Configure default parameters self.min = self.widgets['p_min'].value() self.max = self.widgets['p_max'].value() self.pts = self.widgets['pts'].value() self.reps = self.widgets['reps'].value() self.data_fwd = [] self.data_fwd_2nd_reading = [] ### ADDED self.data_bwd = [] self.avg_fwd = [] self.avg_fwd_2nd_reading = [] ### ADDED self.avg_bwd = [] self.fit_popup = None self.p0_fwd = None self.p0_bwd = None self.x_fwd = self._generate_x_axis() if self.sweep_type != 'sawtooth': self.x_bwd = self._generate_x_axis(backward=True) else: self.x_bwd = self._generate_x_axis() self.fast = fast # Configure list of experiments self.widgets['config'].setText(config) self.exp_path = self.config['exp_path'] model = QtWidgets.QFileSystemModel() model.setRootPath(self.exp_path) model.setNameFilterDisables(False) model.setNameFilters(['*.py']) self.widgets['exp'].setModel(model) self.widgets['exp'].setRootIndex(model.index(self.exp_path)) self.widgets['exp'].hideColumn(1) self.widgets['exp'].hideColumn(2) self.widgets['exp'].hideColumn(3) self.widgets['exp'].clicked.connect(self.display_experiment) # Configure list of clients self.clients = clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=self.clients, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) if (client == None): client_name_concat = client_type client_item = QtWidgets.QListWidgetItem(client_name_concat) client_item.setForeground(Qt.gray) client_item.setToolTip(str("Disconnected")) self.widgets['clients'].addItem(client_item) else: self.log.info(client) client_name_concat = f"{client_type}_{client_config}" client_item = QtWidgets.QListWidgetItem(client_name_concat) client_item.setToolTip(str(client)) self.widgets['clients'].addItem(client_item) #Checking for any missing clients, and adding them as greyed out on the list of clients # Manually add logger to client self.clients['logger'] = logger # Configure button self.widgets['configure'].clicked.connect(self.configure_experiment) self.widgets['run'].clicked.connect(self.run_pressed) self.widgets['autosave'].toggled.connect(self._update_autosave) self.widgets['save'].pressed.connect( lambda: self.save(filename=self.widgets['save_name'].text(), directory=self.config['save_path'])) self.widgets['reps'].valueChanged.connect(self.set_reps) self.widgets['avg'][0].clicked.connect( lambda: self._clear_show_trace(0)) self.widgets['avg'][1].clicked.connect( lambda: self._clear_show_trace(1)) self.gui.fit.clicked.connect(self.fit_config) # Create legends self.widgets['curve'] = [] for index in range(len(self.widgets['graph'])): self.widgets['legend'][index] = get_legend_from_graphics_view( self.widgets['legend'][index]) # Configure hmaps self.widgets['hmap'] = [] for index in range(2): # Configure Hmap to work the way we want hmap = pg.ImageView(view=pg.PlotItem()) self.gui.graph_layout.insertWidget(2 * index + 1, hmap) hmap.setPredefinedGradient('inferno') hmap.show() hmap.view.setAspectLocked(False) hmap.view.invertY(False) self.widgets['hmap'].append(hmap) # Setup stylesheet. self.gui.apply_stylesheet()
class Controller: """ Class for controlling Toptica scan and laser properties """ def __init__(self, dlc: toptica_dl_pro.Client, gui='toptica_control', logger=None, port=None): """ Initializes toptica specific parameters :param dlc: DLC client for the Toptica laser :param gui: .ui file to use :param logger: LogClient for logging purposes :param port: port number of script server """ self.log = LogHandler(logger) # Setup GUI self.gui = Window(gui_template=gui, host=get_ip(), port=port) self.widgets = get_gui_widgets(gui=self.gui, on_off=2, temperature=2, temperature_actual=2, current=2, current_actual=2, offset=2, amplitude=2, frequency=2, scan=2, update_temp=2, update_current=2, update_params=1) self.dlc = dlc self.offset = 65 self.amplitude = 100 self.scan = [False, False] self.emission = [False, False] # Setup stylesheet. self.gui.apply_stylesheet() self._setup_GUI() def run(self, check_vals=False): """ Runs an iteration of checks for updates and implements :param check_vals: (bool) whether or not to check the values of current and temp """ # Update actual current and temperature # self.gui.activate_scalar('temperature_actual') # self.gui.set_scalar(self.dlc.temp_act(), 'temperature_actual') # self.gui.deactivate_scalar('temperature_actual') # self.gui.activate_scalar('current_actual') # self.gui.set_scalar(self.dlc.current_act(), 'current_actual') # self.gui.deactivate_scalar('current_actual') # Check for on/off updates for i in range(2): if self.widgets['on_off'][i].isChecked() != self.emission[i]: # If laser was already on, turn off if self.emission[i]: self.dlc.turn_off(i + 1) self.emission[i] = False self.log.info(f'Toptica DL {i+1} turned off') # Otherwise turn on else: self.dlc.turn_on(i + 1) self.emission[i] = True self.log.info(f'Toptica DL {i+1} turned on') # Now handle a scan event for i in range(2): if self.widgets['scan'][i].isChecked() != self.scan[i]: # If we were previously scanning, terminate the scan if self.scan[i]: self.dlc.stop_scan(i + 1) self.scan[i] = False self.log.info(f'Toptica DL {i+1} scan stopped') # Otherwise, configure and start the scan else: offset = self.widgets['offset'][i].value() amplitude = self.widgets['amplitude'][i].value() frequency = self.widgets['frequency'][i].value() self.dlc.configure_scan(offset=offset, amplitude=amplitude, frequency=frequency, laser_num=i + 1) self.dlc.start_scan(i + 1) self.scan[i] = True self.log.info(f'Toptica DL Scan {i+1} initiated ' f'with offset: {offset}, ' f'amplitude: {amplitude}, ' f'frequency: {frequency}') # Handle value checking if self.widgets['update_params'].isChecked(): for i in range(2): self.widgets['temperature_actual'][i].setDisabled(False) self.widgets['current_actual'][i].setDisabled(False) if check_vals: try: temp_1 = self.dlc.temp_act(1) if temp_1 < 50: self.widgets['temperature_actual'][0].setValue(temp_1) time.sleep(0.1) self.widgets['current_actual'][0].setValue( self.dlc.current_act(1)) temp_2 = self.dlc.temp_act(2) if temp_2 < 50: self.widgets['temperature_actual'][1].setValue(temp_2) time.sleep(0.1) self.widgets['current_actual'][1].setValue( self.dlc.current_act(2)) except ValueError: pass else: for i in range(2): self.widgets['temperature_actual'][i].setDisabled(True) self.widgets['current_actual'][i].setDisabled(True) self.gui.force_update() def _setup_GUI(self): """ Sets values to current parameters """ # Check if laser is on and update for i in range(2): self.emission[i] = self.dlc.is_laser_on(i + 1) self.widgets['on_off'][i].setChecked(self.emission[i]) time.sleep(0.1) # Get temperature setpoint and actual temperature temp_sp_1 = 100 while temp_sp_1 > 50: temp_sp_1 = self.dlc.temp_sp(1) temp_sp_2 = self.dlc.temp_sp(2) time.sleep(0.1) self.widgets['temperature'][0].setValue(temp_sp_1) self.widgets['temperature'][1].setValue(temp_sp_2) temp_act_1 = 100 while temp_act_1 > 50: temp_act_1 = self.dlc.temp_act(1) temp_act_2 = self.dlc.temp_act(2) time.sleep(0.1) self.widgets['temperature_actual'][0].setValue(temp_act_1) self.widgets['temperature_actual'][1].setValue(temp_act_2) # Get current setpoint and actual current self.widgets['current'][0].setValue(self.dlc.current_sp(1)) time.sleep(0.1) self.widgets['current'][1].setValue(self.dlc.current_sp(2)) time.sleep(0.1) self.widgets['current_actual'][0].setValue(self.dlc.current_act(1)) time.sleep(0.1) self.widgets['current_actual'][1].setValue(self.dlc.current_act(2)) time.sleep(0.1) # Assign button pressing self.widgets['update_temp'][0].clicked.connect( lambda: self._set_temperature(1)) self.widgets['update_temp'][1].clicked.connect( lambda: self._set_temperature(2)) self.widgets['update_current'][0].clicked.connect( lambda: self._set_current(1)) self.widgets['update_current'][1].clicked.connect( lambda: self._set_current(2)) def _set_temperature(self, laser_num): """ Sets the temperature to the setpoint value in the GUI """ temperature = self.widgets['temperature'][laser_num - 1].value() self.dlc.set_temp(temperature, laser_num) self.log.info( f'Set Toptica {laser_num} temperature setpoint to {temperature}') def _set_current(self, laser_num): """ Sets the current to the setpoint value in the GUI """ current = self.widgets['current'][laser_num - 1].value() self.dlc.set_current(current, laser_num) self.log.info(f'Set Toptica {laser_num} current setpoint to {current}')
def __init__(self, config, ao_client=None, ai_client=None, hd=None, check_aom=False, logger=None): """Instantiates LaserStabilizer script object for stabilizing the laser :param config: (str) name of config file """ # Instantiate GUI self.gui = Window(gui_template='power_stabilizer', host=get_ip()) self.widgets = get_gui_widgets(self.gui, p_setpoint=1, p_outputVoltage=1, label_power=1, config=1, graph=2, legend=2, hardware_control=1, clear=1, start=1, stop=1) self._ao_client = ao_client self._ai_client = ai_client self._hd = hd self.log = logger self.config = config self.check_aom = check_aom self._load_config_file() #Now initialize control/output voltage to 0, and set up label self._curr_output_voltage = self.widgets['p_outputVoltage'].value( ) #Stores current output voltage that is outputted by the AO self.widgets['p_outputVoltage'].valueChanged.connect( self._set_output_voltage_from_label) # self._ao_client.set_ao_voltage(self._ao_channel, self._curr_output_voltage) #Update the input power label self._last_power_text_update = 2 self.read_power() self._initialize_graphs() #Setting up hardware power control options self.widgets['hardware_control'].enabled = False self._under_hardware_control = False self.widgets['hardware_control'].toggled.connect( self._update_hardware_control_from_gui) #Finally hookup the final buttons self.widgets['start'].clicked.connect( lambda: self.start(update_st_gui=True)) self.widgets['stop'].clicked.connect(self.stop) self.widgets['clear'].clicked.connect( lambda: self._clear_data_plots(display_pts=1000)) #Initially the program starts in the "unlocked" phase self._is_stabilizing = False
class LaserStabilizer: """A class for stabilizing the laser power given a DAQ input, a power control output, and a setpoint""" def __init__(self, config, ao_client=None, ai_client=None, hd=None, check_aom=False, logger=None): """Instantiates LaserStabilizer script object for stabilizing the laser :param config: (str) name of config file """ # Instantiate GUI self.gui = Window(gui_template='power_stabilizer', host=get_ip()) self.widgets = get_gui_widgets(self.gui, p_setpoint=1, p_outputVoltage=1, label_power=1, config=1, graph=2, legend=2, hardware_control=1, clear=1, start=1, stop=1) self._ao_client = ao_client self._ai_client = ai_client self._hd = hd self.log = logger self.config = config self.check_aom = check_aom self._load_config_file() #Now initialize control/output voltage to 0, and set up label self._curr_output_voltage = self.widgets['p_outputVoltage'].value( ) #Stores current output voltage that is outputted by the AO self.widgets['p_outputVoltage'].valueChanged.connect( self._set_output_voltage_from_label) # self._ao_client.set_ao_voltage(self._ao_channel, self._curr_output_voltage) #Update the input power label self._last_power_text_update = 2 self.read_power() self._initialize_graphs() #Setting up hardware power control options self.widgets['hardware_control'].enabled = False self._under_hardware_control = False self.widgets['hardware_control'].toggled.connect( self._update_hardware_control_from_gui) #Finally hookup the final buttons self.widgets['start'].clicked.connect( lambda: self.start(update_st_gui=True)) self.widgets['stop'].clicked.connect(self.stop) self.widgets['clear'].clicked.connect( lambda: self._clear_data_plots(display_pts=1000)) #Initially the program starts in the "unlocked" phase self._is_stabilizing = False def _load_config_file(self): """Read values from config file.""" self._ai_channel = self.config["power_input_channel"] self._hwc_ai_channel = self.config['hardware_ctrl_input_channel'] self._ao_channel = self.config["ctrl_output_channel"] # Configure default parameters self.min_voltage = self.config[ 'min_output_voltage'] #Minimum output voltage self.max_voltage = self.config[ 'max_output_voltage'] #Maximum output voltage self.gain = self.config[ 'gain'] #"Gain" between measured voltage and corresponding power # NOTE: Internally we store all measured powers as the raw voltages # we then only multiply by the gain factor when displaying # it to the user. self.max_input_voltage = self.config[ 'max_input_voltage'] #Maximum possible input voltage, used for scaling #the DAQ acquisition range. self._hwc_thresh = self.config['hardware_ctrl_thresh'] #Threshold to turn hardware control on/off #Loading PID parameters self.paramP = self.config["pid"]["p"] self.paramI = self.config["pid"]["i"] self.paramD = self.config["pid"]["d"] self.paramMemory = self.config["memory"] # self._update_voltageSetpoint_fromGUI() self._update_PID() self.numReadsPerCycle = self.config[ "reads_per_cycle"] #Number of the reads on the DAQ card that are averaged over for an update cycle. def check_aom_up(self): """Checks if a given HDAWG DIO bit attached to an AOM switch is high""" dio_config = load_config('dio_assignment_global') DIO_bit = dio_config[self.config["aom_staticline"]] current_config = self._hd.geti('dios/0/output') DIO_bit_bitshifted = (0b1 << DIO_bit) # for bit 3: 000...0001000 DIO_bitup = current_config & DIO_bit_bitshifted if DIO_bitup > 0: return True else: return False def run(self): """Main function to update both teh feedback loop as well as the gui""" self._check_hardware_control() if self._is_stabilizing: #If we are locking the power, then need to update the feedback loop and change the output label # Check if DIO bit is high if self.check_aom: if self.check_aom_up(): currSignal = self._update_feedback() self._update_output_voltage_label() else: #If we are not locking the power, just read the power currSignal = self.read_power() else: currSignal = self._update_feedback() self._update_output_voltage_label() else: #If we are not locking the power, just read the power currSignal = self.read_power() #We always need to update the plots self._update_plots(currSignal) self.gui.force_update() def start(self, update_st_gui=True, display_pts=1000): """This method turns on power stabilizationpdate_vs to False to not update the setpoint from the GUI :param update_vs_gui: (Boolean) whether the setpoint should be updated based on the value in the gui, Will always be true when start is run in the GUI, but should be false if an external program wishes to start power locking the laser and manually sets the setpoint. :param display_pts: (int) number of display points to use in the plots""" if update_st_gui: #First, update the setpoint based on the text in the GUI self._update_voltageSetpoint_fromGUI() self._set_output_voltage_from_label() #Update hte PID parameters, which will save the new setpoint to the PID object we use self._update_PID() #Reset the graphs #self._clear_data_plots(display_pts) #Finally turn on the power stabilization self._is_stabilizing = True def stop(self): """This stops power stabilization""" self._is_stabilizing = False def _update_hardware_control_from_gui(self): """Updates hardware_control based on the widget being checked""" self._under_hardware_control = self.widgets[ 'hardware_control'].isChecked() def _check_hardware_control(self): """Method updates whether the program is stabilizing the power or not dependent on the status of the voltage at the input port""" if self._under_hardware_control: v_input = self._ai_client.get_ai_voltage( self._hwc_ai_channel, max_range=10) #CHeck status of hwc voltage input v_input = v_input[-1] if self._is_stabilizing: if v_input < self._hwc_thresh: self.stop() else: if v_input > self._hwc_thresh: self.start() def set_hardware_control(self, value): """Allows external program to set hardware control to the value inputed :param value: (Boolean) whether hardware control should be turned off or no """ self.widgets['hardware_control'].setChecked(value) self._under_hardware_control = value def read_power(self): """reads power and passes latest measurement""" currSignal = None while currSignal is None: try: currSignal = self._ai_client.get_ai_voltage( self._ai_channel, self.numReadsPerCycle, max_range=self.max_input_voltage) except: pass #time.sleep(0.005) #Checks if > 1s has elapsed since the last change to the power reading label #I do this since otherwise the text label updates too quickly and it's annoying #to read. currTime = time.time() if currTime - self._last_power_text_update > 1: power = self.gain * currSignal self.widgets['label_power'].setText(str(self.gain * power[-1])[:5]) self._last_power = power[-1] / self.gain self._last_power_text_update = currTime return currSignal #TODO: Can potentially use some getters/setters to clean up the below two functions make them a little more cllean for the user. def _update_output_voltage_label(self): """Updates the output voltage label to the current voltage being outputted. This is called when the laser is "locked" and the PID loop is actively changing the output voltage""" self.widgets['p_outputVoltage'].setValue((self._curr_output_voltage)) def _set_output_voltage_from_label(self): """Updates the output control voltage based on the text in the output voltage text box. This method is automatically run when the user changes the value in the text box, allowing the user to control the output voltage directly when the laser power is not "locked". """ if ( not self._is_stabilizing ): #Only updates value if we are not stabilizing, otherwise the PID loop will be driving the output voltage #as opposed to the user. self._curr_output_voltage = self.widgets['p_outputVoltage'].value() self._ao_client.set_ao_voltage(self._ao_channel, self._curr_output_voltage) def set_control_voltage(self, value): """Allows an external program to directly set the control/output voltage in use by the stabilizer :param value: (float) value to set output voltage to""" self._curr_output_voltage = value self._update_output_voltage_label() def _update_voltageSetpoint_fromGUI(self): """Update the voltage setpoint to whatever value is currently in the setpoint spin box""" self.voltageSetpoint = self.widgets['p_setpoint'].value() / self.gain def set_setpoint(self, value): """Updates the power setpoint, for use by external programs wishing to interface with this one. :param value: (float) setpoint value to use in units of power (not voltage!) NOTE: Using the GUI this is not normally automatically called. Instead the user must hit start again to update the setpoint if they are in the middle of power stabilizing""" self.voltageSetpoint = value / self.gain self.widgets['p_setpoint'].setValue(value) #Need to reset the PID loop with this new setpoint value self._update_PID() def _update_PID(self): """Creates a new PID object based on the current PID member variables to be used for power feedbacking""" self.pid = PID(p=self.paramP, i=self.paramI, d=self.paramD, setpoint=self.voltageSetpoint, memory=self.paramMemory) def _update_feedback(self): """ Runs the actual feedback loop""" #First read in the current voltage (power) #Read in numReadsPerCycle signals (arb) to average #TODO: allow user to select reads per signal currSignal = None while currSignal is None: try: currSignal = self._ai_client.get_ai_voltage( self._ai_channel, self.numReadsPerCycle, max_range=self.max_input_voltage) except: pass #time.sleep(0.005) #Add new data to the pid self.pid.set_pv(np.atleast_1d(np.mean(currSignal))) #self.pid.set_pv(np.mean(currSignal)) #Now compute the new control value and update the AO self.pid.set_cv() self._curr_output_voltage = self._curr_output_voltage + self.pid.cv if self._curr_output_voltage < self.min_voltage: self._curr_output_voltage = self.min_voltage elif self._curr_output_voltage > self.max_voltage: self._curr_output_voltage = self.max_voltage #Finally updating the analog output #Do a final check to make sure that if you are in hardware control mode that the voltage control is still HIGH #This is to avoid the potential error if the voltage control is toggled low between the last call of _check_hardware_control #and update_feedback, whcih would mean that currSignal would be 0 (assuming a pulsed experiment), and causing a garbage #feedback which could be an issue in the next pulse. if ( not self._under_hardware_control ): #or self.ai_client.get_ai_voltage(self._hwc_ai_channel)[-1] > self._hwc_thresh): self._ao_client.set_ao_voltage(self._ao_channel, self._curr_output_voltage) #Checks if > 1s has elapsed since the last change to the power reading label #I do this since otherwise the text label updates too quickly and it's annoying #to read. currTime = time.time() if currTime - self._last_power_text_update > 1: power = self.gain * currSignal self.widgets['label_power'].setText(str(self.gain * power[-1])[:5]) self._last_power = power[-1] / self.gain self._last_power_text_update = currTime return currSignal def _clear_data_plots(self, display_pts=1000): """Initializes/clears the variables holding the data which is plotted""" #Initializing variables for plotting self.out_voltages = np.ones(display_pts) * self._curr_output_voltage self.measured_powers = np.ones(display_pts) * self._last_power # Check that setpoint is reasonable, otherwise set error to 0 self.errors = np.ones(display_pts) * (self._last_power - self.voltageSetpoint) self.sp_data = np.ones(display_pts) * self.voltageSetpoint def _initialize_graphs(self): """Initializes a channel and outputs to the GUI Should only be called in the initialization of the project """ # Add in teh cleared widgets array self.widgets['curve'] = [] self.widgets['legend'] = [ get_legend_from_graphics_view(legend) for legend in self.widgets['legend'] ] # Create curves # Power self.widgets['curve'].append(self.widgets['graph'][0].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(legend=self.widgets['legend'][0], curve=self.widgets['curve'][0], curve_name="Power") # Setpoint self.widgets['curve'].append(self.widgets['graph'][0].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(legend=self.widgets['legend'][0], curve=self.widgets['curve'][1], curve_name="Setpoint") # Voltage self.widgets['curve'].append(self.widgets['graph'][1].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(legend=self.widgets['legend'][1], curve=self.widgets['curve'][2], curve_name="Voltage") # Error self.widgets['curve'].append(self.widgets['graph'][1].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(legend=self.widgets['legend'][1], curve=self.widgets['curve'][3], curve_name="Error") self._clear_data_plots(1000) def _update_plots(self, currSignal): """Updates the plots, both by adding in the new data and then drawing the data on the graph""" #Adding in new data to plots #currSignal = None #while currSignal is None: # try: # currSignal = self._ai_client.get_ai_voltage(self._ai_channel, max_range=self.max_input_voltage) # except: # time.sleep(0.01) self.measured_powers = np.append(self.measured_powers[1:], np.mean(currSignal)) self.out_voltages = np.append(self.out_voltages[1:], self._curr_output_voltage) self.errors = np.append(self.errors[1:], (currSignal[-1] - self.voltageSetpoint)) self.sp_data = np.append(self.sp_data[1:], self.voltageSetpoint) #Update power plot self.widgets['curve'][0].setData(self.measured_powers * self.gain) #Update setpoint plot self.widgets['curve'][1].setData(self.sp_data * self.gain) # Update voltage plot self.widgets['curve'][2].setData(self.out_voltages) # Update error plot self.widgets['curve'][3].setData(self.errors * self.gain)
def __init__(self, ctr: si_tt.Client, log: LogClient, config, ui='histogram', **kwargs): """ Instantiates TimeTrace measurement :param ctr: (si_tt.Client) client to timetagger hardware :param log: (LogClient) instance of logclient for logging :param config: (str) name of config file :param ui: (str) name of ui file :param **kwargs: additional keyword arguments TODO: in future, can implement multiple histograms if useful """ # Setup GUI self.gui = Window( gui_template='histogram', host=get_ip() ) # Setup stylesheet. self.gui.apply_stylesheet() # Store config self.config = config self.correlation = False if 'type' in self.config: if self.config['type'] == 'correlation': self.correlation = True super().__init__( ctr=ctr, log=log, click_ch=self.config['click_ch'], start_ch=self.config['start_ch'], binwidth=int(self._get_binwidth()), n_bins=self.gui.n_bins.value(), update_interval=0, correlation=self.correlation ) if not type(self.config['click_ch']) == int: combined_click_channel = f"{self.config['click_ch'][0]}+{self.config['click_ch'][1]}" ctr.create_combined_channel( channel_name=combined_click_channel, channel_list=self.config['click_ch'] ) self.config['click_ch'] = [combined_click_channel] self.gates = {} if 'gate_ch' in self.config: # Handle singular input if not isinstance(self.config['gate_ch'], list): self.config['gate_ch'] = [self.config['gate_ch']] # Update GUI to handle gates self._configure_gui_gates() # Setup gated channels for gate_ch in self.config['gate_ch']: ch_name = f'Gated histogram channel {gate_ch}' ctr.create_gated_channel( ch_name, self.config['click_ch'], gate_ch, delay=self.delays[ch_name].value() ) self.gates[ch_name] = TimeTrace( ctr=ctr, log=log, click_ch=ch_name, start_ch=self.config['start_ch'], binwidth=int(self._get_binwidth()), n_bins=self.gui.n_bins.value(), update_interval=0, correlation=self.correlation ) # Configure clicks self.gui.configure.clicked.connect(lambda: self.update_parameters( binwidth=int(self._get_binwidth()), n_bins=self.gui.n_bins.value() )) self.gui.clear.clicked.connect(self.clear_all) # Need Lambda to force it to use default args # https://stackoverflow.com/questions/60001583/pyqt5-slot-function-does-not-take-default-argument self.gui.save.clicked.connect(lambda: self.save()) self.gui.run.clicked.connect(self.run) #### CHANGED CHANGED CHANGED self.gui.fit.clicked.connect(self.fit_config) self._configure_delay_updates() # Configure window length preview self.gui.n_bins.valueChanged.connect(self.update_window_length_label) self.gui.binwidth.valueChanged.connect(self.update_window_length_label) # Configure window length preview self.update_window_length_label() # Initialize plot info self.curve = self.gui.graph.plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]) ) self.gui.graph.getPlotItem().setLabel('bottom', 'Time (s)') self.legend = get_legend_from_graphics_view(self.gui.legend) add_to_legend(self.legend, self.curve, 'Histogram') self.gate_curves = {} index = 1 for gate in self.gates: self.gate_curves[gate] = self.gui.graph.plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[index]) ) index += 1 add_to_legend(self.legend, self.gate_curves[gate], gate) self.gui.apply_stylesheet() self.fitting = False
class Monitor: RANGE_LIST = [ 'AUTO', 'R1NW', 'R10NW', 'R100NW', 'R1UW', 'R10UW', 'R100UW', 'R1MW', 'R10MW', 'R100MW', 'R1W', 'R10W', 'R100W', 'R1KW' ] def __init__(self, pm_client, gui='fiber_coupling', logger=None, calibration=None, name=None, port=None): """ Instantiates a monitor for 2-ch power meter with GUI :param pm_clients: (client, list of clients) clients of power meter :param gui_client: client of monitor GUI :param logger: instance of LogClient :calibration: (float) Calibration value for power meter. :name: (str) Humand-readable name of the power meter. """ self.log = LogHandler(logger) self.gui = Window(gui_template=gui, host=get_ip(), port=port) self.gui.apply_stylesheet() self.wavelength = [] self.calibration = calibration self.name = name self.ir_index, self.rr_index = [], [] self.pm = pm_client self.running = False self.num_plots = 3 # Get all GUI widgets self.widgets = get_gui_widgets(self.gui, graph_widget=self.num_plots, number_widget=4, label_widget=2, name_label=1, combo_widget=2) self._initialize_gui() def sync_settings(self): """ Pulls current settings from PM and sets them to GUI """ # Configure wavelength self.wavelength = self.pm.get_wavelength(1) self.widgets['number_widget'][-1].setValue(self.wavelength) # Configure Range to be Auto self.pm.set_range(1, self.RANGE_LIST[0]) self.pm.set_range(2, self.RANGE_LIST[0]) self.ir_index = 0 self.rr_index = 0 # Connect wavelength change action. self.widgets['number_widget'][-1].valueChanged.connect( self._update_wavelength) # Connect range change. self.widgets['combo_widget'][0].currentIndexChanged.connect( lambda: self._update_range(0)) self.widgets['combo_widget'][1].currentIndexChanged.connect( lambda: self._update_range(1)) def _update_wavelength(self): """ Updates wavelength of pm to WL of GUI""" gui_wl = self.widgets['number_widget'][-1].value() if self.wavelength != gui_wl: self.wavelength = gui_wl self.pm.set_wavelength(1, self.wavelength) self.pm.set_wavelength(2, self.wavelength) def _update_range(self, channel): """ Update range settings if combobox has been changed.""" range_index = self.widgets['combo_widget'][channel].currentIndex() if channel == 0: if self.ir_index != range_index: self.ir_index = range_index self.pm.set_range(1, self.RANGE_LIST[self.ir_index]) elif channel == 1: if self.rr_index != range_index: self.rr_index = range_index self.pm.set_range(2, self.RANGE_LIST[self.rr_index]) def update_settings(self, channel=0): """ Checks GUI for settings updates and implements :param channel: (int) channel of power meter to use """ gui_wl = self.widgets['number_widget_4'].value() if self.wavelength[channel] != gui_wl: self.wavelength[channel] = gui_wl self.pm[channel].set_wavelength(1, self.wavelength[channel]) self.pm[channel].set_wavelength(2, self.wavelength[channel]) gui_ir = self.gui.get_item_index(f'ir_{channel}') if self.ir_index[channel] != gui_ir: self.ir_index[channel] = gui_ir self.pm[channel].set_range(2 * channel + 1, self.RANGE_LIST[self.ir_index[channel]]) gui_rr = self.gui.get_item_index(f'rr_{channel}') if self.rr_index[channel] != gui_rr: self.rr_index[channel] = gui_rr self.pm[channel].set_range(2 * channel + 2, self.RANGE_LIST[self.rr_index[channel]]) def run(self): # Continuously update data until paused self.running = True while self.running: time.sleep(BUFFER) self._update_output() self.gui.force_update() def _update_output(self): """ Runs the power monitor """ # Check for/implement changes to settings #self.update_settings(0) # Get all current values try: p_in = self.pm.get_power(1) split_in = split(p_in) # Handle zero error except OverflowError: p_in = 0 split_in = (0, 0) try: p_ref = self.pm.get_power(2) split_ref = split(p_ref) except OverflowError: p_ref = 0 split_ref = (0, 0) try: efficiency = np.sqrt(p_ref / (p_in * self.calibration[0])) except ZeroDivisionError: efficiency = 0 values = [p_in, p_ref, efficiency] # For the two power readings, reformat. # E.g., split(0.003) will return (3, -3) # And prefix(-3) will return 'm' formatted_values = [split_in[0], split_ref[0], efficiency] value_prefixes = [ prefix(split_val[1]) for split_val in [split_in, split_ref] ] # Update GUI for plot_no in range(self.num_plots): # Update Number self.widgets['number_widget'][plot_no].setValue( formatted_values[plot_no]) # Update Curve self.plotdata[plot_no] = np.append(self.plotdata[plot_no][1:], values[plot_no]) self.widgets[f'curve_{plot_no}'].setData(self.plotdata[plot_no]) if plot_no < 2: self.widgets["label_widget"][plot_no].setText( f'{value_prefixes[plot_no]}W') def _initialize_gui(self): """ Instantiates GUI by assigning widgets """ # Store plot data self.plotdata = [np.zeros(1000) for i in range(self.num_plots)] for plot_no in range(self.num_plots): # Create a curve and store the widget in our dictionary self.widgets[f'curve_{plot_no}'] = self.widgets['graph_widget'][ plot_no].plot(pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))
def __init__(self, wlm_client, logger_client, gui='wavemeter_monitor', ao_clients=None, display_pts=5000, threshold=0.0002, port=None, params=None, three_lasers=False): """ Instantiates WlmMonitor script object for monitoring wavemeter :param wlm_client: (obj) instance of wavemeter client :param gui_client: (obj) instance of GUI client. :param logger_client: (obj) instance of logger client. :param ao_clients: (dict, optional) dictionary of ao client objects with keys to identify. Exmaple: {'ni_usb_1': nidaqmx_usb_client_1, 'ni_usb_2': nidaqmx_usb_client_2, 'ni_pxi_multi': nidaqmx_pxi_client} :param display_pts: (int, optional) number of points to display on plot :param threshold: (float, optional) threshold in THz for lock error signal :param port: (int) port number for update server :param params: (dict) see set_parameters below for details :three_lasers: (bool) If three lasers are in use (instead of 2) """ self.channels = [] if three_lasers: gui = 'wavemeter_monitor_3lasers' self.wlm_client = wlm_client self.ao_clients = ao_clients self.display_pts = display_pts self.threshold = threshold self.log = LogHandler(logger_client) # Instantiate gui self.gui = Window( gui_template=gui, host=get_ip(), port=port, ) # Setup stylesheet. self.gui.apply_stylesheet() if three_lasers: self.widgets = get_gui_widgets(gui=self.gui, freq=3, sp=3, rs=3, lock=3, error_status=3, graph=6, legend=6, clear=6, zero=6, voltage=3, error=3) else: self.widgets = get_gui_widgets(gui=self.gui, freq=2, sp=2, rs=2, lock=2, error_status=2, graph=4, legend=4, clear=4, zero=4, voltage=2, error=2) # Set parameters self.set_parameters(**params) # Configure plots # Get actual legend widgets self.widgets['legend'] = [ get_legend_from_graphics_view(legend) for legend in self.widgets['legend'] ] self.widgets['curve'] = [] self.initialize_channels() for channel in self.channels: self.update_parameters( dict(channel=channel.number, setpoint=channel.data[-1]))
class DataTaker: def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) # Configure dataset menu for name, obj in inspect.getmembers(datasets): if inspect.isclass(obj) and issubclass(obj, datasets.Dataset): self.gui.dataset.addItem(name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet() def update_experiment_list(self): """ Updates list of experiments """ self.gui.exp.clear() for filename in os.listdir(self.exp_path): if filename.endswith('.py'): self.gui.exp.addItem(filename[:-3]) self.gui.exp.itemClicked.connect(self.display_experiment) def display_experiment(self, item): """ Displays the currently clicked experiment in the text browser :param item: (QlistWidgetItem) with label of name of experiment to display """ with open(os.path.join(self.exp_path, f'{item.text()}.py'), 'r') as exp_file: exp_content = exp_file.read() self.gui.exp_preview.setText(exp_content) self.gui.exp_preview.setStyleSheet('font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(0, 0, 0);') self.log.update_metadata(experiment_file=exp_content) def configure(self): """ Configures the currently selected experiment + dataset """ # If the experiment is running, do nothing try: if self.experiment_thread.isRunning(): self.log.warn('Did not configure experiment, since it ' 'is still in progress') return except: pass # Load the config self.reload_config() # Set all experiments to normal state and highlight configured expt for item_no in range(self.gui.exp.count()): self.gui.exp.item(item_no).setBackground( QtGui.QBrush(QtGui.QColor('black'))) self.gui.exp.currentItem().setBackground( QtGui.QBrush(QtGui.QColor('darkRed'))) exp_name = self.gui.exp.currentItem().text() self.module = importlib.import_module(exp_name) self.module = importlib.reload(self.module) # Clear graph area and set up new or cleaned up dataset for index in reversed(range(self.gui.graph_layout.count())): try: self.gui.graph_layout.itemAt(index).widget().deleteLater() except AttributeError: try: self.gui.graph_layout.itemAt(index).layout().deleteLater() except AttributeError: pass self.gui.windows = {} # If we're not setting up a new measurement type, just clear the data self.dataset = getattr(datasets, self.gui.dataset.currentText())( gui=self.gui, log=self.log, config=self.config) # Run any pre-experiment configuration try: self.module.configure(dataset=self.dataset, **self.clients) except AttributeError: pass self.experiment = self.module.experiment self.log.info(f'Experiment {exp_name} configured') self.gui.exp_preview.setStyleSheet( 'font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(50, 50, 50);') def run(self): """ Runs/stops the experiment """ # Run experiment if self.gui.run.text() == 'Run': self.gui.run.setStyleSheet('background-color: red') self.gui.run.setText('Stop') self.log.info('Experiment started') # Run update thread self.update_thread = UpdateThread( autosave=self.gui.autosave.isChecked(), save_time=self.gui.autosave_interval.value()) self.update_thread.data_updated.connect(self.dataset.update) self.update_thread.save_flag.connect(self.save) self.gui.autosave.toggled.connect( self.update_thread.update_autosave) self.gui.autosave_interval.valueChanged.connect( self.update_thread.update_autosave_interval) self.experiment_thread = ExperimentThread(self.experiment, dataset=self.dataset, gui=self.gui, **self.clients) self.experiment_thread.status_flag.connect( self.dataset.interpret_status) self.experiment_thread.finished.connect(self.stop) self.log.update_metadata( exp_start_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Stop experiment else: self.experiment_thread.running = False def stop(self): """ Stops the experiment""" self.gui.run.setStyleSheet('background-color: green') self.gui.run.setText('Run') self.log.info('Experiment stopped') self.update_thread.running = False self.log.update_metadata( exp_stop_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Autosave if relevant if self.gui.autosave.isChecked(): self.save() def save(self): """ Saves data """ self.log.update_metadata(notes=self.gui.notes.toPlainText()) filename = self.gui.save_name.text() directory = self.config['save_path'] self.dataset.save(filename=filename, directory=directory, date_dir=True) save_metadata(self.log, filename, directory, True) self.log.info('Data saved') def reload_config(self): """ Loads a new config file """ self.config = load_script_config(script='data_taker', config=self.gui.config.text(), logger=self.log)