Source code for sEQE

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
V.1.0 Created from September 2018 onwards
@author: jungbluth

V.2.0 Created from August 2022 onwards 
@author: jungbluth, hanauske 

sEQE Control Code Script

This script allows the user to use the self-built sEQE setup of the AFMD group. It opens a PyQT5 GUI window which offers the user an interface to run the sEQE measurement. It is assumed that the user has read the online documentation at https://afmd.github.io/sEQE-Control-Software/. 

This tool needs no input to open the GUI.

This script requires that several packages to be installed within the Python
environment you are running this script in. To find out which read the requirements files in the github repository of the AFMD group.

This file is not intended to be imported as a module.

"""


# Standard python packages
import io
import itertools
import math
import os
import re
import sys
import time
import platform
import pathlib
import serial

# Standard scientific python packages
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import style
import pandas as pd
from numpy import *
from scipy.interpolate import interp1d

# AFMD modules
import GUI_template
from monochromator import Monochromator
from microscope.filterwheels.thorlabs import ThorlabsFilterWheel
from lockin import LockIn

# logging packages
import logging
import warnings

# for zurich instruments lock in amplifier:
import zhinst.utils
import zhinst.ziPython

# for the gui
from PyQt5 import QtCore, QtGui, QtWidgets
from tkinter import Tk
from tkinter import filedialog

# bit to unicode translator
import codecs


[docs] class MainWindow(QtWidgets.QMainWindow): """sEQE control software main window. Parameters ---------- QtWidgets.QMainWindow: The Qt5 GUI Interface Attributes: ----------- """ def __init__(self): """Constructor.""" # Initialising ports, device names and save path ------------------------------------------------------------------------ file = pathlib.Path("pathsNdevices_config.txt") if file.exists(): pNpdata = file.read_text().split(",") self.zurich_device = pNpdata[ 0 ] # zurich instruments lock-in amplifier device name self.filter_port = pNpdata[1] # second filter wheel serial port number self.mono_port = pNpdata[2] # monochromator serial port number self.save_path = pNpdata[3] # path to save data to print( f"Found the following details for setup in pathsNdevices.txt: \n zurich instrument device name: {self.zurich_device} \n second filter wheel port: {self.filter_port} \n monochromator port: {self.mono_port} \n default path where data are saved: {self.save_path}" ) for i in range(len(pNpdata)): if pNpdata[i] == "": print( "Empty string in pathsNdevices.txt found. The current file will be deleted, please recreate the file" ) file.unlink() # to delete file else: file.touch(exist_ok=False) if platform.system() == "Linux": port_prefix = "/dev/ttyUSB" elif platform.system() == "Windows": port_prefix = "COM" else: self.logger.error( "Operating System is not known - defaulting to Linux system" ) port_prefix = "/dev/tty" self.zurich_device = str( input( "Which zurich instrument device is used ? - type device address string e.g. UHF-DEV2000. " ) ) #'hf2-dev838' self.filter_port = port_prefix + str( input( "Which port number is used by the second filter wheel ? - type a number " ) ) # AFMD default 'COM4' self.mono_port = port_prefix + str( input( "Which port number is used by the monochromator ? - type a number " ) ) # AFMD default 'COM1' self.save_path = pathlib.Path( input( "Where do you want to save your data ? - copy absolute path of folder " ) ) # AFMD default 'C:\\Users\\Public\\Documents\\sEQE' file.write_text( f"{self.zurich_device},{self.filter_port},{self.mono_port},{self.save_path}" ) # Miscellaneous ------------------------------------------------------------------------------------ # Set up the user interface from Designer and Logger QtWidgets.QMainWindow.__init__(self) warnings.filterwarnings("ignore") # Reduce user distraction self.logger = self.get_logger() self.ui = GUI_template.Ui_MainWindow() self.ui.setupUi(self) # Connection default values - False to prevent MainWindow's methods being use before connecting tools self.mono_connected = False self.lockin_connected = False self.filter_connected = False # Initialize Monochromator and Lock-In Amplifier self.mono = Monochromator(self.mono_port) self.lockin = LockIn(self.zurich_device) # General lock-in amplifier setup self.channel = 1 self.c = str(self.channel - 1) self.c6 = str(6) # TODO: Is deprecated ? self.do_plot = True # Enable plotting during measurement self.complete_scan = False # Enable to stop measurement # these can not be defined here, due to empty text boxes at start up # self.userName = self.ui.user.text() # self.experimentName = self.ui.experiment.text() # self.path =f'{self.save_path}/{self.userName}/{self.experimentName}' self.filter_addition = "None" # TODO: Is deprecated ? # GUI button assignment ------------------------------------------------------------------------------------- # Handle Monochromator Buttons self.ui.connectButton_Mono.clicked.connect( self.connectToMono ) # Connect only to Monochromator self.ui.monoGotoButton.clicked.connect( self.MonoHandleWavelengthButton ) # Go to specific wavelength self.ui.monoSpeedButton.clicked.connect( self.MonoHandleSpeedButton ) # Set scan speed self.ui.monoGratingButton.clicked.connect( self.MonoHandleGratingButtons ) # Change grating self.ui.monoFilterButton.clicked.connect( self.MonoHandleFilterButton ) # Change filter self.ui.monoFilterInitButton.clicked.connect( self.MonoHandleFilterInitButton ) # Initialize filter # Handle Lock-in Buttons self.ui.connectButton_Lockin.clicked.connect( self.connectToLockin ) # Connect only to Lock-in self.ui.lockinParameterButton.clicked.connect( self.LockinHandleParameterButton ) # Set Lock-in parameters # Handle Filterwheel Buttons self.ui.connectButton_Filter.clicked.connect( self.connectToFilter ) # Connect only to Filterwheel # Handle Combined Buttons self.ui.connectButton.clicked.connect(self.connectToEquipment) self.ui.completeScanButton_start.clicked.connect( self.MonoHandleCompleteScanButton ) self.ui.completeScanButton_stop.clicked.connect( self.HandleStopCompleteScanButton ) # Handle Save and Import self.ui.save_to_file.clicked.connect( self.save_mono_parameter ) # Save measurement parameter to file self.ui.import_from_file.clicked.connect(self.load_mono_parameter) self.ui.importNamingButton.clicked.connect(self.load_naming) # Import photodiode calibration files Si_file = pd.ExcelFile( "FDS100-CAL.xlsx" ) # The files are in the sEQE Analysis folder # print(Si_file.sheet_names) self.Si_cal = Si_file.parse("Sheet1") # print(self.Si_cal) InGaAs_file = pd.ExcelFile("FGA21-CAL.xlsx") self.InGaAs_cal = InGaAs_file.parse("Sheet1") # Close connection to Monochromator and Thorlabs filter wheel when window is closed ------------------------- def __del__(self): try: self.thorfilterwheel.close() with serial.Serial(self.mono_port, 9600, timeout=0) as self.p: self.p.close() except: pass # ----------------------------------------------------------------------------------------------------------- #### Functions to connect to Monochromator and Lock-in # ----------------------------------------------------------------------------------------------------------- # Establish serial connection to Monochromator
[docs] def connectToMono(self): """Function to establish connection to monochromator. Returns ------- None """ try: self.mono_connected = self.mono.connect() if self.mono_connected: self.logger.info("Connection to Monochromator Established") self.ui.imageConnect_mono.setPixmap(QtGui.QPixmap("Button_on.png")) except Exception as err: self.logger.exception( "Unexpected error during execution of connectToMono function:" )
# Establish connection to LOCKIN
[docs] def connectToLockin(self): """Function to establish connection to Lockin. Returns ------- list Zurich Instruments localhost name and device details """ try: self.daq, self.device, self.lockin_connected = self.lockin.connect() self.ui.imageConnect_lockin.setPixmap(QtGui.QPixmap("Button_on.png")) return self.daq, self.device except Exception as err: self.logger.exception( "Unexpected error during execution of connectToLockin function:" )
# Establish connection to Filterwheel
[docs] def connectToFilter(self): """Function to establish connection to filter wheel. Returns ------- None """ try: self.thorfilterwheel = ThorlabsFilterWheel( com=self.filter_port ) # Initialize here = GUI openable without equipment physically connected if self.thorfilterwheel.position == 0: self.filter_connected = True self.logger.info("Connection to Thorlabs filter wheel established") self.ui.imageConnect_filter.setPixmap(QtGui.QPixmap("Button_on.png")) else: self.logger.exception( "Could not find the Thorlabs filter wheel in position 1, i.e. in open position. Please check current filter wheel position manually." ) self.filter_connected = False except Exception as err: self.logger.exception( "Unexpected error during execution of connectToFilter function:" )
# ----------------------------------------------------------------------------------------------------------- # Establish connection to all equipment
[docs] def connectToEquipment(self): """Function to establish connection to monochromator, Lockin & filter wheel. Returns ------- None """ try: self.connectToLockin() self.connectToMono() self.connectToFilter() self.ui.imageConnect.setPixmap(QtGui.QPixmap("Button_on.png")) except Exception as err: self.logger.exception( "Unexpected error during execution of connectToEquipment function:" )
# ----------------------------------------------------------------------------------------------------------- #### Functions to handle parameter buttons for Monochromator and Lock-in # ----------------------------------------------------------------------------------------------------------- ## Monochromator Functions # Set and GOTO wavelength
[docs] def MonoHandleWavelengthButton( self, ): # Function sets desired wavelength and calls chooseWavelength function """Function to read wavelength value from GUI. Returns ------- None """ wavelength = self.ui.pickNM.value() self.mono.chooseWavelength(wavelength)
# Update the scan speed
[docs] def MonoHandleSpeedButton( self, ): # Function sets desired scan speed and calls chooseScanSpeed function """Function to read monochromator speed from GUI. Returns ------- None """ speed = self.ui.pickScanSpeed.value() self.mono.chooseScanSpeed(speed)
# Set and move to grating
[docs] def MonoHandleGratingButtons( self, ): # Function sets desired grating number and calls chooseGrating function """Function to read grating number from monochromator. Returns ------- None """ if self.ui.Blaze_300.isChecked(): gratingNo = 1 elif self.ui.Blaze_750.isChecked(): gratingNo = 2 elif self.ui.Blaze_1600.isChecked(): gratingNo = 3 self.mono.chooseGrating(gratingNo)
# Update filter number
[docs] def MonoHandleFilterButton(self): """Function to read filter position from GUI. Returns ------- None """ filterNo = int(self.ui.pickFilter.value()) self.mono.chooseFilter(filterNo)
# Initialize filter
[docs] def MonoHandleFilterInitButton(self): """Function to read filter initialization position from GUI. Returns ------- None """ filterStart = self.ui.pickFilterInitStart.value() filterDiff = int(8 - filterStart) self.mono.initializeFilter(filterDiff) self.ui.imageInit_filterwheel.setPixmap(QtGui.QPixmap("Button_on.png")) self.logger.info("Monochromator Filter Wheel initialized")
# ----------------------------------------------------------------------------------------------------------- ## Lock-in Functions # Define and set Lock-in parameters
[docs] def LockinHandleParameterButton(self): """Function to read Lockin amplification value from GUI. Returns ------- None """ try: if self.lockin_connected: self.amplification = self.ui.pickAmp.value() self.LockinUpdateParameters(self.amplification) else: self.logger.info("Lock-In not connected") except Exception as err: self.logger.exception( "Unexpected error during execution of LockinHandleParametersButton function:" )
[docs] def LockinUpdateParameters( self, amplification ): # Function sets desired Lock-in parameters and calls setParameter function """Function to update Lockin parameters. Parameters ---------- amplification int, required amplification value of the LockIn signal Returns ------- None Raises ------ LoggerError Raises error if Lockin not connected or Exception handling """ try: if self.lockin_connected: self.c_2 = str( self.channel ) # Channel 2, with value 1, for the reference input self.tc = self.ui.pickTC.value() # Import value for time constant self.rate = ( self.ui.pickDTR.value() ) # Import value for data transfer rate self.lowpass = ( self.ui.pickLPFO.value() ) # Import value for low pass filter order self.range = 2 # This sets the default voltage range to 2 self.ac = 0 # AC off self.imp50 = 0 # 50 Ohm off self.imp50_2 = 1 # Turn on 50 Ohm on channel 2 to attenuate signal from chopper controller as reference signal self.diff = 1 # Diff off self.diff_2 = 0 # diff for channel 2 off # if self.ui.acButton.isChecked(): # AC on if button is checked # self.ac = 1 # if self.ui.imp50Button.isChecked(): # 50 Ohm on if button is checked # self.imp50 = 1 # if self.ui.diffButton.isChecked(): # Diff on if button is checked # self.diff = 1 # self.frequency = self.ui.pickFreq.value() # For manual frequency control. The frequency tab is currently not implemented in the GUI self.lockin.setParameters( self.diff_2, self.diff, self.imp50, self.imp50_2, self.ac, self.range, self.lowpass, self.rate, self.tc, self.c_2, amplification, ) self.logger.info("Updating Lock-In Settings") else: self.logger.error("Lock-In not connected") except Exception as err: self.logger.exception( "Unexpected error during execution of LockinUpdateParameters function:" )
# ----------------------------------------------------------------------------------------------------------- #### Functions to handle filter and grating changes # -----------------------------------------------------------------------------------------------------------
[docs] def monoCheckFilter(self, wavelength): # Filter switching points from GUI """ Function to update position of first filter wheel from GUI defaults. Parameters ---------- wavelength: float, required Current wavelength position of monochromator Returns ------- None Raises ------ LoggerError Raises error if filter wheel commands are invalid or monochromator not connected """ filterNo = self.mono.checkFilter() data_average_factor = self.ui.data_average_factor.value() startNM_F2 = int(self.ui.startNM_F2.value()) stopNM_F2 = int(self.ui.stopNM_F2.value()) startNM_F3 = int(self.ui.startNM_F3.value()) stopNM_F3 = int(self.ui.stopNM_F3.value()) startNM_F4 = int(self.ui.startNM_F4.value()) stopNM_F4 = int(self.ui.stopNM_F4.value()) startNM_F5 = int(self.ui.startNM_F5.value()) stopNM_F5 = int(self.ui.stopNM_F5.value()) if ( startNM_F2 <= wavelength < stopNM_F2 ): # Filter 3 [FESH0700]: from 350 - 649 -- including start, excluing end shouldbeFilterNo = 2 elif ( startNM_F3 <= wavelength < stopNM_F3 ): # Filter 3 [FESH0700]: from 350 - 649 -- including start, excluing end shouldbeFilterNo = 3 elif ( startNM_F4 <= wavelength < stopNM_F4 ): # Filter 4 [FESH1000]: from 650 - 984 -- including start, excluding end shouldbeFilterNo = 4 elif ( startNM_F5 <= wavelength <= stopNM_F5 ): # Filter 5 [FELH0950]: from 985 - 1800 -- including start, including end shouldbeFilterNo = 5 else: self.logger.error("Error: Filter Out Of Range") if shouldbeFilterNo != filterNo: self.mono.chooseFilter(shouldbeFilterNo) # Take data and discard it, this is required to avoid kinks # Poll data for data_average_factor * time constants, second parameter is poll timeout in [ms] (recomended value is 500ms) dataDict = self.daq.poll(data_average_factor * self.tc, 500) # Dictionary with ['timestamp']['x']['y']['frequency']['phase']['dio']['trigger']['auxin0']['auxin1']['time'] else: pass
[docs] def monoCheckGrating(self, wavelength): # Grating switching points from GUI """Function to update monochromator grating position from GUI defaults. Parameters ---------- wavelength: float, required Current wavelength position of monochromator Returns -------- None Raises ------ LoggerError Raises error if grating commands are invalid or monochromator not connected """ gratingNo = self.mono.checkGrating() data_average_factor = self.ui.data_average_factor.value() startNM_G1 = int(self.ui.startNM_G1.value()) stopNM_G1 = int(self.ui.stopNM_G1.value()) startNM_G2 = int(self.ui.startNM_G2.value()) stopNM_G2 = int(self.ui.stopNM_G2.value()) startNM_G3 = int(self.ui.startNM_G3.value()) stopNM_G3 = int(self.ui.stopNM_G3.value()) if ( startNM_G1 <= wavelength < stopNM_G1 ): # Grating 1: from 350 - 549 -- including start, excluding end shouldbeGratingNo = 1 elif ( startNM_G2 <= wavelength < stopNM_G2 ): # Grating 2: from 550 - 1299 -- including start, excluding end shouldbeGratingNo = 2 elif ( startNM_G3 <= wavelength <= stopNM_G3 ): # Grating 3: from 1300 - 1800 -- including start, including end shouldbeGratingNo = 3 else: # Do I need this? self.logger.error("Error: Grating Out Of Range") if shouldbeGratingNo != gratingNo: self.mono.chooseGrating(shouldbeGratingNo) # Take data and discard it, this is required to avoid kinks # Poll data for multiple of time constants, second parameter is poll timeout in [ms] (recomended value is 500ms) dataDict = self.daq.poll(data_average_factor * self.tc, 500) # Dictionary with ['timestamp']['x']['y']['frequency']['phase']['dio']['trigger']['auxin0']['auxin1']['time'] else: pass
# ----------------------------------------------------------------------------------------------------------- #### Function to handle filter changes of Thorlabs filter wheel # -----------------------------------------------------------------------------------------------------------
[docs] def thorChangeFilter(self, pos): """Function to update position of second filter wheel. Parameters ---------- pos: int, required Target filter position, between 1-6 Returns ------- bool True if connection to second filter wheel is successful, False otherwise Raises ------ LoggerError Raises error if second filter wheel not connected """ try: if not self.filter_connected: self.logger.exception("External Filter Wheel Not Connected") return False self.thorfilterwheel._do_set_position( pos - 1 ) # -1 due to microscope.thorfilterwheel code accepting only 0-5 self.logger.info(f"Thorlabs filterwheel moved to {pos}. position") return True except Exception as err: self.logger.exception( "Unexpected error during execution of thorChangeFilter function:" )
# ----------------------------------------------------------------------------------------------------------- #### Functions to handle measurement parameter and measurment itself # -----------------------------------------------------------------------------------------------------------
[docs] def MonoHandleCompleteScanButton(self): """Function to measure samples with different filters. Returns ------- None """ try: self.complete_scan = True self.ui.imageCompleteScan_start.setPixmap(QtGui.QPixmap("Button_on.png")) measurement_values = {} if self.ui.scan_noFilter.isChecked(): self.thorChangeFilter(1) if self.thorChangeFilter(1): self.filter_addition = "no" self.logger.info("Moving to Open Filter Position") start_f1 = self.ui.scan_startNM_1.value() stop_f1 = self.ui.scan_stopNM_1.value() step_f1 = self.ui.scan_stepNM_1.value() amp_f1 = self.ui.scan_pickAmp_1.value() measurement_values["f1"] = [start_f1, stop_f1, step_f1, amp_f1] self.amplification = amp_f1 self.LockinUpdateParameters(self.amplification) self.MonoHandleSpeedButton() scan_list = self.createScanJob(start_f1, stop_f1, step_f1) self.HandleMeasurement( scan_list, start_f1, stop_f1, step_f1, amp_f1, 3 ) if self.ui.scan_Filter2.isChecked(): self.thorChangeFilter(2) if self.thorChangeFilter(2): self.filter_addition = str(int(self.ui.cuton_filter_2.value())) self.logger.info("Moving to %s nm Filter" % self.filter_addition) start_f2 = self.ui.scan_startNM_2.value() stop_f2 = self.ui.scan_stopNM_2.value() step_f2 = self.ui.scan_stepNM_2.value() amp_f2 = self.ui.scan_pickAmp_2.value() measurement_values["f2"] = [start_f2, stop_f2, step_f2, amp_f2] self.amplification = amp_f2 self.LockinUpdateParameters(self.amplification) self.MonoHandleSpeedButton() scan_list = self.createScanJob(start_f2, stop_f2, step_f2) self.HandleMeasurement( scan_list, start_f2, stop_f2, step_f2, amp_f2, 3 ) if self.ui.scan_Filter3.isChecked(): self.thorChangeFilter(3) if self.thorChangeFilter(3): self.filter_addition = str(int(self.ui.cuton_filter_3.value())) self.logger.info("Moving to %s nm Filter" % self.filter_addition) start_f3 = self.ui.scan_startNM_3.value() stop_f3 = self.ui.scan_stopNM_3.value() step_f3 = self.ui.scan_stepNM_3.value() amp_f3 = self.ui.scan_pickAmp_3.value() measurement_values["f3"] = [start_f3, stop_f3, step_f3, amp_f3] self.amplification = amp_f3 self.LockinUpdateParameters(self.amplification) self.MonoHandleSpeedButton() scan_list = self.createScanJob(start_f3, stop_f3, step_f3) self.HandleMeasurement( scan_list, start_f3, stop_f3, step_f3, amp_f3, 3 ) if self.ui.scan_Filter4.isChecked(): self.thorChangeFilter(4) if self.thorChangeFilter(4): self.filter_addition = str(int(self.ui.cuton_filter_4.value())) self.logger.info("Moving to %s nm Filter" % self.filter_addition) start_f4 = self.ui.scan_startNM_4.value() stop_f4 = self.ui.scan_stopNM_4.value() step_f4 = self.ui.scan_stepNM_4.value() amp_f4 = self.ui.scan_pickAmp_4.value() measurement_values["f4"] = [start_f4, stop_f4, step_f4, amp_f4] self.amplification = amp_f4 self.LockinUpdateParameters(self.amplification) self.MonoHandleSpeedButton() scan_list = self.createScanJob(start_f4, stop_f4, step_f4) self.HandleMeasurement( scan_list, start_f4, stop_f4, step_f4, amp_f4, 3 ) if self.ui.scan_Filter5.isChecked(): self.thorChangeFilter(5) if self.thorChangeFilter(5): self.filter_addition = str(int(self.ui.cuton_filter_5.value())) self.logger.info("Moving to %s nm Filter" % self.filter_addition) start_f5 = self.ui.scan_startNM_5.value() stop_f5 = self.ui.scan_stopNM_5.value() step_f5 = self.ui.scan_stepNM_5.value() amp_f5 = self.ui.scan_pickAmp_5.value() measurement_values["f5"] = [start_f5, stop_f5, step_f5, amp_f5] self.amplification = amp_f5 self.LockinUpdateParameters(self.amplification) self.MonoHandleSpeedButton() scan_list = self.createScanJob(start_f5, stop_f5, step_f5) self.HandleMeasurement( scan_list, start_f5, stop_f5, step_f5, amp_f5, 3 ) if self.ui.scan_Filter6.isChecked(): self.thorChangeFilter(6) if self.thorChangeFilter(6): self.filter_addition = str(int(self.ui.cuton_filter_6.value())) self.logger.info("Moving to %s nm Filter" % self.filter_addition) start_f6 = self.ui.scan_startNM_6.value() stop_f6 = self.ui.scan_stopNM_6.value() step_f6 = self.ui.scan_stepNM_6.value() amp_f6 = self.ui.scan_pickAmp_6.value() measurement_values["f6"] = [start_f6, stop_f6, step_f6, amp_f6] self.amplification = amp_f6 self.LockinUpdateParameters(self.amplification) self.MonoHandleSpeedButton() scan_list = self.createScanJob(start_f6, stop_f6, step_f6) self.HandleMeasurement( scan_list, start_f6, stop_f6, step_f6, amp_f6, 3 ) self.thorChangeFilter(1) self.logger.info("Moving to open filter") self.mono.chooseFilter(1) self.complete_scan = False self.ui.imageCompleteScan_start.setPixmap(QtGui.QPixmap("Button_off.png")) self.ui.imageCompleteScan_stop.setPixmap(QtGui.QPixmap("Button_off.png")) self.logger.info("Finished Measurement") measurement_parameter = pd.DataFrame.from_dict(measurement_values) # self.save(measurement_values) except Exception as err: self.logger.exception( "Unexpected error during execution of MonoHandleCompleteScanButton function:" )
[docs] def load_naming(self): """Function to load naming from directory path Parameters ---------- None Returns ------- None """ try: root = Tk() # Creates master window for tkinters filedialog window root.withdraw() # Hides master window filepath = ( filedialog.askdirectory() ) # Creates pop-up window to ask for file save names = filepath.split("/") self.ui.user.setText(names[5]) self.ui.experiment.setText(names[6]) except Exception as err: self.logger.exception( "Unexpected error during execution of load_naming function:" )
[docs] def save_mono_parameter(self): """Function to save monochromator measurement parameters to file Parameters ---------- None Returns ------- None Raises ------ LoggerWarning Raises warning if tkinter saving dialog was closed without entering filename Notes ----- Reads the spinbox values and saves them into a file selected via tkinter dialog """ try: measurement_values = {} if self.ui.scan_noFilter.isChecked(): start_f1 = self.ui.scan_startNM_1.value() stop_f1 = self.ui.scan_stopNM_1.value() step_f1 = self.ui.scan_stepNM_1.value() amp_f1 = self.ui.scan_pickAmp_1.value() measurement_values["f1"] = [start_f1, stop_f1, step_f1, amp_f1] if self.ui.scan_Filter2.isChecked(): start_f2 = self.ui.scan_startNM_2.value() stop_f2 = self.ui.scan_stopNM_2.value() step_f2 = self.ui.scan_stepNM_2.value() amp_f2 = self.ui.scan_pickAmp_2.value() measurement_values["f2"] = [start_f2, stop_f2, step_f2, amp_f2] if self.ui.scan_Filter3.isChecked(): start_f3 = self.ui.scan_startNM_3.value() stop_f3 = self.ui.scan_stopNM_3.value() step_f3 = self.ui.scan_stepNM_3.value() amp_f3 = self.ui.scan_pickAmp_3.value() measurement_values["f3"] = [start_f3, stop_f3, step_f3, amp_f3] if self.ui.scan_Filter4.isChecked(): start_f4 = self.ui.scan_startNM_4.value() stop_f4 = self.ui.scan_stopNM_4.value() step_f4 = self.ui.scan_stepNM_4.value() amp_f4 = self.ui.scan_pickAmp_4.value() measurement_values["f4"] = [start_f4, stop_f4, step_f4, amp_f4] if self.ui.scan_Filter5.isChecked(): start_f5 = self.ui.scan_startNM_5.value() stop_f5 = self.ui.scan_stopNM_5.value() step_f5 = self.ui.scan_stepNM_5.value() amp_f5 = self.ui.scan_pickAmp_5.value() measurement_values["f5"] = [start_f5, stop_f5, step_f5, amp_f5] if self.ui.scan_Filter6.isChecked(): start_f6 = self.ui.scan_startNM_6.value() stop_f6 = self.ui.scan_stopNM_6.value() step_f6 = self.ui.scan_stepNM_6.value() amp_f6 = self.ui.scan_pickAmp_6.value() measurement_values["f6"] = [start_f6, stop_f6, step_f6, amp_f6] measurement_parameter = pd.DataFrame.from_dict(measurement_values) root = Tk() # Creates master window for tkinters filedialog window root.withdraw() # Hides master window filepath = ( filedialog.asksaveasfilename() ) # Creates pop-up window to ask for file save # This somehow destroys the Qt event manager, error message: # "QCoreApplication::exec: The event loop is already running" # self.filename = str(input('What is the name of this measurement routine ?: ')) # if self.path.exists() not: # os. measurement_parameter.to_csv(filepath, index=False) self.logger.info("Saved measurement parameter into: " + filepath) except FileNotFoundError: self.logger.warning("No parameters were safed due to missing filename") except Exception as err: self.logger.exception( "Unexpected error during execution of save_mono_parameter function:" )
[docs] def load_mono_parameter(self): """Function to load monochromator measurement parameters from file. Parameters ---------- None Returns ------- None Notes ----- Selects a file via tkinter dialog, reads the values and sets measurement parameters """ try: root = Tk() # Creates master window for tkinters filedialog window root.withdraw() # Hides master window filepath = ( filedialog.askopenfilename() ) # Creates pop-up window to ask for file save measurement_parameters = pd.read_csv(filepath) if self.ui.scan_noFilter.isChecked(): self.ui.scan_startNM_1.setValue(measurement_parameters["f1"][0]) self.ui.scan_stopNM_1.setValue(measurement_parameters["f1"][1]) self.ui.scan_stepNM_1.setValue(measurement_parameters["f1"][2]) self.ui.scan_pickAmp_1.setValue(measurement_parameters["f1"][3]) if self.ui.scan_Filter2.isChecked(): self.ui.scan_startNM_2.setValue(measurement_parameters["f2"][0]) self.ui.scan_stopNM_2.setValue(measurement_parameters["f2"][1]) self.ui.scan_stepNM_2.setValue(measurement_parameters["f2"][2]) self.ui.scan_pickAmp_2.setValue(measurement_parameters["f2"][3]) if self.ui.scan_Filter3.isChecked(): self.ui.scan_startNM_3.setValue(measurement_parameters["f3"][0]) self.ui.scan_stopNM_3.setValue(measurement_parameters["f3"][1]) self.ui.scan_stepNM_3.setValue(measurement_parameters["f3"][2]) self.ui.scan_pickAmp_3.setValue(measurement_parameters["f3"][3]) if self.ui.scan_Filter4.isChecked(): self.ui.scan_startNM_4.setValue(measurement_parameters["f4"][0]) self.ui.scan_stopNM_4.setValue(measurement_parameters["f4"][1]) self.ui.scan_stepNM_4.setValue(measurement_parameters["f4"][2]) self.ui.scan_pickAmp_4.setValue(measurement_parameters["f4"][3]) if self.ui.scan_Filter5.isChecked(): self.ui.scan_startNM_5.setValue(measurement_parameters["f5"][0]) self.ui.scan_stopNM_5.setValue(measurement_parameters["f5"][1]) self.ui.scan_stepNM_5.setValue(measurement_parameters["f5"][2]) self.ui.scan_pickAmp_5.setValue(measurement_parameters["f5"][3]) if self.ui.scan_Filter6.isChecked(): self.ui.scan_startNM_6.setValue(measurement_parameters["f6"][0]) self.ui.scan_stopNM_6.setValue(measurement_parameters["f6"][1]) self.ui.scan_stepNM_6.setValue(measurement_parameters["f6"][2]) self.ui.scan_pickAmp_6.setValue(measurement_parameters["f6"][3]) except Exception as err: self.logger.exception( "Unexpected error during execution of load_mono_parameter function:" )
# General function to create scanning list
[docs] def createScanJob(self, start, stop, step): """Function to compile scan parameters. Parameters ---------- start: float, required Wavelength start value stop: float, required Wavelength stop value step: float, required Wavelength step value Returns ------- List List of integer wavelength values """ scan_list = [] number = int((stop - start) / step) for n in range(-1, number + 1): # -1 to start from before the beginning, +1 to include the last iteration of 'number', [and +2 to go above stop (this can # be changed later]) wavelength = start + n * step scan_list.append(wavelength) return scan_list
# ----------------------------------------------------------------------------------------------------------- #### Functions to handle measurement # ----------------------------------------------------------------------------------------------------------- # Measure LOCKIN response
[docs] def HandleMeasurement(self, scan_list, start, stop, step, amp, number): """Function to prepare sample measurement. Parameters ---------- scan_list: list of ints, required List of wavelength values to scan start: float, required Wavelength start value stop: float, required Wavelength stop value step: float, required Wavelength step value amp: float, required Pre-amplifier amplification value number: int, required Specifier to decide if power value is calculated (1) or not (0) Returns ------- None """ if self.mono_connected and self.lockin_connected and self.filter_connected: # Assign user, expriment and file name for current measurement userName = self.ui.user.text() experimentName = self.ui.experiment.text() start_no = str(int(start)) stop_no = str(int(stop)) step_no = str(int(step)) amp_no = str(int(amp)) if number == 1: # name = 'Si_ref_diode' name = self.ui.file.text() if number == 2: # name = 'InGaAs_ref_diode' name = self.ui.file.text() if number == 3: name = self.ui.file.text() if not self.complete_scan: # If not a complete scan is taken fileName = ( name + "_(" + start_no + "-" + stop_no + "nm_" + step_no + "nm_" + amp_no + "x)" ) elif self.complete_scan: fileName = ( name + "_" + self.filter_addition + "Filter" + "_(" + start_no + "-" + stop_no + "nm_" + step_no + "nm_" + amp_no + "x)" ) # Set up path to save data self.path = f"{self.save_path}/{userName}/{experimentName}" self.logger.info(f"Saving data to: {self.path}") if not os.path.exists(self.path): os.makedirs(self.path) else: pass self.naming( fileName, self.path, 2 ) # This function defines a variable called self.file_name self.measure(scan_list, number)
[docs] def measure(self, scan_list, number): """Function to perform sample measurement. Parameters ---------- scan_list: list of ints, required List of wavelength values to scan number: int, required Specifier to decide if power value is calculated (1) or not (0) Returns ------- None """ # columns = ['Wavelength', 'Mean Current', 'Amplification', 'Mean R', 'Log Mean R', 'Mean RMS', 'Mean X', 'Mean Y', 'Mean Frequency', 'Mean Phase'] columns = [ "Wavelength", "Mean Current", "Amplification", "Mean R", "Mean Frequency", "Mean Phase", ] self.measuring = True self.ui.imageCompleteScan_stop.setPixmap(QtGui.QPixmap("Button_off.png")) data_average_factor = self.ui.data_average_factor.value() # Set up plot style if self.do_plot: # plt.close() self.set_up_plot() time.sleep(1) # Set up empty lists for measurements plot_list_x = [] plot_list_y = [] plot_log_list_y = [] plot_list_phase = [] data_list = [] data_df = pd.DataFrame(data_list, columns=columns) # Subscribe to scope self.path0 = "/" + self.device + "/demods/", self.c, "/sample" self.daq.subscribe(self.path0) count = 0 # self.chooseFilter(2) while len(scan_list) > 0: if self.measuring: wavelength = scan_list[0] self.monoCheckFilter(wavelength) self.monoCheckGrating(wavelength) self.mono.chooseWavelength(wavelength) # Poll data for multiple of time constants, second parameter is poll timeout in [ms] (recomended value is 500ms) dataDict = self.daq.poll( data_average_factor * self.tc, 500 ) # Dictionary with ['timestamp']['x']['y']['frequency']['phase']['dio']['trigger']['auxin0']['auxin1']['time'] # print(dataDict[self.device]['demods'][self.c]['sample']['timestamp']) # Recreate data if self.device in dataDict: if dataDict[self.device]["demods"][self.c]["sample"]["time"][ "dataloss" ]: self.logger.info("Sample Loss Detected") else: if ( count > 0 ): # Cut off the first measurement before the start to cut off the initial spike in the spectrum # if self.imp50==0: #### FIX THIS TO HANDLE IMP 50 # e = amp_coeff*amplitude/sqrt(2) # elif self.imp50==1: # If 50 Ohm impedance is enabled, the signal is cut in half # e = 0.5*amp_coeff*amplitude/sqrt(2) data = dataDict[self.device]["demods"][self.c]["sample"] rdata = sqrt(data["x"] ** 2 + data["y"] ** 2) rms = sqrt(0.5 * (data["x"] ** 2 + data["y"] ** 2)) current = rdata / self.amplification mean_curr = mean(current) mean_r = mean(rdata) log_mean_r = log(mean_r) mean_rms = mean(rms) mean_x = mean(data["x"]) mean_y = mean(data["y"]) mean_freq = mean(data["frequency"]) mean_phase = mean(data["phase"]) # scanValues = [wavelength, mean_curr, self.amplification, mean_r, log_mean_r, mean_rms, mean_x, mean_y, mean_freq, mean_phase] scanValues = [ wavelength, mean_curr, self.amplification, mean_r, mean_freq, mean_phase, ] plot_list_x.append(wavelength) plot_list_y.append(mean_r) plot_log_list_y.append(log_mean_r) plot_list_phase.append(mean_phase) data_list.append(scanValues) data_df = pd.DataFrame(data_list, columns=columns) if number == 1: self.calculatePower(data_df, self.Si_cal) elif number == 2: self.calculatePower(data_df, self.InGaAs_cal) else: #### CHECK THAT THIS WORKS!!! pass data_file = data_df.to_csv( os.path.join(self.path, self.file_name) ) if self.do_plot: self.ax1.plot(plot_list_x, plot_list_y, color="#000000") self.ax2.plot( plot_list_x, plot_log_list_y, color="#000000" ) self.ax3.plot( plot_list_x, plot_list_phase, color="#000000" ) self.pause(0.1) del scan_list[0] count += 1 else: self.ui.imageCompleteScan_stop.setPixmap(QtGui.QPixmap("Button_on.png")) break # Unsubscribe to scope self.daq.unsubscribe(self.path0)
# ----------------------------------------------------------------------------------------------------------- # Function to calculate the reference power
[docs] def calculatePower(self, ref_df, cal_df): """Function to calculate power. Parameters ---------- ref_df: DataFrame, required DataFrame of reference measurements cal_df: DataFrame, required DataFrame of reference calibration measurements Returns ------- DataFrame DataFrame of reference diode measurements incl. power """ cal_wave_dict = {} # Create an empty dictionary power = [] # Create an empty list for x in range( len(cal_df["Wavelength [nm]"]) ): # Iterate through columns of calibration file cal_wave_dict[cal_df["Wavelength [nm]"][x]] = cal_df["Responsivity [A/W]"][ x ] # Add wavelength and corresponding responsivity to dictionary for y in range( len(ref_df["Wavelength"]) ): # Iterate through columns of reference file # print(ref_df['Wavelength'][y]) if ( ref_df["Wavelength"][y] in cal_wave_dict.keys() ): # Check if reference wavelength is in calibraton file power.append( float(ref_df["Mean Current"][y]) / float(cal_wave_dict[ref_df["Wavelength"][y]]) ) # Add power to the list else: # If reference wavelength is not in calibration file resp_int = self.interpolate( ref_df["Wavelength"][y], cal_df["Wavelength [nm]"], cal_df["Responsivity [A/W]"], ) # Interpolate responsivity power.append( float(ref_df["Mean Current"][y]) / float(resp_int) ) # Add power to the list ref_df["Power"] = power # Create new column in reference file return ref_df["Power"]
# Function to interpolate values
[docs] def interpolate(self, num, x, y): f = interp1d(x, y) return f(num)
# -----------------------------------------------------------------------------------------------------------
[docs] def set_up_plot(self): """Function to set up plot. Returns ------- None """ style.use("ggplot") fig1 = plt.figure() self.ax1 = fig1.add_subplot(3, 1, 1) # plt.xlabel('Time (s)', fontsize=17, fontweight='medium') plt.ylabel("R component (V)", fontsize=17, fontweight="medium") plt.grid(True) # plt.box() plt.title("Demodulator data", fontsize=17, fontweight="medium") plt.tick_params(labelsize=14) plt.minorticks_on() plt.rcParams["figure.facecolor"] = "white" plt.rcParams["figure.edgecolor"] = "white" plt.tick_params( labelsize=15, direction="in", axis="both", which="major", length=8, width=2 ) plt.tick_params( labelsize=15, direction="in", axis="both", which="minor", length=4, width=2 ) self.ax2 = fig1.add_subplot(3, 1, 2) # plt.xlabel('Time (s)', fontsize=17, fontweight='medium') plt.ylabel("Log(R)", fontsize=17, fontweight="medium") plt.grid(True) # plt.box() plt.tick_params(labelsize=14) plt.minorticks_on() plt.rcParams["figure.facecolor"] = "white" plt.rcParams["figure.edgecolor"] = "white" plt.tick_params( labelsize=15, direction="in", axis="both", which="major", length=8, width=2 ) plt.tick_params( labelsize=15, direction="in", axis="both", which="minor", length=4, width=2 ) self.ax3 = fig1.add_subplot(3, 1, 3) plt.xlabel("Wavelength [nm]", fontsize=17, fontweight="medium") plt.ylabel("Phase", fontsize=17, fontweight="medium") plt.grid(True) # plt.box() # plt.title('Demodulator data', fontsize=17, fontweight='medium') plt.tick_params(labelsize=14) plt.minorticks_on() plt.rcParams["figure.facecolor"] = "white" plt.rcParams["figure.edgecolor"] = "white" plt.tick_params( labelsize=15, direction="in", axis="both", which="major", length=8, width=2 ) plt.tick_params( labelsize=15, direction="in", axis="both", which="minor", length=4, width=2 ) plt.show() return fig1
[docs] def pause(self, interval): """Function to pause matplotlib without plt.show() Parameters ---------- interval: int, required Pause time Returns ------- None Notes ----- This is a reimplementation of the matplotlib pause function, removing the final show() call. It prevents the matplotlib plot window to pop up after each measurement. The matplotlib 3.6 docstring says: "Run the GUI event loop for *interval* seconds. If there is an active figure, it will be updated and displayed before the pause, and the GUI event loop (if any) will run during the pause. This can be used for crude animation. For more complex animation use :mod:`matplotlib.animation`. If there is no active figure, sleep for *interval* seconds instead. See Also -------- matplotlib.animation : Proper animations show : Show all figures and optional block until all figures are closed." """ manager = matplotlib._pylab_helpers.Gcf.get_active() if manager is not None: canvas = manager.canvas if canvas.figure.stale: canvas.draw_idle() # show(block=False) canvas.start_event_loop(interval) else: time.sleep(interval)
# -----------------------------------------------------------------------------------------------------------
[docs] def naming(self, file_name, path_name, num): """Function to compile filename. Parameters ---------- file_name: str, required File name path_name: str, required Path name num: str, required Filter number Returns ------- None """ name = os.path.join(path_name, file_name) exists = os.path.exists(name) if exists: if num == 2: filename = file_name + "_%d" % num else: filename = file_name[:-1] + str(num) num += 1 self.naming(filename, path_name, num) else: self.file_name = file_name
# -----------------------------------------------------------------------------------------------------------
[docs] def HandleStopCompleteScanButton(self): """Function to stop multi-filter measurement. Returns ------- None """ self.measuring = False self.ui.imageCompleteScan_stop.setPixmap(QtGui.QPixmap("Button_on.png")) return self.measuring
# -----------------------------------------------------------------------------------------------------------
[docs] def get_logger(self): """Function to set up logger. Returns ------- logger """ logger = logging.getLogger() logger.setLevel(logging.DEBUG) formatter = logging.Formatter( fmt="%(asctime)s %(levelname)s %(name)s: %(message)s", datefmt="%Y-%m-%d - %H:%M:%S", ) console = logging.StreamHandler(sys.stdout) console.setLevel(logging.DEBUG) console.setFormatter(formatter) # logfile = logging.FileHandler('run.log', 'w') # logfile.setLevel(logging.DEBUG) # logfile.setFormatter(formatter) logger.addHandler(console) # logger.addHandler(logfile) return logger
# -----------------------------------------------------------------------------------------------------------
[docs] def main(): try: app = QtWidgets.QApplication(sys.argv) monoUI = MainWindow() monoUI.show() sys.exit(app.exec_()) except Exception as error: logging.exception("Unexpected error during main function: ")
if __name__ == "__main__": main()