class CV: """ Base class for impedance analysis experiments """ def __init__(self, master, devman, in_batch=False): self.master = master self.dm = devman self.id = 'CV' # Create the main variables of the class self.in_batch = in_batch self.create_variables() self.plot_format = { 'ratios': (1, 1), 'xlabel': 'Frequency (Hz)', 'x_scale': 'log', 'Ch1_ylabel': '|Z| (Ohm)', 'Ch2_ylabel': 'Tz (deg)', 'Ch1_scale': 'log', 'Ch2_scale': 'linear' } top0 = 'Bias = ' top1 = '0 V' col0 = self.plot_format['xlabel'] col1 = self.plot_format['Ch1_ylabel'] col2 = self.plot_format['Ch2_ylabel'] self.header = str(top0) + '= ' + str(top1) + '\n' + str( col0) + '\t' + str(col1) + '\t' + str(col2) self.header.encode('utf-8') self.extension = 'txt' self.batch = Batch(self.master, self.dm, fileheader=self.header) ## Dummy mode # Create the interface self.create_interface() # We load the dummy devices by default self.fill_devices() def quit(self): """ Safe closing of the devices. The devices must be closed by the Device Manager, not directly, so they are registered as "free". """ if self.za is not None: self.dm.close_device(self.za) self.batch.window.destroy() def create_variables(self): # Data variables self.record = None # Hardware variables self.za = None # Acquisition variables self.n_averages = 10 self.stop = True def create_menu_bar(self): """ Add elements to the master menu bar """ # Hardware menu self.master.menu_hardware.add_command( label='ZA', command=lambda: self.za.interface(self.master)) self.master.menu_hardware.entryconfig("ZA", state="disabled") # Batch menu if not self.in_batch: self.master.menu_batch.add_command(label='Disable', command=self.batch.disable) self.master.menu_batch.add_command( label='Temperature', command=lambda: self.new_batch('Temperature')) self.master.menu_batch.add_command( label='Time', command=lambda: self.new_batch('Time')) def new_batch(self, batch_mode): """ Shows current batch window or, if a different batch is chosen, destroys the old one and creates a new one for the new batch mode :param batch_mode: the selected type of batch :return: None """ if self.batch.mode == batch_mode: self.batch.show() else: self.batch.window.destroy() self.batch = Batch(self.master, self.dm, fileheader=self.header, mode=batch_mode) def create_interface(self): """ Create GUI for the CV experiment :return: None """ # Add elements to the menu bar self.create_menu_bar() cv_frame = ttk.Frame(self.master.control_frame) cv_frame.grid(column=0, row=0, sticky=(tk.EW)) cv_frame.columnconfigure(0, weight=1) # Hardware widgets hardware_frame = ttk.Labelframe(cv_frame, text='Selected hardware:', padding=(0, 5, 0, 15)) hardware_frame.grid(column=0, row=0, sticky=(tk.EW)) hardware_frame.columnconfigure(0, weight=1) self.za_var = tk.StringVar() self.za_box = ttk.Combobox(master=hardware_frame, textvariable=self.za_var, state="readonly") self.za_box.grid(column=0, row=0, sticky=(tk.EW)) self.za_box.bind('<<ComboboxSelected>>', self.select_za) # Scan mode widgets --------------------------------- mode_frame = ttk.Labelframe(cv_frame, text='Scan mode:', padding=(0, 5, 0, 15)) mode_frame.grid(column=0, row=1, sticky=(tk.EW)) mode_frame.columnconfigure(1, weight=1) self.fixed_var = tk.StringVar() self.fixed_var.set('bias') ttk.Radiobutton(mode_frame, text="Fixed Bias", variable=self.fixed_var, value='bias', command=self.mode).grid(column=0, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(mode_frame, text="Fixed Frequency", variable=self.fixed_var, value='freq', command=self.mode).grid(column=1, row=0, sticky=(tk.E, tk.W, tk.S)) self.fixed_value_var = tk.StringVar() self.fixed_value_var.set('0') self.fixed_value_label = ttk.Label(mode_frame, text='Set bias (V): ') self.fixed_value_label.grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=mode_frame, width=10, textvariable=self.fixed_value_var).grid(column=1, row=1, sticky=(tk.E, tk.W, tk.S)) self.osc_amp_var = tk.StringVar() self.osc_amp_var.set('0.01') self.osc_amp_label = ttk.Label(mode_frame, text='Set AC bias (V): ') self.osc_amp_label.grid(column=0, row=2, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=mode_frame, width=10, textvariable=self.osc_amp_var).grid(column=1, row=2, sticky=(tk.E, tk.W, tk.S)) # Ch1 setup widgets -------------------------------- Ch1_frame = ttk.Labelframe(cv_frame, text='Ch1 setup:', padding=(0, 5, 0, 15)) Ch1_frame.grid(column=0, row=2, sticky=(tk.EW)) Ch1_frame.columnconfigure(1, weight=1) self.Ch1_param_var = tk.StringVar() self.Ch1_param_var.set('Z') self.Ch1_scale_var = tk.StringVar() self.Ch1_scale_var.set('log') ttk.Radiobutton(Ch1_frame, text="Z", variable=self.Ch1_param_var, value='Z', command=self.channel_param).grid(column=0, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch1_frame, text="Cs", variable=self.Ch1_param_var, value='Cs', command=self.channel_param).grid(column=1, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch1_frame, text="Cp", variable=self.Ch1_param_var, value='Cp', command=self.channel_param).grid(column=2, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch1_frame, text="R", variable=self.Ch1_param_var, value='R', command=self.channel_param).grid(column=3, row=0, sticky=(tk.E, tk.W, tk.S)) self.idc_button = ttk.Radiobutton(Ch1_frame, text="Idc", variable=self.Ch1_param_var, value='Idc', command=self.channel_param) self.idc_button.grid(column=4, row=0, sticky=(tk.E, tk.W, tk.S)) self.idc_button.configure(state='disabled') ttk.Radiobutton(Ch1_frame, text="Linear", variable=self.Ch1_scale_var, value='linear', command=self.scan_scale).grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch1_frame, text="Log10", variable=self.Ch1_scale_var, value='log', command=self.scan_scale).grid(column=1, row=1, sticky=(tk.E, tk.W, tk.S)) # Ch2 setup widgets -------------------------------- Ch2_frame = ttk.Labelframe(cv_frame, text='Ch2 setup:', padding=(0, 5, 0, 15)) Ch2_frame.grid(column=0, row=3, sticky=(tk.EW)) Ch2_frame.columnconfigure(1, weight=1) self.Ch2_param_var = tk.StringVar() self.Ch2_param_var.set('Tz') self.Ch2_scale_var = tk.StringVar() self.Ch2_scale_var.set('linear') ttk.Radiobutton(Ch2_frame, text="Tz", variable=self.Ch2_param_var, value='Tz', command=self.channel_param).grid(column=0, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch2_frame, text="Rs", variable=self.Ch2_param_var, value='Rs', command=self.channel_param).grid(column=1, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch2_frame, text="Rp", variable=self.Ch2_param_var, value='Rp', command=self.channel_param).grid(column=2, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch2_frame, text="X", variable=self.Ch2_param_var, value='X', command=self.channel_param).grid(column=3, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(Ch2_frame, text="Linear", variable=self.Ch2_scale_var, value='linear', command=self.scan_scale).grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) self.Ch2log_button = ttk.Radiobutton(Ch2_frame, text="Log10", variable=self.Ch2_scale_var, value='log', command=self.scan_scale) self.Ch2log_button.grid(column=1, row=1, sticky=(tk.E, tk.W, tk.S)) self.Ch2log_button.configure(state='disabled') # Sweep setup widgets --------------------------------- sweep_frame = ttk.Labelframe(cv_frame, text='Sweep setup:', padding=(0, 5, 0, 15)) sweep_frame.columnconfigure(0, weight=1) sweep_frame.grid(column=0, row=4, sticky=(tk.EW)) self.sweep_start_var = tk.StringVar() self.sweep_start_var.set('20') self.sweep_start_label = ttk.Label(sweep_frame, text='Frequency start (Hz): ') self.sweep_start_label.grid(column=0, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=sweep_frame, width=10, textvariable=self.sweep_start_var).grid(column=1, row=0, sticky=(tk.E, tk.W, tk.S)) self.sweep_stop_var = tk.StringVar() self.sweep_stop_var.set('1e07') self.sweep_stop_label = ttk.Label(sweep_frame, text='Frequency stop (Hz): ') self.sweep_stop_label.grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=sweep_frame, width=10, textvariable=self.sweep_stop_var).grid(column=1, row=1, sticky=(tk.E, tk.W, tk.S)) self.x_scale_var = tk.StringVar() self.x_scale_var.set('log') ttk.Radiobutton(sweep_frame, text="Linear", variable=self.x_scale_var, value='linear', command=self.scan_scale).grid(column=0, row=2, sticky=(tk.E, tk.W, tk.S)) self.xlog_button = ttk.Radiobutton(sweep_frame, text="Log10", variable=self.x_scale_var, value='log', command=self.scan_scale) self.xlog_button.grid(column=1, row=2, sticky=(tk.E, tk.W, tk.S)) self.nop_var = tk.StringVar() self.nop_var.set('400') self.nop_label = ttk.Label(sweep_frame, text='Number of points: ') self.nop_label.grid(column=0, row=3, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=sweep_frame, width=10, textvariable=self.nop_var).grid(column=1, row=3, sticky=(tk.E, tk.W, tk.S)) self.navg_var = tk.StringVar() self.navg_var.set('3') self.navg_label = ttk.Label(sweep_frame, text='Number of point averages: ') self.navg_label.grid(column=0, row=4, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=sweep_frame, width=10, textvariable=self.navg_var).grid(column=1, row=4, sticky=(tk.E, tk.W, tk.S)) self.sweep_dir_var = tk.StringVar() self.sweep_dir_var.set('up') ttk.Radiobutton(sweep_frame, text="Sweep Up", variable=self.sweep_dir_var, value='up').grid(column=0, row=5, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(sweep_frame, text="Sweep Down", variable=self.sweep_dir_var, value='down').grid(column=1, row=5, sticky=(tk.E, tk.W, tk.S)) #self.abort_button = ttk.Button(master=sweep_frame, width=10, text='ABORT!', command=self.abort) #self.abort_button.grid(column=0, row=98, sticky=( tk.E, tk.SW, tk.S)) # The "Run" button is only available if not in batch mode if not self.in_batch: self.run_button = ttk.Button(master=sweep_frame, width=10, text='Run', command=self.start_stop_scan) self.run_button.grid(column=1, row=98, sticky=(tk.E, tk.SW, tk.S)) def update_header(self): """ Updates header in output text file :return: None """ top0 = self.fixed_var.get() top1 = self.fixed_value_var.get() if top0 == 'bias': top2 = 'V' else: top2 = 'Hz' col0 = self.plot_format['xlabel'] col1 = self.plot_format['Ch1_ylabel'] col2 = self.plot_format['Ch2_ylabel'] self.header = str(top0) + '= ' + str(top1) + ' ' + str( top2) + '\n' + str(col0) + '\t' + str(col1) + '\t' + str(col2) self.header.encode('utf-8') def fill_devices(self): """ Fills the device selectors with the corresponding type of devices :return: None """ self.za_box['values'] = self.dm.get_devices(['ZA']) self.za_box.current(0) self.select_za() def select_za(self, *args): """ When the Z analyser selector changes, this function updates some variables and the graphical interface to adapt it to the selected device. :param args: Dummy variable that does nothing but must exist (?) :return: None """ if self.za is not None: self.dm.close_device(self.za) dev_name = self.za_var.get() self.za = self.dm.open_device(dev_name) if self.za is None: self.za_box.current(0) self.za = self.dm.open_device(self.za_var.get()) elif self.dm.current_config[dev_name]['Type'] == 'ZA': pass ## Line required to avoid 'busy device' error else: self.za_box.current(0) self.za = self.dm.open_device(self.za_var.get()) # If the device has an interface to set options, we link it to the entry in the menu interface = getattr(self.za, "interface", None) if callable(interface): self.master.menu_hardware.entryconfig("ZA", state="normal") else: self.master.menu_hardware.entryconfig("ZA", state="disabled") def refresh_buttons(self): """ Updates button features when different experimental setups are selected :return: None """ if self.fixed_var.get() == 'freq': self.x_scale_var.set('linear') self.scan_scale( ) ## Updates plot_format, which is used to pass parameters to the device self.xlog_button.configure(state='disabled') self.idc_button.configure(state='normal') else: self.xlog_button.configure(state='normal') self.idc_button.configure(state='disabled') if self.Ch2_param_var.get() == 'Tz' or self.Ch2_param_var.get() == 'X': self.Ch2_scale_var.set('linear') self.scan_scale( ) ## Updates plot_format, which is used to pass parameters to the device self.Ch2log_button.configure(state='disabled') else: self.Ch2log_button.configure(state='normal') def mode(self): """ When the experimental setup is changed, this function updates some internal variables and the graphical interface :return: None """ if self.fixed_var.get() == 'bias': self.sweep_start_label['text'] = 'Frequency start (Hz): ' self.sweep_stop_label['text'] = 'Frequency stop (Hz): ' self.sweep_start_var.set('20') self.sweep_stop_var.set('1e7') self.fixed_value_label['text'] = 'Set Bias (V): ' self.fixed_value_var.set('0') self.osc_amp_var.set('0.01') self.plot_format['xlabel'] = 'Frequency (Hz)' self.Ch1_param_var.set( 'Z') ## Required to avoid havind Idc selected and disabled elif self.fixed_var.get() == 'freq': self.sweep_start_label['text'] = 'Bias start (V): ' self.sweep_stop_label['text'] = 'Bias stop (V): ' self.sweep_start_var.set('-0.5') self.sweep_stop_var.set('0.5') self.fixed_value_label['text'] = 'Set Frequency (Hz): ' self.fixed_value_var.set('1e5') self.plot_format['xlabel'] = 'Bias (V)' self.fixed_value_var.set('1e5') self.osc_amp_var.set('0.01') self.refresh_buttons() self.master.update_plot_axis(self.plot_format) def channel_param(self): """ When the channel parameters are changed, this function updates some internal variables and the graphical interface :return: None """ ## Channel 1 labels ---------------------------------- if self.Ch1_param_var.get() == 'Z': self.plot_format['Ch1_ylabel'] = '|Z| (Ohm)' elif self.Ch1_param_var.get() == 'Cp': self.plot_format['Ch1_ylabel'] = 'Cp (F)' elif self.Ch1_param_var.get() == 'Cs': self.plot_format['Ch1_ylabel'] = 'Cs (F)' elif self.Ch1_param_var.get() == 'R': self.plot_format['Ch1_ylabel'] = 'Zreal (Ohm)' elif self.Ch1_param_var.get() == 'Idc': self.plot_format['Ch1_ylabel'] = 'Idc (A)' ## Channel 2 labels ---------------------------------- if self.Ch2_param_var.get() == 'Tz': self.plot_format['Ch2_ylabel'] = 'Tz (deg)' elif self.Ch2_param_var.get() == 'Rp': self.plot_format['Ch2_ylabel'] = 'Rp (Ohm)' elif self.Ch2_param_var.get() == 'Rs': self.plot_format['Ch2_ylabel'] = 'Rs (Ohm)' elif self.Ch2_param_var.get() == 'X': self.plot_format['Ch2_ylabel'] = 'Zimag (Ohm)' self.refresh_buttons() self.master.update_plot_axis(self.plot_format) def scan_scale(self): """ When the scan scales are changed, this function updates some internal variables and the graphical interface :return: None """ ## X-axis scale ---------------------------------- if self.x_scale_var.get() == 'linear': self.plot_format['x_scale'] = 'linear' elif self.x_scale_var.get() == 'log': self.plot_format['x_scale'] = 'log' ## Channel 1 scale ---------------------------------- if self.Ch1_scale_var.get() == 'linear': self.plot_format['Ch1_scale'] = 'linear' elif self.Ch1_scale_var.get() == 'log': self.plot_format['Ch1_scale'] = 'log' ## Channel 2 scale ---------------------------------- if self.Ch2_scale_var.get() == 'linear': self.plot_format['Ch2_scale'] = 'linear' elif self.Ch2_scale_var.get() == 'log': self.plot_format['Ch2_scale'] = 'log' self.master.update_plot_axis(self.plot_format) def start_stop_scan(self): """ Starts and stops a scan :return: None """ self.run_check() if False not in self.run_ok: if self.stop: self.stop = False if self.batch.ready: self.run_button['text'] = 'Stop batch' else: self.run_button['state'] = 'disabled' self.prepare_scan() else: if self.batch.ready: self.batch.ready = False self.stop = True self.run_button['text'] = 'Run' else: self.run_button['state'] = 'enabled' self.finish_scan() def prepare_scan(self): """ Any scan is divided in three stages: 1) Prepare the conditions of the scan (this function), creating all relevant variables. 2) Calls the 'start_scan' function to begin the measurement :return: None """ ## If inputs are OK, fill up the options dictionary and proceed. self.options = { 'fixed': self.mode, 'fixed_value': self.fixed_value, 'start': self.sweep_start, 'stop': self.sweep_stop, 'nop': self.nop, 'navg': self.navg, 'osc_amp': self.osc_amp, 'sweep_dir': self.sweep_dir } # # If we are in a batch, we proceed to the next point if self.batch.ready: self.batch.batch_proceed() print('Starting scan...') # Create the record array. In this case it is irrelevant self.record = np.zeros((self.options['nop'] + 1, 4)) self.record[:, 0] = np.linspace(0, 1, self.options['nop'] + 1) self.record[:, 1] = np.ones_like(self.record[:, 0]) self.record[:, 2] = np.ones_like(self.record[:, 0]) self.master.prepare_meas(self.record) self.start_scan() def start_scan(self): """ Begins the experiment :return: None """ self.za.setup_measurement(self.plot_format, self.options) self.za.measure(self.options) self.get_data() def get_data(self): """ Retireves the data collected during the scan :return: None """ data0, data1, data2 = self.za.return_data() self.record = np.zeros((len(data0), 3)) self.record[:, 0] = data0 self.record[:, 1] = data1 self.record[:, 2] = data2 self.master.update_plot(self.record) self.finish_scan() def finish_scan(self): """ Finish the scan, updating some global variables, saving the data in the temp file and offering to save the data somewhere else. :return: None """ if self.batch.ready: self.master.finish_meas(self.record, finish=False) self.batch.batch_wrapup(self.record) else: self.master.finish_meas(self.record, finish=True) self.stop = True if self.stop or not self.batch.ready: # We reduce the number of counts in the batch to resume at the unfinished point if necessary # In other words, it repeats the last, un-finished measurement self.batch.count = self.batch.count - 1 self.run_button['text'] = 'Run' self.run_button['state'] = 'enabled' else: self.prepare_scan() def check_inputs(self, parameter, value, min, max): """ Check the values of relevant scan parameters and gives an error if they are out of range :param parameter: the name of the parameter to check :param: value: the value stored in the parameter variable :param: min: the minimum value allowed for the parameter :param: max: the maximum value allowed for the parameter :return: Error: parameter out of range """ if value < min or value > max: messagebox.showerror("Error", parameter + " is out of range!") self.run_button['state'] = 'enabled' self.run_ok.append(False) else: self.run_ok.append(True) def run_check(self): """ Calls the 'check_inputs' function on all of relevant scan parameters :return: Error - parameter out of range """ self.mode = self.fixed_var.get() self.fixed_value = float(self.fixed_value_var.get()) self.sweep_start = float(self.sweep_start_var.get()) self.sweep_stop = float(self.sweep_stop_var.get()) self.nop = int(self.nop_var.get()) self.navg = int(self.navg_var.get()) self.osc_amp = float(self.osc_amp_var.get()) self.sweep_dir = self.sweep_dir_var.get() ## Check that inputs are within safe/correct limits self.run_ok = [] if self.mode == 'bias': self.check_inputs('DC bias', self.fixed_value, -10, 10) self.check_inputs('Minimun frequency', self.sweep_start, 20, 1e7) self.check_inputs('Maximun frequency', self.sweep_stop, 20, 1e7) elif self.mode == 'freq': self.check_inputs('CW frequency', self.fixed_value, 20, 1e7) self.check_inputs('Minimun DC bias', self.sweep_start, -10, 10) self.check_inputs('Maximun frequency', self.sweep_stop, -10, 10) self.check_inputs('Number of points', self.nop, 1, 1000) self.check_inputs('Number of averages', self.navg, 1, 100) self.check_inputs('AC bias', self.osc_amp, 0.001, 1) def abort(self): """ Aborts scan :return: None """ self.za.abort_sweep() self.get_data()
class IV: """ Base class for IV experiments """ def __init__(self, master, devman, in_batch=False): self.master = master self.dm = devman self.id = 'iv' # Create the main variables of the class self.in_batch = in_batch self.create_variables() # Pre-load the batch, witout creating any interface self.header = 'Voltage (V)\tAbs(Current) (A)\tCurrent (A)' self.extension = 'iv' self.batch = Batch(self.master, self.dm, fileheader=self.header) # Create the interface self.create_interface() self.plot_format = {'ratios': (1, 1), 'xlabel': 'Voltage (V)', 'Ch1_ylabel': 'Abs(Current) (A)', 'Ch2_ylabel': 'Current (A)', 'Ch1_scale': 'log', 'Ch2_scale': 'linear'} # We load the dummy devices by default self.fill_devices() def quit(self): """ Safe closing of the devices. The devices must be closed by the Device Manager, not directly, so they are registered as "free". """ if self.smu is not None: self.dm.close_device(self.smu) self.batch.window.destroy() def create_variables(self): # Data variables self.record = None # Hardware variables self.smu = None # Adquisition variables self.integration_time = 300 self.delay = 100 self.stop = True def create_menu_bar(self): """ Add elememnts to the master menubar """ # Hardware menu self.master.menu_hardware.add_command(label='SMU', command=lambda: self.smu.interface(self.master)) self.master.menu_hardware.entryconfig("SMU", state="disabled") # Batch menu if not self.in_batch: self.master.menu_batch.add_command(label='Disable', command=self.batch.disable) self.master.menu_batch.add_command(label='Temperature', command=lambda: self.new_batch('Temperature')) self.master.menu_batch.add_command(label='Time', command=lambda: self.new_batch('Time')) def new_batch(self, batch_mode): """ Shows current batch window or, if a different batch is chosen, destroys the old one and creates a new one for the new batch mpde :param batch_mode: the selected type of batch :return: None """ if self.batch.mode == batch_mode: self.batch.show() else: self.batch.window.destroy() self.batch = Batch(self.master, self.dm, mode=batch_mode, fileheader=self.header) def create_interface(self): # Add elements to the menu bar self.create_menu_bar() iv_frame = ttk.Frame(self.master.control_frame) iv_frame.grid(column=0, row=0, sticky=(tk.EW)) iv_frame.columnconfigure(0, weight=1) # Hardware widgets hardware_frame = ttk.Labelframe(iv_frame, text='Selected hardware:', padding=(0, 5, 0, 15)) hardware_frame.grid(column=0, row=0, sticky=(tk.EW)) hardware_frame.columnconfigure(0, weight=1) self.smu_var = tk.StringVar() self.smu_box = ttk.Combobox(master=hardware_frame, textvariable=self.smu_var, state="readonly") self.smu_box.grid(column=0, row=0, sticky=(tk.EW)) self.smu_box.bind('<<ComboboxSelected>>', self.select_smu) # Set widgets --------------------------------- set_frame = ttk.Labelframe(iv_frame, text='Set:', padding=(0, 5, 0, 15)) set_frame.grid(column=0, row=1, sticky=(tk.EW)) set_frame.columnconfigure(1, weight=1) self.integration_time_button = ttk.Button(master=set_frame, text='Integration time (ms)', command=self.update_integration_time) self.integration_time_list = ttk.Combobox(set_frame, state="readonly", width=10) self.waiting_time_button = ttk.Button(master=set_frame, text='Waiting time (ms)', command=self.update_waiting_time) self.waiting_time_entry = ttk.Entry(master=set_frame, width=10) self.waiting_time_entry.insert(0, '10') self.integration_time_button.grid(column=0, row=2, sticky=(tk.EW)) self.integration_time_list.grid(column=1, row=2, sticky=(tk.EW)) self.waiting_time_button.grid(column=0, row=3, sticky=(tk.EW)) self.waiting_time_entry.grid(column=1, row=3, sticky=(tk.EW)) self.source_var = tk.StringVar() self.source_var.set('v') ttk.Radiobutton(set_frame, text="Voltage (V)", variable=self.source_var, value='v', command=self.mode).grid(column=0, row=4, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(set_frame, text="Current (A)", variable=self.source_var, value='i', command=self.mode).grid(column=1, row=4, sticky=(tk.E, tk.W, tk.S)) # Scan widgets --------------------------------- scan_frame = ttk.Labelframe(iv_frame, text='Scan:', padding=(0, 5, 0, 15)) scan_frame.columnconfigure(0, weight=1) scan_frame.grid(column=0, row=3, sticky=(tk.EW)) self.start_var = tk.StringVar() self.start_var.set('0.00') self.start_label = ttk.Label(scan_frame, text='Start (V): ') self.start_label.grid(column=0, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=scan_frame, width=10, textvariable=self.start_var).grid(column=1, row=0, sticky=(tk.E, tk.W, tk.S)) self.stop_var = tk.StringVar() self.stop_var.set('1.00') self.stop_label = ttk.Label(scan_frame, text='Stop (V): ') self.stop_label.grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(master=scan_frame, width=10, textvariable=self.stop_var).grid(column=1, row=1, sticky=(tk.E, tk.W, tk.S)) self.step_var = tk.StringVar() self.step_var.set('0.05') self.step_label = ttk.Label(scan_frame, text='Step (V): ') self.step_entry = ttk.Entry(master=scan_frame, width=10, textvariable=self.step_var) self.points_label = ttk.Label(scan_frame, text='Points (per decade): ') self.points_list = ttk.Combobox(scan_frame, state="readonly", width=10) self.compliance_var = tk.StringVar() self.compliance_var.set('0.01') self.compliance_label = ttk.Label(scan_frame, text='Compliance (A): ') self.compliance_label.grid(column=0, row=3, sticky=(tk.E, tk.W, tk.S)) ttk.Entry(scan_frame, width=10, textvariable=self.compliance_var).grid(column=1, row=3, sticky=(tk.E, tk.W, tk.S)) # Combobox containing the available hardware ttk.Label(scan_frame, text='Range: ').grid(column=0, row=4, sticky=(tk.E, tk.W, tk.S)) self.range_list = ttk.Combobox(scan_frame, state="readonly", width=10) self.range_list.grid(column=1, row=4, sticky=(tk.E, tk.W, tk.S)) # The "Run" button is only available if not in batch mode if not self.in_batch: self.run_button = ttk.Button(master=scan_frame, width=10, text='Run', command=self.start_stop_scan) self.run_button.grid(column=1, row=98, sticky=( tk.E, tk.W, tk.S)) def fill_devices(self): """ Fills the device selectors with the corresponding type of devices :return: None """ self.smu_box['values'] = self.dm.get_devices(['SMU']) self.smu_box.current(0) self.select_smu() def select_smu(self, *args): """ When the SMU selector changes, this function updates some variables and the graphical interface to adapt it to the selected device. :param args: Dummy variable that does nothing but must exist (?) :return: None """ if self.smu is not None: self.dm.close_device(self.smu) dev_name = self.smu_var.get() self.smu = self.dm.open_device(dev_name) if self.smu is None: self.smu_box.current(0) self.smu = self.dm.open_device(self.smu_var.get()) elif self.dm.current_config[dev_name]['Type'] == 'SMU': self.source_var.set('v') self.range_list['values'] = self.smu.i_range self.range_list.current(0) self.integration_time_list['values'] = self.smu.int_time self.integration_time_list.current(0) self.points_list['values'] = self.smu.log_points self.points_list.current(1) self.mode() else: self.smu_box.current(0) self.smu = self.dm.open_device(self.smu_var.get()) # If the device has an interface to set options, we link it to the entry in the menu interface = getattr(self.smu, "interface", None) if callable(interface): self.master.menu_hardware.entryconfig("SMU", state="normal") else: self.master.menu_hardware.entryconfig("SMU", state="disabled") def mode(self): """ When the bias mode is changed, this function updates some internal variables and the graphical interface :return: None """ if self.source_var.get() == 'v': self.start_label['text'] = 'Start (V): ' self.stop_label['text'] = 'Stop (V): ' self.compliance_label['text'] = 'Compliance (A): ' self.range_list['values'] = self.smu.i_range self.start_var.set('0') self.stop_var.set('1') self.compliance_var.set('1e-3') self.step_label.grid(column=0, row=2, sticky=(tk.E, tk.W, tk.S)) self.step_entry.grid(column=1, row=2, sticky=(tk.E, tk.W, tk.S)) self.points_label.grid_forget() self.points_list.grid_forget() else: self.start_label['text'] = 'Start (A): ' self.stop_label['text'] = 'Stop (A): ' self.compliance_label['text'] = 'Compliance (V): ' self.range_list['values'] = self.smu.v_range self.start_var.set('1e-9') self.stop_var.set('1e-3') self.compliance_var.set('2') self.points_label.grid(column=0, row=2, sticky=(tk.E, tk.W, tk.S)) self.points_list.grid(column=1, row=2, sticky=(tk.E, tk.W, tk.S)) self.step_label.grid_forget() self.step_entry.grid_forget() def update_integration_time(self): """ Sets the value of the integration time. Informtaion IS sent to the instrument. :return: """ self.integration_time = int(self.integration_time_list.current()) self.smu.update_integration_time(self.integration_time) def update_waiting_time(self): """ Sets the value of the delay between applying a bias and taking the measurement. Informtaion IS sent to the instrument. :return: """ self.delay = int(self.waiting_time_entry.get()) self.smu.update_waiting_time(self.delay) def update_compliance_and_range(self): """ Sets the value of the variables compliance and meas_range. Information IS NOT sent to the instrument. :return: None """ self.compliance = float(self.compliance_var.get()) self.meas_range = self.range_list.current() def start_stop_scan(self): """ Starts and stops an scan :return: None """ if self.stop: self.stop = False if self.batch.ready: self.run_button['text'] = 'Stop batch' else: self.run_button['state'] = 'disabled' self.prepare_scan() else: if self.batch.ready: self.batch.ready = False self.stop = True self.run_button['text'] = 'Run' self.run_button['state'] = 'disabled' def prepare_scan(self): """ Any scan is divided in three stages: 1) Prepare the conditions of the scan (this function), getting starting point, integration time and creating all relevant variables. 2) Runing the scan, performed by a recursive function "get_next_datapoint" 3) Finish the scan, where we update some variables and save the data. :return: None """ mode = self.source_var.get() start = float(self.start_var.get()) stop = float(self.stop_var.get()) step = float(self.step_var.get()) points = int(self.points_list.current()) integration_time = int(self.integration_time_list.current()) delay = int(self.waiting_time_entry.get()) compliance = float(self.compliance_var.get()) meas_range = self.range_list.current() self.options = {'source': mode, 'start': start, 'stop': stop, 'step': step, 'points': points, 'compliance':compliance, 'measRange':meas_range, 'delay': delay, 'intTime': integration_time} # # If we are in a batch, we proceed to the next point if self.batch.ready: self.batch.batch_proceed() print('Starting scan...') # Create the record array. In this case it is irrelevant self.record = np.zeros((points+1, 3)) self.record[:, 0] = np.linspace(0, 1, points+1) self.record[:, 1] = np.ones_like(self.record[:, 1]) self.record[:, 2] = np.ones_like(self.record[:, 1]) self.master.prepare_meas(self.record) self.start_scan() def start_scan(self): measTime = self.smu.measure(**self.options) self.master.window.after(int(measTime), self.get_data) def get_data(self): data0, data1 = self.smu.get_data() self.record = np.zeros((len(data0), 3)) self.record[:, 0] = data0 self.record[:, 1] = abs(data1) self.record[:, 2] = data1 self.master.update_plot(self.record) self.finish_scan() def finish_scan(self): """ Finish the scan, updating some global variables, saving the data in the temp file and offering to save the data somewhere else. :return: None """ if self.batch.ready: self.master.finish_meas(self.record, finish=False) self.batch.batch_wrapup(self.record) else: self.master.finish_meas(self.record, finish=True) self.stop = True if self.stop or not self.batch.ready: # We reduce the number of counts in the batch to resume at the unfinished point if necessary # In other words, it repeats the last, un-finished measurement self.batch.count = self.batch.count - 1 self.run_button['state'] = 'enabled' else: self.prepare_scan()
class Spectroscopy: """ Base class for spectroscopy experiments """ def __init__(self, master, devman): self.master = master self.dm = devman self.id = 'spec' self.header = 'Wavelength (nm)\tCh-1\tCh-2' self.extension = 'txt' # Pre-load the batch, witout creating any interface self.batch = Batch(self.master, self.dm, fileheader=self.header) # Create the main variables of the class self.create_variables() # Create the interface self.create_interface() self.plot_format = {'ratios' : (3, 1), 'xlabel' : 'Wavelength (nm)', 'Ch1_ylabel' : 'Ch-1', 'Ch2_ylabel' : 'Ch-2', 'Ch1_scale' : 'linear', 'Ch2_scale' : 'linear'} # We load the dummy devices by default self.fill_devices() def quit(self): """ Safe closing of the devices. The devices must be closed by the Device Manager, not directly, so they are registered as "free". """ if self.monochromator is not None: self.dm.close_device(self.monochromator) if self.adquisition is not None: self.dm.close_device(self.adquisition) self.batch.quit() def create_variables(self): # Data variables self.record = None self.background = None # Hardware variables self.monochromator = None self.adquisition = None # Adquisition variables self.integration_time = 300 self.waiting_time = 100 self.stop = True def create_interface(self): # Add elements to the menu bar self.create_menu_bar() # Creates the spectroscopy frame self.spectroscopy_frame = ttk.Frame(master=self.master.control_frame) self.spectroscopy_frame.grid(column=0, row=0, sticky=(tk.EW)) self.spectroscopy_frame.columnconfigure(0, weight=1) # Hardware widgets hardware_frame = ttk.Labelframe(self.spectroscopy_frame, text='Selected hardware:', padding=(0, 5, 0, 15)) hardware_frame.columnconfigure(0, weight=1) hardware_frame.grid(column=0, row=0, sticky=(tk.EW)) self.mono_var = tk.StringVar() self.adq_var = tk.StringVar() self.monochromator_box = ttk.Combobox(master=hardware_frame, textvariable=self.mono_var, state="readonly") self.adquisition_box = ttk.Combobox(master=hardware_frame, textvariable=self.adq_var, state="readonly") self.monochromator_box.bind('<<ComboboxSelected>>', self.select_monochromator) self.adquisition_box.bind('<<ComboboxSelected>>', self.select_adquisition) self.monochromator_box.grid(column=0, row=0, sticky=(tk.EW)) self.adquisition_box.grid(column=0, row=1, sticky=(tk.EW)) # Set widgets --------------------------------- set_frame = ttk.Labelframe(self.spectroscopy_frame, text='Set:', padding=(0, 5, 0, 15)) set_frame.columnconfigure(1, weight=1) self.GoTo_button = ttk.Button(master=set_frame, text='GoTo', command=self.goto) self.GoTo_entry = ttk.Entry(master=set_frame, width=10) self.GoTo_entry.insert(0, '700.0') self.integration_time_button = ttk.Button(master=set_frame, text='Integration time (ms)', command=self.update_integration_time) self.integration_time_entry = ttk.Entry(master=set_frame, width=10) self.integration_time_entry.insert(0, '300') self.waiting_time_button = ttk.Button(master=set_frame, text='Waiting time (ms)', command=self.update_waiting_time) self.waiting_time_entry = ttk.Entry(master=set_frame, width=10) self.waiting_time_entry.insert(0, '100') set_frame.grid(column=0, row=1, sticky=(tk.EW)) self.GoTo_button.grid(column=0, row=0, sticky=(tk.EW)) self.GoTo_entry.grid(column=1, row=0, sticky=(tk.EW)) self.integration_time_button.grid(column=0, row=2, sticky=(tk.EW)) self.integration_time_entry.grid(column=1, row=2, sticky=(tk.EW)) self.waiting_time_button.grid(column=0, row=3, sticky=(tk.EW)) self.waiting_time_entry.grid(column=1, row=3, sticky=(tk.EW)) # Live adquisition widgets live_frame = ttk.Labelframe(self.spectroscopy_frame, text='Live:', padding=(0, 5, 0, 15)) live_frame.columnconfigure(1, weight=1) live_frame.columnconfigure(2, weight=1) self.window_live_lbl = ttk.Label(master=live_frame, text="Window (points):") self.window_live_entry = ttk.Entry(master=live_frame, width=10) self.window_live_entry.insert(0, '100') self.record_live_button = ttk.Button(master=live_frame, text='Record', command=self.record_live) self.pause_live_button = ttk.Button(master=live_frame, text='Pause', state=tk.DISABLED, command=self.pause_live) live_frame.grid(column=0, row=2, sticky=(tk.EW)) self.window_live_lbl.grid(column=0, row=0, sticky=(tk.EW)) self.window_live_entry.grid(column=1, row=0, columnspan=2, sticky=(tk.EW)) self.record_live_button.grid(column=1, row=3, sticky=(tk.EW)) self.pause_live_button.grid(column=2, row=3, sticky=(tk.EW)) # Scan widgets --------------------------------- scan_frame = ttk.Labelframe(self.spectroscopy_frame, text='Scan:', padding=(0, 5, 0, 15)) scan_frame.columnconfigure(0, weight=1) Start_lbl = ttk.Label(master=scan_frame, text="Start (nm):") self.Start_entry = ttk.Entry(master=scan_frame) self.Start_entry.insert(0, '700.0') Stop_lbl = ttk.Label(master=scan_frame, text="Stop (nm):") self.Stop_entry = ttk.Entry(master=scan_frame) self.Stop_entry.insert(0, '900.0') Step_lbl = ttk.Label(master=scan_frame, text="Step (nm):") self.Step_entry = ttk.Entry(master=scan_frame) self.Step_entry.insert(0, '5.0') self.scan_button = ttk.Button(master=scan_frame, text='Run', command=self.start_stop_scan, width=7) self.pause_button = ttk.Button(master=scan_frame, text='Pause', state=tk.DISABLED, command=self.pause_scan, width=7) scan_frame.grid(column=0, row=3, sticky=(tk.EW)) Start_lbl.grid(column=0, row=0, sticky=(tk.EW)) self.Start_entry.grid(column=1, row=0, columnspan=2, sticky=(tk.EW)) Stop_lbl.grid(column=0, row=1, sticky=(tk.EW)) self.Stop_entry.grid(column=1, row=1, columnspan=2, sticky=(tk.EW)) Step_lbl.grid(column=0, row=2, sticky=(tk.EW)) self.Step_entry.grid(column=1, row=2, columnspan=2, sticky=(tk.EW)) self.scan_button.grid(column=1, row=3, sticky=(tk.EW)) self.pause_button.grid(column=2, row=3, sticky=(tk.EW)) # Background widgets self.background_frame = ttk.Labelframe(self.spectroscopy_frame, text='Background:', padding=(0, 5, 0, 15)) self.background_frame.columnconfigure(0, weight=1) self.background_frame.columnconfigure(1, weight=1) self.background_button = ttk.Button(master=self.background_frame, text='Get', command=self.get_background) self.clear_background_button = ttk.Button(master=self.background_frame, text='Clear', command=self.clear_background) self.background_frame.grid(column=0, row=4, sticky=(tk.NSEW)) self.background_button.grid(column=0, row=0, sticky=(tk.EW, tk.S)) self.clear_background_button.grid(column=1, row=0, sticky=(tk.EW, tk.S)) def create_menu_bar(self): """ Add elememnts to the master menubar """ # Hardware menu self.master.menu_hardware.add_command(label='Monochromator', command=lambda: self.monochromator.interface(self.master)) self.master.menu_hardware.entryconfig("Monochromator", state="disabled") self.master.menu_hardware.add_command(label='Adquisition', command=lambda: self.adquisition.interface(self.master)) self.master.menu_hardware.entryconfig("Adquisition", state="disabled") # Batch menu self.master.menu_batch.add_command(label='Disable', command=self.batch.disable) self.master.menu_batch.add_command(label='IV', command=lambda: self.new_batch('IV')) self.master.menu_batch.add_command(label='Temperature', command=lambda: self.new_batch('Temperature')) self.master.menu_batch.add_command(label='Time', command=lambda: self.new_batch('Time')) def new_batch(self, batch_mode): """ Shows current batch window or, if a different batch is chosen, destroys the old one and creates a new one for the new batch mpde :param batch_mode: the selected type of batch :return: None """ if self.batch.mode == batch_mode: self.batch.show() else: self.batch.quit() self.batch = Batch(self.master, self.dm, mode=batch_mode) def fill_devices(self): """ Fills the device selectors with the corresponding type of devices :return: """ self.monochromator_box['values'] = self.dm.get_devices(['Monochromator']) self.monochromator_box.current(0) self.adquisition_box['values'] = self.dm.get_devices(['Lock-In', 'Spectrometer']) self.adquisition_box.current(0) self.select_monochromator() self.select_adquisition() def select_monochromator(self, *args): if self.monochromator is not None: self.dm.close_device(self.monochromator) dev_name = self.mono_var.get() self.monochromator = self.dm.open_device(dev_name) if self.monochromator is None: self.monochromator_box.current(0) self.monochromator = self.dm.open_device(self.mono_var.get()) elif self.dm.current_config[dev_name]['Type'] == 'Monochromator': self.move = self.monochromator.move else: self.monochromator_box.current(0) self.monochromator = self.dm.open_device(self.mono_var.get()) # If the device has an interface to set options, we link it to the entry in the menu interface = getattr(self.monochromator, "interface", None) if callable(interface): self.master.menu_hardware.entryconfig("Monochromator", state="normal") else: self.master.menu_hardware.entryconfig("Monochromator", state="disabled") def select_adquisition(self, *args): """ When the adquisition selector changes, this function updates some variables and the graphical interface to adapt it to the selected device. :param args: Dummy variable that does nothing but must exist (?) :return: None """ if self.adquisition is not None: self.dm.close_device(self.adquisition) dev_name = self.adq_var.get() self.adquisition = self.dm.open_device(dev_name) if self.adquisition is None: self.adquisition_box.current(0) self.adquisition = self.dm.open_device(self.adq_var.get()) elif self.dm.current_config[dev_name]['Type'] == 'Spectrometer': self.move = self.null self.prepare_scan = self.prepare_scan_spectrometer self.get_next_datapoint = self.mode_spectrometer self.start_live = self.prepare_live_spectrometer self.live = self.live_spectrometer self.background_frame.grid(column=0, row=4, sticky=(tk.NSEW)) self.window_live_lbl.grid_forget() self.window_live_entry.grid_forget() self.monochromator_box['state'] = 'disabled' self.Step_entry['state'] = 'disabled' self.GoTo_button['state'] = 'disabled' self.GoTo_entry['state'] = 'disabled' elif self.dm.current_config[dev_name]['Type'] in ['Lock-In', 'Multimeter']: self.move = self.monochromator.move self.prepare_scan = self.prepare_scan_lockin self.get_next_datapoint = self.mode_lockin self.start_live = self.prepare_live_lockin self.live = self.live_lockin self.background_frame.grid_forget() self.window_live_lbl.grid(column=0, row=0, sticky=(tk.EW)) self.window_live_entry.grid(column=1, row=0, columnspan=2, sticky=(tk.EW)) self.monochromator_box['state'] = 'normal' self.Step_entry['state'] = 'normal' self.GoTo_button['state'] = 'normal' self.GoTo_entry['state'] = 'normal' else: self.adquisition_box.current(0) self.adquisition = self.dm.open_device(self.adq_var.get()) interface = getattr(self.adquisition, "interface", None) if callable(interface): self.master.menu_hardware.entryconfig("Adquisition", state="normal") else: self.master.menu_hardware.entryconfig("Adquisition", state="disabled") def null(self, *args, **kwargs): """ Empty function that does nothing :return: None """ pass def start_stop_scan(self): """ Starts and stops an scan :return: None """ if self.stop: self.prepare_scan() else: self.stop = True self.finish_scan() def pause_scan(self): """ Pauses an scan or resumes the adquisition :return: None """ self.stop = not self.stop if self.stop: self.pause_button['text'] = 'Resume' else: self.pause_button['text'] = 'Pause' self.get_next_datapoint() def prepare_scan_lockin(self): """ Any scan is divided in three stages: 1) Prepare the conditions of the scan (this function), getting starting point, integration time and creating all relevant variables. 2) Runing the scan, performed by a recursive function "mode_spectrometer" or "mode_lockin" 3) Finish the scan, where we update some variables and save the data. :return: None """ self.update_integration_time() self.update_waiting_time() # Get the scan conditions self.start_wl = max(float(self.Start_entry.get()), 250) self.stop_wl = max(min(float(self.Stop_entry.get()), 3000), self.start_wl + 1) step = min(max(float(self.Step_entry.get()), self.adquisition.min_wavelength), self.stop_wl - self.start_wl) # # If we are in a batch, we proceed to the next point if self.batch.ready: self.batch.batch_proceed() self.move(self.start_wl, speed='Fast') print('Starting scan...') # Create the record array self.size = int(np.ceil((self.stop_wl - self.start_wl + 0.5 * step) / step)) self.num = self.size self.record = np.zeros((self.size, 3)) self.record[:, 0] = np.arange(self.start_wl, self.stop_wl + 0.5 * step, step) self.record[:, 1] = self.record[:, 1] * np.NaN self.record[:, 2] = self.record[:, 1] * np.NaN self.master.prepare_meas(self.record) self.i = 0 self.scan_running() self.mode_lockin() def mode_lockin(self): """ Gets the next data point in a scan. This function depends on the adquisition device :return: None """ if not self.stop: data = self.adquisition.measure() self.record[self.i, 1] = data[0] self.record[self.i, 2] = data[1] self.master.update_plot(self.record) if self.i < self.num - 1: self.i += 1 self.move(self.record[self.i, 0], speed='Fast') self.master.window.after(int(self.integration_time + self.waiting_time), self.mode_lockin) else: self.finish_scan() def prepare_scan_spectrometer(self): """ Any scan is divided in three stages: 1) Prepare the conditions of the scan (this function), getting starting point, integration time and creating all relevant variables. 2) Runing the scan, performed by a recursive function "mode_spectrometer" or "mode_lockin" 3) Finish the scan, where we update some variables and save the data. :return: None """ self.update_integration_time() self.update_waiting_time() # We check the background is not none and offer to update it if self.background is None: meas_bg = messagebox.askyesno(message='There is no background spectrum for this integration time!', detail='Do you want to measure it now?', icon='question', title='Measure background?') self.get_background(meas_bg) # # If we are in a batch, we proceed to the next point if self.batch.ready: self.batch.batch_proceed() # If the integration time is too long, we have to split the adquisition in several steps, # otherwise the spectrometer hangs self.num = int(np.ceil(self.integration_time / self.adquisition.max_integration_time)) # Here we select the wavelength range we want to record and re-shape the record array # Get the scan conditions self.start_wl = max(float(self.Start_entry.get()), 300) self.stop_wl = max(min(float(self.Stop_entry.get()), 2000), self.start_wl + 1) wl = self.adquisition.measure()[0] self.idx = np.where((self.start_wl <= wl) & (wl <= self.stop_wl)) self.size = len(self.idx[0]) # Create the record array self.record = np.zeros((self.size, 3)) self.record[:, 0] = wl[self.idx] self.record[:, 2] = self.background[self.idx] self.master.prepare_meas(self.record) self.i = 0 self.scan_running() self.mode_spectrometer() def mode_spectrometer(self): """ Gets the whole spectrum at once recorded by the spectrometer in the range selected :return: None """ if not self.stop: data = self.adquisition.measure() intensity = data[1][self.idx] - self.background[self.idx] self.record[:, 1] = (intensity + self.i * self.record[:, 1]) / (self.i + 1.) self.master.update_plot(self.record) if self.i < self.num - 1: self.i = self.i + 1 self.master.window.after(int(self.integration_time / self.num), self.mode_spectrometer) else: self.finish_scan() def finish_scan(self): """ Finish the scan, updating some global variables, saving the data in the temp file and offering to save the data somewhere else. :return: None """ if self.batch.ready: self.master.finish_meas(self.record, finish=False) self.batch.batch_wrapup(self.record) else: self.master.finish_meas(self.record, finish=True) if self.stop or not self.batch.ready: # We reduce the number of counts in the batch to resume at the unfinished point if necessary # In other words, it repeats the last, un-finished measurement self.batch.count = max(self.batch.count - 1, 0) self.scan_stopped() else: self.prepare_scan() def scan_running(self): """ Updates the graphical interface, disable the buttoms that must be disabled during the measurement. :return: None """ self.scan_button['text'] = 'Stop' self.pause_button['state'] = 'enabled' self.record_live_button['state'] = 'disabled' self.GoTo_button['state'] = 'disabled' self.integration_time_button['state'] = 'disabled' self.waiting_time_button['state'] = 'disabled' self.background_button['state'] = 'disabled' self.clear_background_button['state'] = 'disabled' self.stop = False def scan_stopped(self): """ Returns the graphical interface to normal once the measurement has finished. :return: None """ self.scan_button['text'] = 'Run' self.pause_button['state'] = 'disabled' self.record_live_button['state'] = 'enabled' self.GoTo_button['state'] = 'enabled' self.integration_time_button['state'] = 'enabled' self.waiting_time_button['state'] = 'enabled' self.background_button['state'] = 'enabled' self.clear_background_button['state'] = 'enabled' self.stop = True def get_background(self, meas_bg=True): """ Gets the background when using the spectrometer, if requested. :parameter: meas_bg True or False: wether to measure a background or just produce a bg with zeros :return: None """ if meas_bg: self.update_integration_time() self.background = self.adquisition.measure()[1] messagebox.showinfo(message='Background taken!', detail='Press OK to continue.', title='Background taken!') else: self.background = self.adquisition.measure()[1]*0.0 def clear_background(self): """ Clears the background when using the spectrometer. :return: None """ self.background = None def record_live(self): """ Starts and stops a live recording. :return: None """ if self.stop: self.stop = False self.record_live_button['text'] = 'Stop' self.pause_live_button['state'] = 'enabled' self.scan_button['state'] = 'disabled' self.background_button['state'] = 'disabled' self.clear_background_button['state'] = 'disabled' self.start_live() else: self.record_live_button['text'] = 'Record' self.pause_live_button['state'] = 'disabled' self.scan_button['state'] = 'enabled' self.background_button['state'] = 'enabled' self.clear_background_button['state'] = 'enabled' self.stop = True self.finish_live() def pause_live(self): """ Pauses a live recording or resumes the adquisition """ self.stop = not self.stop if self.stop: self.pause_live_button['text'] = 'Resume' else: self.pause_live_button['text'] = 'Pause' self.live() def prepare_live_lockin(self): """ Prepares the lock-in live adquisition and prepare some variables """ self.goto() self.update_integration_time() self.window_points = int(self.window_live_entry.get()) self.live_data = np.zeros((self.window_points, 3)) self.live_data[:, 0] = np.arange(self.window_points) # Removes all plots, but not the data, and change the horizontal axis conditions self.master.clear_plot(xtitle='Time', ticks='off') self.master.prepare_meas(self.live_data) self.live_lockin() def live_lockin(self): """ Runs the live lock-in adquisition """ if not self.stop: self.live_data[:-1, 1] = self.live_data[1:, 1] self.live_data[:-1, 2] = self.live_data[1:, 2] self.live_data[-1, 1], self.live_data[-1, 2] = self.adquisition.measure() self.master.update_plot(self.live_data) self.master.window.after(self.integration_time, self.live_lockin) def prepare_live_spectrometer(self): """ Prepares the spectrometer live adquisition and prepare some variables """ self.update_integration_time() if self.integration_time > self.adquisition.max_integration_time: self.integration_time = int(self.adquisition.max_integration_time) self.adquisition.update_integration_time(self.integration_time) data0, data1 = self.adquisition.measure() self.live_data = np.zeros((len(data0), 3)) self.live_data[:, 0] = data0 self.live_data[:, 1] = data1 # Removes all plots, but not the data, and change the horizontal axis conditions self.master.clear_plot(xtitle='Wavelength (nm)', ticks='on') self.master.prepare_meas(self.live_data) self.live_spectrometer() def live_spectrometer(self): """ Runs the live spectrometer adquisition """ if not self.stop: data0, data1 = self.adquisition.measure() self.live_data[:, 1] = data1 self.master.update_plot(self.live_data) self.master.window.after(self.integration_time, self.live_spectrometer) def finish_live(self): """ Finish the live adquisition, returning the front end to the scan mode """ self.master.replot_data(xtitle='Wavelength (nm)', ticks='on') def update_integration_time(self): """ Updates the integration time """ old_integration_time = self.integration_time self.integration_time = int(self.integration_time_entry.get()) if old_integration_time != self.integration_time: self.clear_background() self.integration_time = self.adquisition.update_integration_time(self.integration_time) self.integration_time_entry.delete(0, tk.END) self.integration_time_entry.insert(0, '%i' % self.integration_time) def update_waiting_time(self): """ Updates the waiting time """ self.waiting_time = int(self.waiting_time_entry.get()) def goto(self): """ Go to the specified wavelength """ wl = float(self.GoTo_entry.get()) self.move(wl, speed='Fast') print('Done! Wavelength = {} nm'.format(wl))
class Photoreflectance: """ Base class for photoreflectance experiments """ def __init__(self, master, devman): self.master = master self.dm = devman self.id = 'PR' # Create the main variables of the class self.create_variables() self.plot_format = { 'ratios': (1, 1), 'xlabel': 'Wavelength (nm)', 'x_scale': 'linear', 'Ch1_ylabel': 'Xsignal/Rbaseline', 'Ch2_ylabel': 'Rbaseline', 'Ch1_scale': 'linear', 'Ch2_scale': 'linear' } self.header = '' self.header.encode('utf-8') self.extension = 'txt' # Pre-load the batch, witout creating any interface self.batch = Batch(self.master, self.dm, fileheader=self.header) # Create the interface self.create_interface() # We load the dummy devices by default self.fill_devices() def quit(self): """ Safe closing of the devices. The devices must be closed by the Device Manager, not directly, so they are registered as "free". """ if self.monochromator is not None: self.monochromator.close() ## Destroys the system model self.dm.close_device(self.monochromator) if self.acquisition is not None: self.acquisition.close() ## Unsubscribes from the demodulators self.dm.close_device(self.acquisition) self.batch.quit() def create_variables(self): # Data variables self.record = None self.background = None self.plotdata = None ## Transformed data to be plotted # Hardware variables self.monochromator = None self.acquisition = None # acquisition variables self.integration_time = 100 self.waiting_time = 100 self.stop = True def create_interface(self): # Add elements to the menu bar self.create_menu_bar() # Creates the photoreflectance frame self.spectroscopy_frame = ttk.Frame(master=self.master.control_frame) self.spectroscopy_frame.grid(column=0, row=0, sticky=(tk.EW)) self.spectroscopy_frame.columnconfigure(0, weight=1) # Hardware widgets hardware_frame = ttk.Labelframe(self.spectroscopy_frame, text='Selected hardware:', padding=(0, 5, 0, 15)) hardware_frame.columnconfigure(0, weight=1) hardware_frame.grid(column=0, row=0, sticky=(tk.EW)) self.mono_var = tk.StringVar() self.acq_var = tk.StringVar() self.monochromator_box = ttk.Combobox(master=hardware_frame, textvariable=self.mono_var, state="readonly") self.acquisition_box = ttk.Combobox(master=hardware_frame, textvariable=self.acq_var, state="readonly") self.monochromator_box.bind('<<ComboboxSelected>>', self.select_monochromator) self.acquisition_box.bind('<<ComboboxSelected>>', self.select_acquisition) self.monochromator_box.grid(column=0, row=0, sticky=(tk.EW)) self.acquisition_box.grid(column=0, row=1, sticky=(tk.EW)) # Set widgets --------------------------------- set_frame = ttk.Labelframe(self.spectroscopy_frame, text='Set:', padding=(0, 5, 0, 15)) set_frame.columnconfigure(1, weight=1) self.GoTo_button = ttk.Button(master=set_frame, text='GoTo', command=self.goto) self.GoTo_entry = ttk.Entry(master=set_frame, width=10) self.GoTo_entry.insert(0, '700.0') self.integration_time_button = ttk.Button( master=set_frame, text='Integration time (ms)', command=self.update_integration_time) self.integration_time_entry = ttk.Entry(master=set_frame, width=10) self.integration_time_entry.insert(0, '100') self.waiting_time_button = ttk.Button(master=set_frame, text='Waiting time (ms)', command=self.update_waiting_time) self.waiting_time_entry = ttk.Entry(master=set_frame, width=10) self.waiting_time_entry.insert(0, '100') set_frame.grid(column=0, row=1, sticky=(tk.EW)) self.GoTo_button.grid(column=0, row=0, sticky=(tk.EW)) self.GoTo_entry.grid(column=1, row=0, sticky=(tk.EW)) self.integration_time_button.grid(column=0, row=2, sticky=(tk.EW)) self.integration_time_entry.grid(column=1, row=2, sticky=(tk.EW)) self.waiting_time_button.grid(column=0, row=3, sticky=(tk.EW)) self.waiting_time_entry.grid(column=1, row=3, sticky=(tk.EW)) # Live acquisition widgets live_frame = ttk.Labelframe(self.spectroscopy_frame, text='Live:', padding=(0, 5, 0, 15)) live_frame.columnconfigure(1, weight=1) live_frame.columnconfigure(2, weight=1) self.window_live_lbl = ttk.Label(master=live_frame, text="Window (points):") self.window_live_entry = ttk.Entry(master=live_frame, width=10) self.window_live_entry.insert(0, '100') self.record_live_button = ttk.Button(master=live_frame, text='Record', command=self.record_live) self.pause_live_button = ttk.Button(master=live_frame, text='Pause', state=tk.DISABLED, command=self.pause_live) live_frame.grid(column=0, row=2, sticky=(tk.EW)) self.window_live_lbl.grid(column=0, row=0, sticky=(tk.EW)) self.window_live_entry.grid(column=1, row=0, columnspan=2, sticky=(tk.EW)) self.record_live_button.grid(column=1, row=3, sticky=(tk.EW)) self.pause_live_button.grid(column=2, row=3, sticky=(tk.EW)) # Scan widgets --------------------------------- scan_frame = ttk.Labelframe(self.spectroscopy_frame, text='Scan:', padding=(0, 5, 0, 15)) scan_frame.columnconfigure(0, weight=1) Start_lbl = ttk.Label(master=scan_frame, text="Start (nm):") self.Start_entry = ttk.Entry(master=scan_frame) self.Start_entry.insert(0, '700.0') Stop_lbl = ttk.Label(master=scan_frame, text="Stop (nm):") self.Stop_entry = ttk.Entry(master=scan_frame) self.Stop_entry.insert(0, '900.0') Step_lbl = ttk.Label(master=scan_frame, text="Step (nm):") self.Step_entry = ttk.Entry(master=scan_frame) self.Step_entry.insert(0, '5.0') self.scan_button = ttk.Button(master=scan_frame, text='Run', command=self.start_stop_scan, width=7) self.pause_button = ttk.Button(master=scan_frame, text='Pause', state=tk.DISABLED, command=self.pause_scan, width=7) scan_frame.grid(column=0, row=3, sticky=(tk.EW)) Start_lbl.grid(column=0, row=0, sticky=(tk.EW)) self.Start_entry.grid(column=1, row=0, columnspan=2, sticky=(tk.EW)) Stop_lbl.grid(column=0, row=1, sticky=(tk.EW)) self.Stop_entry.grid(column=1, row=1, columnspan=2, sticky=(tk.EW)) Step_lbl.grid(column=0, row=2, sticky=(tk.EW)) self.Step_entry.grid(column=1, row=2, columnspan=2, sticky=(tk.EW)) self.scan_button.grid(column=1, row=3, sticky=(tk.EW)) self.pause_button.grid(column=2, row=3, sticky=(tk.EW)) # Background widgets--------------------------------------------- self.background_frame = ttk.Labelframe(self.spectroscopy_frame, text='Background:', padding=(0, 5, 0, 15)) self.background_frame.columnconfigure(0, weight=1) self.background_frame.columnconfigure(1, weight=1) self.background_button = ttk.Button(master=self.background_frame, text='Get', command=self.get_background) self.clear_background_button = ttk.Button( master=self.background_frame, text='Clear', command=self.clear_background) self.background_frame.grid(column=0, row=4, sticky=(tk.NSEW)) self.background_button.grid(column=0, row=0, sticky=(tk.EW, tk.S)) self.clear_background_button.grid(column=1, row=0, sticky=(tk.EW, tk.S)) # Ch1 setup widgets -------------------------------- self.Ch1_frame = ttk.Labelframe(self.spectroscopy_frame, text='Ch1 setup:', padding=(0, 5, 0, 15)) self.Ch1_frame.grid(column=0, row=5, sticky=(tk.EW)) self.Ch1_frame.columnconfigure(0, weight=1) self.Ch1_frame.columnconfigure(1, weight=1) self.Ch1_param1_var = tk.StringVar() self.Ch1_param1_var.set('Xsignal') self.Ch1_param2_var = tk.StringVar() self.Ch1_param2_var.set('Rbaseline') self.numer_label = ttk.Label(self.Ch1_frame, text='Numerator: ') self.numer_label.grid(column=0, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(self.Ch1_frame, text="X signal", variable=self.Ch1_param1_var, value='Xsignal', command=self.channel_param).grid(column=1, row=0, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(self.Ch1_frame, text="R signal", variable=self.Ch1_param1_var, value='Rsignal', command=self.channel_param).grid(column=2, row=0, sticky=(tk.E, tk.W, tk.S)) self.denom_label = ttk.Label(self.Ch1_frame, text='Denominator: ') self.denom_label.grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(self.Ch1_frame, text="R baseline", variable=self.Ch1_param2_var, value='Rbaseline', command=self.channel_param).grid(column=1, row=1, sticky=(tk.E, tk.W, tk.S)) ttk.Radiobutton(self.Ch1_frame, text="No denominator", variable=self.Ch1_param2_var, value='ND', command=self.channel_param).grid(column=2, row=1, sticky=(tk.E, tk.W, tk.S)) # Ch2 setup widgets -------------------------------- self.Ch2_frame = ttk.Labelframe(self.spectroscopy_frame, text='Ch2 setup:', padding=(0, 5, 0, 15)) self.Ch2_frame.grid(column=0, row=6, sticky=(tk.EW)) self.Ch2_frame.columnconfigure(0, weight=1) self.Ch2_frame.columnconfigure(1, weight=1) self.Ch2_param_var = tk.StringVar() self.Ch2_param_var.set('Rbaseline') ttk.Radiobutton(self.Ch2_frame, text="R baseline", variable=self.Ch2_param_var, value='Rbaseline', command=self.channel_param).grid(column=0, row=1, sticky=(tk.E, tk.W, tk.S)) def update_header(self): col0 = self.plot_format['xlabel'] col1 = 'Xc' col2 = 'Yc' col3 = 'Xsum' col4 = 'Ysum' col5 = 'Xdif' col6 = 'Ydif' self.header = str(col0) + '\t' + str(col1) + '\t' + str( col2) + '\t' + str(col3) + '\t' + str(col4) + '\t' + str( col5) + '\t' + str(col6) self.header.encode('utf-8') def create_menu_bar(self): """ Add elememnts to the master menubar """ # Hardware menu self.master.menu_hardware.add_command( label='Monochromator', command=lambda: self.monochromator.interface(self.master)) self.master.menu_hardware.entryconfig("Monochromator", state="disabled") self.master.menu_hardware.add_command( label='acquisition', command=lambda: self.acquisition.interface(self.master)) self.master.menu_hardware.entryconfig("acquisition", state="disabled") # Batch menu self.master.menu_batch.add_command(label='Disable', command=self.batch.disable) self.master.menu_batch.add_command( label='IV', command=lambda: self.new_batch('IV')) self.master.menu_batch.add_command( label='Temperature', command=lambda: self.new_batch('Temperature')) self.master.menu_batch.add_command( label='Time', command=lambda: self.new_batch('Time')) def new_batch(self, batch_mode): """ Shows current batch window or, if a different batch is chosen, destroys the old one and creates a new one for the new batch mpde :param batch_mode: the selected type of batch :return: None """ if self.batch.mode == batch_mode: self.batch.show() else: self.batch.quit() self.batch = Batch(self.master, self.dm, mode=batch_mode) def fill_devices(self): """ Fills the device selectors with the corresponding type of devices :return: """ self.monochromator_box['values'] = self.dm.get_devices( ['Monochromator']) self.monochromator_box.current(0) self.acquisition_box['values'] = self.dm.get_devices( ['Lock-In', 'Spectrometer']) self.acquisition_box.current(0) self.select_monochromator() self.select_acquisition() def select_monochromator(self, *args): if self.monochromator is not None: self.dm.close_device(self.monochromator) dev_name = self.mono_var.get() self.monochromator = self.dm.open_device(dev_name) if self.monochromator is None: self.monochromator_box.current(0) self.monochromator = self.dm.open_device(self.mono_var.get()) elif self.dm.current_config[dev_name]['Type'] == 'Monochromator': self.move = self.monochromator.move else: self.monochromator_box.current(0) self.monochromator = self.dm.open_device(self.mono_var.get()) # If the device has an interface to set options, we link it to the entry in the menu interface = getattr(self.monochromator, "interface", None) if callable(interface): self.master.menu_hardware.entryconfig("Monochromator", state="normal") else: self.master.menu_hardware.entryconfig("Monochromator", state="disabled") def select_acquisition(self, *args): """ When the acquisition selector changes, this function updates some variables and the graphical interface to adapt it to the selected device. :param args: Dummy variable that does nothing but must exist (?) :return: None """ if self.acquisition is not None: self.dm.close_device(self.acquisition) dev_name = self.acq_var.get() self.acquisition = self.dm.open_device(dev_name) if self.acquisition is None: self.acquisition_box.current(0) self.acquisition = self.dm.open_device(self.acq_var.get()) elif self.dm.current_config[dev_name]['Type'] == 'Spectrometer': self.move = self.null self.prepare_scan = self.prepare_scan_spectrometer self.get_next_datapoint = self.mode_spectrometer self.start_live = self.prepare_live_spectrometer self.live = self.live_spectrometer self.background_frame.grid(column=0, row=4, sticky=(tk.NSEW)) self.window_live_lbl.grid_forget() self.window_live_entry.grid_forget() self.monochromator_box['state'] = 'disabled' self.Step_entry['state'] = 'disabled' self.GoTo_button['state'] = 'disabled' self.GoTo_entry['state'] = 'disabled' elif self.dm.current_config[dev_name]['Type'] in [ 'Lock-In', 'Multimeter' ]: self.move = self.monochromator.move self.prepare_scan = self.prepare_scan_lockin self.get_next_datapoint = self.mode_lockin self.start_live = self.prepare_live_lockin self.live = self.live_lockin self.background_frame.grid_forget() self.window_live_lbl.grid(column=0, row=0, sticky=(tk.EW)) self.window_live_entry.grid(column=1, row=0, columnspan=2, sticky=(tk.EW)) self.monochromator_box['state'] = 'normal' self.Step_entry['state'] = 'normal' self.GoTo_button['state'] = 'normal' self.GoTo_entry['state'] = 'normal' else: self.acquisition_box.current(0) self.acquisition = self.dm.open_device(self.acq_var.get()) interface = getattr(self.acquisition, "interface", None) if callable(interface): self.master.menu_hardware.entryconfig("acquisition", state="normal") else: self.master.menu_hardware.entryconfig("acquisition", state="disabled") def channel_param(self): """ When the channel parameters are changed, this function updates some internal variables and the graphical interface :return: None """ ## Channel 1 labels ---------------------------------- if self.Ch1_param1_var.get() == 'Xsignal' and self.Ch1_param2_var.get( ) == 'Rbaseline': self.plot_format['Ch1_ylabel'] = 'Xsignal/Rbaseline' elif self.Ch1_param1_var.get( ) == 'Xsignal' and self.Ch1_param2_var.get() == 'ND': self.plot_format['Ch1_ylabel'] = 'Xsignal' elif self.Ch1_param1_var.get( ) == 'Rsignal' and self.Ch1_param2_var.get() == 'Rbaseline': self.plot_format['Ch1_ylabel'] = 'Rsignal/Rbaseline' elif self.Ch1_param1_var.get( ) == 'Rsignal' and self.Ch1_param2_var.get() == 'ND': self.plot_format['Ch1_ylabel'] = 'Rsignal' ## Channel 2 labels ---------------------------------- if self.Ch2_param_var.get() == 'Rbaseline': self.plot_format['Ch2_ylabel'] = 'Rbaseline' self.master.update_plot_axis(self.plot_format) def null(self, *args, **kwargs): """ Empty function that does nothing :return: None """ pass def start_stop_scan(self): """ Starts and stops an scan :return: None """ self.run_check() if self.stop: self.prepare_scan() else: self.stop = True self.finish_scan() def pause_scan(self): """ Pauses an scan or resumes the acquisition :return: None """ self.stop = not self.stop if self.stop: self.pause_button['text'] = 'Resume' else: self.pause_button['text'] = 'Pause' self.get_next_datapoint() def prepare_scan_lockin(self): """ Any scan is divided in three stages: 1) Prepare the conditions of the scan (this function), getting starting point, integration time and creating all relevant variables. 2) Runing the scan, performed by a recursive function "mode_spectrometer" or "mode_lockin" 3) Finish the scan, where we update some variables and save the data. :return: None """ self.update_integration_time() self.update_waiting_time() self.acquisition.open() # Get the scan conditions self.start_wl = max(float(self.Start_entry.get()), 250) self.stop_wl = max(min(float(self.Stop_entry.get()), 3000), self.start_wl + 1) step = min( max(float(self.Step_entry.get()), self.acquisition.min_wavelength), self.stop_wl - self.start_wl) # # If we are in a batch, we proceed to the next point if self.batch.ready: self.batch.batch_proceed() self.move(self.start_wl, speed='Fast') print('Starting scan...') # Create the record array self.size = int( np.ceil((self.stop_wl - self.start_wl + 0.5 * step) / step)) self.num = self.size self.record = np.zeros((self.size, 7)) ## Data to be saved self.record[:, 0] = np.arange(self.start_wl, self.stop_wl + 0.5 * step, step) self.record[:, 1] = self.record[:, 1] * np.NaN self.record[:, 2] = self.record[:, 1] * np.NaN self.record[:, 3] = self.record[:, 1] * np.NaN self.record[:, 4] = self.record[:, 1] * np.NaN self.plotdata = np.zeros((self.size, 3)) ## Data to be plotted self.plotdata[:, 0] = self.record[:, 0] self.plotdata[:, 1] = self.plotdata[:, 1] * np.NaN self.plotdata[:, 2] = self.plotdata[:, 1] * np.NaN self.master.prepare_meas(self.record) self.i = 0 self.scan_running() self.mode_lockin() def mode_lockin(self): """ Gets the next data point in a scan. This function depends on the acquisition device :return: None """ if not self.stop: Xc, Yc, Xsum, Ysum, Xdif, Ydif = self.acquisition.measure() ## Correct for RMS and half signals Xc = Xc * sqrt(2) Yc = Yc * sqrt(2) Xsum = Xsum * 2 * sqrt(2) Ysum = Ysum * 2 * sqrt(2) Xdif = Xdif * 2 * sqrt(2) Ydif = Ydif * 2 * sqrt(2) self.record[self.i, 1] = Xc self.record[self.i, 2] = Yc self.record[self.i, 3] = Xsum self.record[self.i, 4] = Ysum self.record[self.i, 5] = Xdif self.record[self.i, 6] = Ydif if self.plot_format['Ch1_ylabel'] == 'Xsignal/Rbaseline': self.plotdata[self.i, 1] = Xsum / sqrt(Xc**2 + Yc**2) elif self.plot_format['Ch1_ylabel'] == 'Rsignal/Rbaseline': self.plotdata[self.i, 1] = sqrt(Xsum**2 + Ysum**2) / sqrt(Xc**2 + Yc**2) elif self.plot_format['Ch1_ylabel'] == 'Xsignal': self.plotdata[self.i, 1] = Xsum elif self.plot_format['Ch1_ylabel'] == 'Rsignal': self.plotdata[self.i, 1] = sqrt(Xsum**2 + Ysum**2) else: self.plotdata[self.i, 1] = 0 ## Zero signal indicates an error print( 'The signal in Channel 1 is 0, possibly due to a communication Error' ) if self.plot_format['Ch2_ylabel'] == 'Rbaseline': self.plotdata[self.i, 2] = sqrt(Xc**2 + Yc**2) else: self.plotdata[self.i, 2] = 0 ## Zero signal indicates an error print( 'The signal in Channel 2 is 0, possibly due to a communication Error' ) self.master.update_plot(self.plotdata) if self.i < self.num - 1: self.i += 1 self.move(self.record[self.i, 0], speed='Fast') self.master.window.after( int(self.integration_time + self.waiting_time), self.mode_lockin) else: self.finish_scan() def prepare_scan_spectrometer(self): """ Any scan is divided in three stages: 1) Prepare the conditions of the scan (this function), getting starting point, integration time and creating all relevant variables. 2) Runing the scan, performed by a recursive function "mode_spectrometer" or "mode_lockin" 3) Finish the scan, where we update some variables and save the data. :return: None """ self.options_mono = { 'opt1': self.start_wl, } #### Edit as required self.update_integration_time() self.update_waiting_time() # We check the background is not none and offer to update it if self.background is None: meas_bg = messagebox.askyesno( message= 'There is no background spectrum for this integration time!', detail='Do you want to measure it now?', icon='question', title='Measure background?') self.get_background(meas_bg) # # If we are in a batch, we proceed to the next point if self.batch.ready: self.batch.batch_proceed() # If the integration time is too long, we have to split the acquisition in several steps, # otherwise the spectrometer hangs self.num = int( np.ceil(self.integration_time / self.acquisition.max_integration_time)) # Here we select the wavelength range we want to record and re-shape the record array # Get the scan conditions self.start_wl = max(float(self.Start_entry.get()), 300) self.stop_wl = max(min(float(self.Stop_entry.get()), 2000), self.start_wl + 1) wl = self.acquisition.measure()[0] self.idx = np.where((self.start_wl <= wl) & (wl <= self.stop_wl)) self.size = len(self.idx[0]) # Create the record array self.record = np.zeros((self.size, 3)) self.record[:, 0] = wl[self.idx] self.record[:, 2] = self.background[self.idx] self.master.prepare_meas(self.record) self.i = 0 self.scan_running() self.mode_spectrometer() def mode_spectrometer(self): """ Gets the whole spectrum at once recorded by the spectrometer in the range selected :return: None """ if not self.stop: data = self.acquisition.measure() intensity = data[1][self.idx] - self.background[self.idx] self.record[:, 1] = (intensity + self.i * self.record[:, 1]) / (self.i + 1.) self.master.update_plot(self.record) if self.i < self.num - 1: self.i = self.i + 1 self.master.window.after(int(self.integration_time / self.num), self.mode_spectrometer) else: self.finish_scan() def finish_scan(self): """ Finish the scan, updating some global variables, saving the data in the temp file and offering to save the data somewhere else. :return: None """ if self.batch.ready: self.master.finish_meas(self.record, finish=False) self.batch.batch_wrapup(self.record) else: self.master.finish_meas(self.record, finish=True) if self.stop or not self.batch.ready: # We reduce the number of counts in the batch to resume at the unfinished point if necessary # In other words, it repeats the last, un-finished measurement self.batch.count = max(self.batch.count - 1, 0) self.scan_stopped() else: self.prepare_scan() def check_inputs(self, parameter, value, min, max): """ Checks that the input parameters are within a specified range. :return: Error popup """ if value < min or value > max: messagebox.showerror("Error", parameter + " is out of range!") self.run_button['state'] = 'enabled' self.run_ok.append(False) else: self.run_ok.append(True) def run_check(self): self.Start_wave = float(self.Start_entry.get()) self.Stop_wave = float(self.Stop_entry.get()) self.Step_wave = float(self.Step_entry.get()) ## Check that inputs are within safe/correct limits self.run_ok = [] self.check_inputs('Start wavelength', self.Start_wave, 100, 2000) self.check_inputs('End wavelength', self.Stop_wave, 100, 2000) self.check_inputs('Step', self.Step_wave, 0.1, 2000) def scan_running(self): """ Updates the graphical interface, disable the buttoms that must be disabled during the measurement. :return: None """ self.scan_button['text'] = 'Stop' self.pause_button['state'] = 'enabled' self.record_live_button['state'] = 'disabled' self.GoTo_button['state'] = 'disabled' self.integration_time_button['state'] = 'disabled' self.waiting_time_button['state'] = 'disabled' self.background_button['state'] = 'disabled' self.clear_background_button['state'] = 'disabled' self.stop = False def scan_stopped(self): """ Returns the graphical interface to normal once the measurement has finished. :return: None """ self.scan_button['text'] = 'Run' self.pause_button['state'] = 'disabled' self.record_live_button['state'] = 'enabled' self.GoTo_button['state'] = 'enabled' self.integration_time_button['state'] = 'enabled' self.waiting_time_button['state'] = 'enabled' self.background_button['state'] = 'enabled' self.clear_background_button['state'] = 'enabled' self.stop = True def get_background(self, meas_bg=True): """ Gets the background when using the spectrometer, if requested. :parameter: meas_bg True or False: wether to measure a background or just produce a bg with zeros :return: None """ if meas_bg: self.update_integration_time() self.background = self.acquisition.measure()[1] messagebox.showinfo(message='Background taken!', detail='Press OK to continue.', title='Background taken!') else: self.background = self.acquisition.measure()[1] * 0.0 def clear_background(self): """ Clears the background when using the spectrometer. :return: None """ self.background = None def record_live(self): """ Starts and stops a live recording. :return: None """ if self.stop: self.stop = False self.record_live_button['text'] = 'Stop' self.pause_live_button['state'] = 'enabled' self.scan_button['state'] = 'disabled' self.background_button['state'] = 'disabled' self.clear_background_button['state'] = 'disabled' self.start_live() else: self.record_live_button['text'] = 'Record' self.pause_live_button['state'] = 'disabled' self.scan_button['state'] = 'enabled' self.background_button['state'] = 'enabled' self.clear_background_button['state'] = 'enabled' self.stop = True self.finish_live() def pause_live(self): """ Pauses a live recording or resumes the acquisition """ self.stop = not self.stop if self.stop: self.pause_live_button['text'] = 'Resume' else: self.pause_live_button['text'] = 'Pause' self.live() def prepare_live_lockin(self): """ Prepares the lock-in live acquisition and prepare some variables """ self.acquisition.open() self.goto() self.update_integration_time() self.window_points = int(self.window_live_entry.get()) self.live_data = np.zeros((self.window_points, 3)) self.live_data[:, 0] = np.arange(self.window_points) # Removes all plots, but not the data, and change the horizontal axis conditions self.master.clear_plot(xtitle='Time', ticks='off') self.master.prepare_meas(self.live_data) self.live_lockin() def live_lockin(self): """ Runs the live lock-in acquisition """ if not self.stop: self.live_data[:-1, 1] = self.live_data[1:, 1] self.live_data[:-1, 2] = self.live_data[1:, 2] Xc, Yc, Xsum, Ysum, Xdif, Ydif = self.acquisition.measure() ## Correct for RMS and half signals Xc = Xc * sqrt(2) Yc = Yc * sqrt(2) Xsum = Xsum * 2 * sqrt(2) Ysum = Ysum * 2 * sqrt(2) Xdif = Xdif * 2 * sqrt(2) Ydif = Ydif * 2 * sqrt(2) if self.plot_format['Ch1_ylabel'] == 'Xsignal/Rbaseline': self.live_data[-1, 1] = Xsum / sqrt(Xc**2 + Yc**2) elif self.plot_format['Ch1_ylabel'] == 'Rsignal/Rbaseline': self.live_data[-1, 1] = sqrt(Xsum**2 + Ysum**2) / sqrt(Xc**2 + Yc**2) elif self.plot_format['Ch1_ylabel'] == 'Xsignal': self.live_data[-1, 1] = Xsum elif self.plot_format['Ch1_ylabel'] == 'Rsignal': self.live_data[-1, 1] = sqrt(Xsum**2 + Ysum**2) else: self.live_data[-1, 1] = 0 ## Zero signal indicates an error print( 'The signal in Channel 1 is 0, possibly due to a communication Error' ) if self.plot_format['Ch2_ylabel'] == 'Rbaseline': self.live_data[-1, 2] = sqrt(Xc**2 + Yc**2) else: self.live_data[-1, 2] = 0 ## Zero signal indicates an error print( 'The signal in Channel 2 is 0, possibly due to a communication Error' ) self.master.update_plot(self.live_data) self.master.window.after(self.integration_time, self.live_lockin) def prepare_live_spectrometer(self): """ Prepares the spectrometer live acquisition and prepare some variables """ self.update_integration_time() if self.integration_time > self.acquisition.max_integration_time: self.integration_time = int(self.acquisition.max_integration_time) self.acquisition.update_integration_time(self.integration_time) data0, data1 = self.acquisition.measure() self.live_data = np.zeros((len(data0), 3)) self.live_data[:, 0] = data0 self.live_data[:, 1] = data1 # Removes all plots, but not the data, and change the horizontal axis conditions self.master.clear_plot(xtitle='Wavelength (nm)', ticks='on') self.master.prepare_meas(self.live_data) self.live_spectrometer() def live_spectrometer(self): """ Runs the live spectrometer acquisition """ if not self.stop: data0, data1 = self.acquisition.measure() self.live_data[:, 1] = data1 self.master.update_plot(self.live_data) self.master.window.after(self.integration_time, self.live_spectrometer) def finish_live(self): """ Finish the live acquisition, returning the front end to the scan mode """ self.master.replot_data(xtitle='Wavelength (nm)', ticks='on') def update_integration_time(self): """ Updates the integration time """ old_integration_time = self.integration_time self.integration_time = int(self.integration_time_entry.get()) if old_integration_time != self.integration_time: self.clear_background() self.integration_time = self.acquisition.update_integration_time( self.integration_time) self.integration_time_entry.delete(0, tk.END) self.integration_time_entry.insert(0, '%i' % self.integration_time) def update_waiting_time(self): """ Updates the waiting time """ self.waiting_time = int(self.waiting_time_entry.get()) def goto(self): """ Go to the specified wavelength """ wl = float(self.GoTo_entry.get()) self.move(wl, speed='Fast') print('Done! Wavelength = {} nm'.format(wl))