diff --git a/interfaces/photodiode_ivl.py b/interfaces/photodiode_ivl.py new file mode 100644 index 0000000000000000000000000000000000000000..787b21103e4110d95fb62eb0e438062070706ec7 --- /dev/null +++ b/interfaces/photodiode_ivl.py @@ -0,0 +1,406 @@ +""" +This is the photodiode measurement. Now the parser arguments are widgets +""" + +import sys +import os +import time +import datetime +import traceback + + +import numpy as np +import matplotlib.pyplot as plt + +try: + import ivl + from ivl.instruments import Agilent66332A + from ivl.measurements import ivl_scan_photodiode +except ModuleNotFoundError: + sys.path.append(".") + sys.path.append("..") + import ivl + from ivl.instruments import Agilent66332A + from ivl.measurements import ivl_scan_photodiode + +from PyQt6.QtWidgets import * +from PyQt6.QtGui import * +from PyQt6.QtCore import * + +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg,NavigationToolbar2QT as NavigationToolbar +from matplotlib.figure import Figure + +import tkinter as tk +from tkinter import filedialog +import tkinter.messagebox + + + +def create_file(filename): + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + file = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files","*.txt")],title = "save results path",initialfile =filename) + + #check if the file path is correct(.txt) + while file.endswith(".txt") == False: + #open again filedialog with error message box + tk.messagebox.showerror(message='invalid filename!') + file = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files","*.txt")],title = "save results path",initialfile =filename) + root.destroy() + return file + +def information_box(information): + #open dialog and hide the main window + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + #display meaagebox + tkinter.messagebox.showinfo(message=information) + root.destroy() + + +class MplCanvas(FigureCanvasQTAgg): + def __init__(self): + fig = Figure(figsize=(15,9),dpi=100) + self.axes = fig.subplots(nrows=2,ncols=2) + self.axes = self.axes.flatten() + + self.axes[0].set_xlabel("Voltage [$V$]") + self.axes[0].set_ylabel("Current Density [$mA/cm^2$]") + + self.axes[1].set_xlabel("Voltage [$V$]") + self.axes[1].set_ylabel("Radiance [$W/m^2*sr$]") + + self.axes[2].set_xlabel("Current Density [$mA/cm^2$]") + self.axes[2].set_ylabel("Radiance [$W/m^2*sr$]") + + self.axes[3].set_xlabel("Radiance [$W/m^2*sr$]") + self.axes[3].set_ylabel("EQE [%]") + + # set the scales + self.axes[2].set_xscale("log") + self.axes[3].set_xscale("log") + self.axes[2].set_yscale("log") + + self.axes[0].set_yscale("log") + self.axes[1].set_yscale("log") + + + super().__init__(fig) + + def clear_axes(self): + for ax in self.axes: + # remove line plots + for line in ax.get_lines(): + line.remove() + + #remove scatter plots + for scatter in ax.collections: + scatter.remove() + + ax.set_prop_cycle(None) # Reset colors + + +# define the mainwindow + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setupUI() + + def setupUI(self): + """ + Here we set the layout of the app + + V_start : First voltage point in [V] + V_stop : "Last voltage point in [V]", + V_step : Voltage sweep increament in [V] + + for the file we will create a Filedialog + the info can be added + + info: Information to add to the header of result file. + + points: Points to average for a single measurement. + (Reduce to reduce voltage on time. Reducing below + ~100 points does not yield increase measurement speed above ~ 50 ms + per data points.) + + offset: offset voltage of amplifier in [V]. If not supplied,offset is measured at the start of the script + + delay: Time in [s] to wait between measurement points. (Voltage is + set to 0 V during this time, mimicking a simple pulsed measurement) + + + address: Address of GPIB and serial interfaces + verbose: "Write info about current measurement state" True/False + + RFA: Feedback resistivity * configuration factor * pinhole area + spectrum: path to reference spectrum for calculation of Radio-photometric quantities + + substrate:"Active area in cm^2" + + We always display plots + """ + + self.widget = QWidget() # Abstract main widget + self.app_grid = QGridLayout() + + self.parameters = QVBoxLayout() + self.start_meas = QPushButton(text="Start Measurement") + self.load_spectrum = QPushButton(text = "Load spectrum reference file") + + # I will search for the acceptable range of values later + self.v_start = QDoubleSpinBox() + self.v_start.setDecimals(3) + self.v_start.setRange(-20.475,20.475) + self.v_start.setValue(-8) + + self.v_stop = QDoubleSpinBox() + self.v_stop.setDecimals(3) + self.v_stop.setRange(-20.475,20.475) + self.v_stop.setValue(2) + + self.v_step = QDoubleSpinBox() + self.v_step.setDecimals(3) + self.v_step.setRange(-2*20.475,2*20.475) + self.v_step.setValue(0.5) + + self.info = QPlainTextEdit() + + self.points = QSpinBox() + self.points.setRange(0,4096) + self.points.setValue(4096) + + + self.offset = QDoubleSpinBox() + self.offset.setDecimals(4) + self.offset.setRange(5e-3,10e-3) + self.offset.setValue(7.5e-3) + + self.measure_offset = QCheckBox("Measure offset voltage instead of using the above value") + + + self.delay = QDoubleSpinBox() #only softwrare delay leave it like this + # Minimum 0 Maximum 99.99 and 2 decimals places + + + self.address_serial = QSpinBox() # well this is also ok for the 0 to 99 just set the default value + self.address_serial.setValue(3) + + self.address_gpib = QSpinBox() + self.address_gpib.setValue(1) + + self.verbose = QCheckBox("Verbose") + + self.rfa = QDoubleSpinBox() # here also set the default value + self.rfa.setDecimals(6) + self.rfa.setValue(0.827575) + + self.spectrum = QLineEdit() + + self.substrate = QDoubleSpinBox() + self.substrate.setRange(0,1) + self.substrate.setValue(0.11) + self.clear_plots = QCheckBox("Clear Plots Before Measurement") + + + + self.parameters.addWidget(QLabel("First voltage point in [V]")) + self.parameters.addWidget(self.v_start) + self.parameters.addWidget(QLabel("Last voltage point in [V]")) + self.parameters.addWidget(self.v_stop) + self.parameters.addWidget(QLabel("Voltage sweep increament in [V]")) + self.parameters.addWidget(self.v_step) + + self.parameters.addWidget(QLabel("Information to add to the header of result file")) + self.parameters.addWidget(self.info) + self.parameters.addWidget(QLabel("Points to average for a single measurement")) + self.parameters.addWidget(self.points) + self.parameters.addWidget(QLabel("Offset voltage of amplifier in [V]")) + self.parameters.addWidget(self.offset) + self.parameters.addWidget(self.measure_offset) + self.parameters.addWidget(QLabel("Time in [s] to wait between measurement points")) + self.parameters.addWidget(self.delay) + + self.parameters.addWidget(QLabel("Serial address")) + self.parameters.addWidget(self.address_serial) + self.parameters.addWidget(QLabel("GPIB address")) + self.parameters.addWidget(self.address_gpib) + + self.parameters.addWidget(self.verbose) + self.parameters.addWidget(QLabel("RFA: Feedback resistivity * configuration factor * pinhole area")) + self.parameters.addWidget(self.rfa) + self.parameters.addWidget(QLabel("path to reference spectrum for calculation of Radio-photometric quantities")) + self.parameters.addWidget(self.spectrum) + self.parameters.addWidget(QLabel("Active area in cm^2")) + self.parameters.addWidget(self.substrate) + self.parameters.addWidget(self.clear_plots) + self.parameters.addWidget(self.load_spectrum) + self.parameters.addWidget(self.start_meas) + + + self.parameters_widget = QWidget() + self.parameters_widget.setLayout(self.parameters) # an abstract widget to hold the QVBoxLayout + + self.app_grid.addWidget(self.parameters_widget,0,0) + # add the canvas + # abstract widget for figure + self.figure_widget = QWidget() + + self.sc = MplCanvas() + self.figure_toolbar = NavigationToolbar(self.sc, self) + self.figure_widget_layout = QVBoxLayout() + self.figure_widget_layout.addWidget(self.figure_toolbar) + self.figure_widget_layout.addWidget(self.sc) + + self.figure_widget.setLayout(self.figure_widget_layout) + + self.app_grid.addWidget(self.figure_widget,0,1) + + self.setWindowTitle("Photodiode IVL Measurement") + self.widget.setLayout(self.app_grid) + self.setCentralWidget(self.widget) + self.showMaximized() + + self.start_meas.clicked.connect(self.start_meas_clicked) + self.load_spectrum.clicked.connect(self.load_spectrum_clicked) + + #initialize a counter to keep track of how many measurements are done + self.counter = 1 + + # use of threads + self.threadpool = QThreadPool() + + def start_meas_clicked(self): + self.worker = Worker(self) + self.threadpool.start(self.worker) + + def load_spectrum_clicked(self): + self.widget.setEnabled(False) + + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + + file = filedialog.askopenfilename(title ='Select spectrum reference file') + root.destroy() + self.spectrum.setText(file) + + self.widget.setEnabled(True) + + + + + + +class Worker(QRunnable): + def __init__(self,window:MainWindow): + self.window = window # Keep a referernce to the main window + super().__init__() + + @pyqtSlot() + def run(self): + self.window.widget.setEnabled(False) + + with( + Agilent66332A(address=str(self.window.address_gpib.value()),timeout=30) as agi_source, + Agilent66332A(connection = "serial", address = str(self.window.address_serial.value()),timeout=30) as agi_photo): + + # clear the plots + if self.window.clear_plots.isChecked(): + self.window.sc.clear_axes() + self.window.counter = 1 + else: + self.window.counter =+ 1 #increase counter + + + if agi_source.device is None or agi_photo.device is None: + if "IVL_OFFLINE" not in os.environ.keys(): + information_box("Could not open one of the two necessary sourcemeters!") + self.window.widget.setEnabled(True) + return + + agi_source.set_measurement_param(points=self.window.points.value()) + agi_photo.set_measurement_param(points=self.window.points.value()) + + agi_source.low_current_mode(True) + agi_photo.low_current_mode(True) + + if self.window.measure_offset.isChecked(): + dark_voltage = np.array([agi_photo.get_voltage() for _ in range(100)]) + offset = dark_voltage.mean() + rms = np.std(dark_voltage) + if self.window.verbose.isChecked(): + information_box(f"Ampl. Offset: {offset*1e3:.3f} mV\n"+ f"RMS: {rms*1e3:.3} mV\n") + self.window.offset.setValue(offset) # change the value on the interface + + scan_data = ivl_scan_photodiode( + self.window.v_start.value(),self.window.v_stop.value(),self.window.v_step.value(), + agi_source,agi_photo, + delay=self.window.delay.value(), verbose = self.window.verbose.isChecked(), + offset = self.window.offset.value(), substrate=self.window.substrate.value(), + ref_spectrum= self.window.spectrum.text(), + RFA= self.window.rfa.value() + ) + + + voltage = scan_data["voltage_source"] + current_dens = scan_data["current_density"] + radiance = scan_data["radiance"] + eqe = scan_data["eqe"] + luminance = scan_data["luminance"] + + # Plot the results + self.window.sc.axes[0].plot(voltage, current_dens,label = f"Measurement {self.window.counter}") + self.window.sc.axes[1].plot(voltage, radiance,label = f"Measurement {self.window.counter}") + self.window.sc.axes[2].scatter(current_dens,radiance,label = f"Measurement {self.window.counter}") + self.window.sc.axes[3].scatter(radiance,eqe,label = f"Measurement {self.window.counter}") + + for i in range(4): + self.window.sc.axes[i].legend() + + self.window.sc.draw_idle() + + filename = "_ivl.txt" + + header = "Voltage Current_Density Radiance Luminance EQE" + header += " Raw_Current Raw_Photovoltage\n" + header += "[V] [mA/cm^2] [W/m^2*sr] [cd/m^2] [%] [A] [V]\n" + + + info = str(datetime.datetime.now().date()) + + data = ( + voltage, current_dens, radiance, luminance, eqe, + scan_data["current"], scan_data["photo_voltage"] + ) + + data = np.array(data).T + + if self.window.info.toPlainText() != "": + info += ": "+ self.window.info.toPlainText() + + header += info + + data_path = create_file(filename) + np.savetxt(data_path, data, header = header, fmt='%.3e') + self.window.widget.setEnabled(True) + +app= QApplication(sys.argv) +w= MainWindow() +app.exec() + + + +