"""
run this before using this library:
ipython -i prctrl_interactive.py
"""
import numpy as np
import matplotlib.pyplot as plt
import yaml
import time
# some imports are not needed here, but are convenient to have in the shell
from datetime import datetime as dtime
import os
from os import path, makedirs
import shutil
import threading as mt
import multiprocessing as multiprocessing
import argparse
import sys
import logging
log = logging.getLogger(__name__)
# logging.basicConfig(level=logging.INFO)
if __name__ == "__main__":
if __package__ is None:
# make relative imports work as described here: https://peps.python.org/pep-0366/#proposed-change
__package__ = "prsctrl"
from os import path
filepath = path.realpath(path.abspath(__file__))
# use local version of prsctrl
sys.path.insert(0, path.dirname(path.dirname(filepath)))
# use local version of devctrl
sys.path.insert(0, r"C:\Users\Administrator\Desktop\Software\Python\Python\github\photoreflectance\devctrl")
sys.path.insert(0, os.path.expanduser("~/Projekte/science/py-devctrl"))
sys.path.append(r"C:\Users\Administrator\Desktop\Software\Python\Python\github\photoreflectance") # contains cleware switch
# import device modules
from devctrl.devices import lock_in as mod_lock_in
from devctrl.devices.device_info import DeviceInfo
from devctrl.devices.device_manager import DeviceManager
from devctrl.devices.device_select import select_device_interactive
# import base classes
from devctrl.devices.lock_in.impl.sr830 import SR830
from devctrl.devices import *
from devctrl.devices.device import DummyDevice
from devctrl.utility.config_file import ConfigFile
from devctrl.utility.time import duration_to_string
from devctrl.utility.file_io import sanitize_filename, get_date_for_filename, get_date_time_for_filename, get_next_filename, listdir_abs
from devctrl.utility.monitor import Monitor
from devctrl.utility.log import ColorFormatter, PackageFilter
from .measurement.measurement import MeasurementHandler
from .measurement.settings import PrsSettings, ModeSettings, MeasurementSettings, LedSettings, LaserSettings, \
MonochromatorSettings, AmplifierSettings, LockInSettings
from .data.prsdata import PrsData, REFLECTION, TRANSMISSION, ABSORPTION, PARTIAL_PREFIX
from .data.calc import calc_absorption_data
from .data.plot import plot_data, plot_spectrum, plot_spectrum_on_ax, DEFAULT_COLOR
from . import data as prsdata
from . import scripts
from .version import version
from .data.functions import nm_to_eV, eV_to_nm
from . import devices
# SETUP INFORMATION
# setup_info = {
# "version": "3.5",
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 55,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 50,
# "tra_with_quartz_mV": None,
# }
# setup_info = {
# "version": "5.0",
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 65.0,
# "tra_with_quartz_mV": 762.0,
# "filter_ref": "405 nm LP",
# # "filter_tra": "375 nm LP",
# # "filter_ref": "No filter",
# "filter_tra": "No filter",
# }
# setup_info = {
# "version": "5.1",
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 46.0,
# "tra_with_quartz_mV": 630.0,
# "filter_ref": "405 nm LP",
# "filter_tra": "405 nm LP",
# "notes": "Samples mounted in filter wheel",
# # "filter_ref": "No filter",
# # "filter_tra": "No filter",
# }
# setup_info = {
# "version": "5.2",
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 60.0,
# "tra_with_quartz_mV": 720.0,
# "filter_ref": "405 nm LP",
# # "filter_tra": "375 nm LP",
# # "filter_ref": "No filter",
# "filter_tra": "405 nm LP",
# }
# setup_info = {
# "version": "5.3",
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 60.0,
# "tra_with_quartz_mV": 720.0,
# "filter_ref": "600 nm SP",
# # "filter_tra": "375 nm LP",
# # "filter_ref": "No filter",
# "filter_tra": "600 nm SP",
# }
# setup_info = {
# "version": "5.4",
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 60.0,
# "tra_with_quartz_mV": 720.0,
# "filter_ref": "375 nm SP",
# # "filter_tra": "375 nm LP",
# # "filter_ref": "No filter",
# "filter_tra": "375 nm SP",
# }
setup_info = {
"version": "5.12", # 5.5: before lamp change
"angle_deg (probe beam, sample normal)": 26.6,
"angle_deg (LED pump beam, sample normal)": 0,
"angle_deg (Laser pump beam, sample normal)": 0,
# Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
"ref_with_quartz_mV": 55.0,
"tra_with_quartz_mV": 900.0,
"filter_ref": "420 nm LP",
"filter_tra": "420 nm LP",
# "filter_ref": "No filter",
# "filter_ref": "375 nm LP",
# "filter_tra": "375 nm LP",
# "notes": "90/10 beam splitter at probe light exit removed; changed xenon lamp bulb",
"notes": "Filter wheel"
}
# setup_info = {
# "version": "5.9", # 5.5: before lamp change
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": 48.0,
# "tra_with_quartz_mV": 1050.0,
# "filter_ref": "420 nm LP",
# # "filter_tra": "375 nm LP",
# # "filter_ref": "No filter",
# "filter_tra": "420 nm LP",
# "notes": "samples mounted in Thorlabs Filter Wheel",
# }
# setup_info = {
# "version": "5.7", # 5.6: T/V cell with glass panes
# "angle_deg (probe beam, sample normal)": 26.6,
# "angle_deg (LED pump beam, sample normal)": 0,
# "angle_deg (Laser pump beam, sample normal)": 0,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# # "ref_with_quartz_mV": 67.0,
# # "tra_with_quartz_mV": 595.0,
# "filter_ref": "420 nm LP",
# # "filter_tra": "375 nm LP",
# # "filter_ref": "No filter",
# "filter_tra": "420 nm LP",
# "notes": "90/10 beam splitter at probe light exit removed",
# "mount": "temperature controlled cell with glass panes removed; used iris to limit transmission intensity",
# }
# Uncomment if measuring in TA holder with Kymera and adjust minor version number
# setup_info = {
# "version": "4.1",
# "description": "Using TAS setup/sample holder, Monochromator before detector",
# "holder_diameter_cm": 1.0,
# "angle_deg (probe beam, sample normal)": 15.0,
# "angle_deg (LED pump beam, sample normal)": 30.0,
# # "angle_deg (Laser pump beam, sample normal)": 55,
# # Detector voltages measured with oscilloscope for 500 nm beam @1nm resolution hitting quartz sample
# "ref_with_quartz_mV": None, # TODO
# "tra_with_quartz_mV": None, # TODO
# }
laser_power_set_mW_to_measured_mWcm2_v50 = np.array([
[ 7.75 , 7.97293828],
[ 15.5 , 14.30525007],
[ 23.25 , 20.46454866],
[ 31. , 26.59180522],
[ 38.75 , 32.39875515],
[ 46.5 , 38.00229323],
[ 54.25 , 43.42652863],
[ 62. , 48.55616939],
[ 69.75 , 53.44245769],
[ 77.5 , 57.86125614],
[ 85.25 , 61.9086134 ],
[ 93. , 65.37319633],
[100.75 , 68.54960804],
[108.5 , 71.70040494],
[116.25 , 74.15315388],
[124. , 77.01576361],
[131.75 , 78.8921488 ],
[139.5 , 81.29366319],
[147.25 , 83.22127757],
[155. , 84.68781255]
])
laser_power_conversion = laser_power_set_mW_to_measured_mWcm2_v50
@np.vectorize
def reported_power_mW_to_probe_power_mWcm2(power_mW: float) -> float:
"""
:param power_mW: Laser output power
:return: power in mW/cm^2
"""
f = np.polynomial.Polynomial.fit(laser_power_conversion[:, 0], laser_power_conversion[:, 1], 2)
return float(round(f(power_mW), 3))
@np.vectorize
def target_power_mWcm2_to_laser_power_mW(power_mWcm2: float) -> float:
"""
:return: Laser output power when the power density hitting the sample is power_mWcm2
"""
f = np.polynomial.Polynomial.fit(laser_power_conversion[:, 1], laser_power_conversion[:, 0], 4)
return float(round(f(power_mWcm2), 3))
# XENON_SPIKE_WAVELENGTHS_NM = [
# 762, 763, 764, 765,
# ]
#
# def filter_xenon_spikes(wavelengths: list[int|float|str]):
# for spike in XENON_SPIKE_WAVELENGTHS_NM:
# if spike in wavelengths:
# wavelengths.remove(spike)
# power_mW/cm^2, current_A
power_and_current_v33 = np.array([
[ 1.0, 0.056],
[ 3.0, 0.125],
[ 5.0, 0.196],
[ 8.0, 0.311],
[10.0, 0.390],
[12.0, 0.475],
[14.0, 0.563],
[16.0, 0.656],
])
power_and_current_v40 = np.array([
[ 1.0, 0.126],
[ 2.0, 0.233],
[ 3.0, 0.344],
[ 4.0, 0.465],
[ 5.0, 0.589],
[ 5.8, 0.700],
]) # 08.07.25
power_and_current_v41 = np.array([
[ 1.0, 0.0158],
[ 2.0, 0.0300],
[ 3.0, 0.0438],
[ 5.0, 0.0717],
[10.0, 0.1410],
[20.0, 0.2810],
[30.0, 0.4280],
[40.0, 0.6010],
[44.8, 0.7000],
]) # 15.07.25 625 nm LED
power_and_current_v36 = np.array([
[ 1.0, 0.0057],
[ 2.0, 0.0105],
[ 3.0, 0.0152],
[ 5.0, 0.0243],
[20.0, 0.0910],
[50.0, 0.2580],
[80.0, 0.5910],
[85.7, 0.7000],
]) # 14.07.2025 625 nm LED
power_and_current_v37 = np.array([
[ 1.0, 0.068],
[ 2.0, 0.113],
[ 3.0, 0.159],
[ 5.0, 0.256],
[ 8.0, 0.414],
[10.0, 0.525],
[13.0, 0.7000],
]) # 16.07.2025 340 nm LED
power_and_current_v54_275nm = np.array([
[ 0.38724017,0.07 ],
[ 0.9349505 ,0.14 ],
[ 1.49761924,0.21 ],
[ 2.05277008,0.28 ],
[ 2.58594635,0.35 ],
[ 3.0927144 ,0.42 ],
[ 3.57133932,0.49 ],
[ 4.01970054,0.56 ],
# [0.63 , 4.25756774], removed because it plateaus here, dont go over 4 mW/cm2
# [0.7 , 4.25313412]
]) # 15.09.25 275 nm
power_and_current_v59_275nm = np.array([
[ 0.15254059, 0.07],
[ 0.3965642 , 0.14],
[ 0.64991181, 0.21],
[ 0.90025356, 0.28],
[ 1.14081893, 0.35],
[ 1.37213182, 0.42],
[ 1.59091537, 0.49],
[ 1.79639827, 0.56],
[ 1.90627183, 0.63],
[ 1.90511523, 0.7 ]
]) # 01.12.25 275 nm
power_and_current_v59_340nm = np.array([
[ 1.63872003, 0.07],
[ 3.79108646, 0.14],
[ 5.82432653, 0.21],
[ 7.74576585, 0.28],
[ 9.59035464, 0.35],
[11.36460492, 0.42],
[13.09478553, 0.49],
[14.79431001, 0.56],
[15.74658011, 0.63],
[15.75041312, 0.7 ],
]) # 04.12.25 340 nm
power_and_current_v510_340nm = np.array([
[0.68811492, 0.07],
[1.63831168, 0.14],
[2.54809265, 0.21],
[3.41295767, 0.28],
[4.24461396, 0.35],
[5.04470214, 0.42],
[5.82116017, 0.49],
[6.57820588, 0.56],
[7.32427999, 0.63],
[8.05178609, 0.7 ]
]) # 04.02.26
power_and_current = power_and_current_v510_340nm
method = "interpolate"
@np.vectorize
def led_current_A_to_probe_power_mWcm2(current_A: float) -> float:
"""
:param current_A: LED current in A
:return: power in mW/cm^2
"""
f = np.polynomial.Polynomial.fit(power_and_current[:, 1], power_and_current[:, 0], 2)
return float(round(f(current_A), 1))
@np.vectorize
def target_probe_power_mWcm2_to_led_current_A(target_power_mW: float) -> float:
"""
:param target_power_mW:
:return: Led current in A
"""
f = np.polynomial.Polynomial.fit(power_and_current[:, 0], power_and_current[:, 1], 4)
return float(round(f(target_power_mW), 3))
[docs]
def plot_probe_power_conversion():
fig, axs = plt.subplots(2, 2)
linaxs = np.reshape(axs, (4))
for i, (conversion, xlabel, ylabel, f) in enumerate([
(power_and_current[:,::-1], "LED Current [A]", "Probe Power Density [mW/cm$^2$]", led_current_A_to_probe_power_mWcm2),
(power_and_current, "Probe Power Density [mW/cm$^2$]", "LED Current [A]", target_probe_power_mWcm2_to_led_current_A),
(laser_power_conversion, "Laser Power [mW]", "Probe Power Density [mW/cm$^2$]", reported_power_mW_to_probe_power_mWcm2),
(laser_power_conversion[:,::-1], "Probe Power Density [mW/cm$^2$]", "Laser Power [mW]", target_power_mWcm2_to_laser_power_mW),
]):
xdata = np.linspace(conversion[0,0], conversion[-1,0], 200)
linaxs[i].plot(xdata, f(xdata), color="black")
linaxs[i].scatter(conversion[:,0], conversion[:,1], color="red")
linaxs[i].set_xlabel(xlabel)
linaxs[i].set_ylabel(ylabel)
fig.tight_layout()
return fig, axs
# CONFIGURATION
# defaults, these may be overridden by a config file
default_settings = {
"data_dir_final": path.expanduser("~/Desktop/PR/data"),
"data_dir_cache": path.expanduser("~/.cache/prsctrl/data"),
"data_dir_archive": path.expanduser("~/.cache/prsctrl/archive"),
"log_dir": path.expanduser("~/.cache/prsctrl"),
"put_ref_tra_in_subdir": True,
"name": "interactive-test",
}
cfilename: str = "photoreflectance.yaml"
config_path: str = ""
config_file: ConfigFile = ConfigFile("")
# DEVICES
devmg = None
# global variable for the instrument/client returned by pyvisa/bleak
ref_lockin: LockInAmp|None = None
tra_lockin: LockInAmp|None = None
shutter: Shutter|None = None
xenon: Lamp | None = None
halogen: Lamp | None = None
mcm: Monochromator|None = None
kymera: Monochromator|None = None
laser: Laser|None = None
power_switch_PRS: Switch|None = None
power_switch_TAS: Switch|None = None
ref_amplifier: Amplifier|None = None
tra_amplifier: Amplifier|None = None
led: LedController|None = None
sam: Switch|None = None
temp_sensor: TemperatureSensor|None = None
temp_controller: TemperatureController|None = None
wheel: Wheel|None = None
t0 = 0
data = None
md = None
default_measurement_settings = {
"measurement_time_s": 30,
"wait_time_s": 0,
"pump_source": "none"
}
default_amplifier_settings = {
"coupling": "DC",
"gain": 7,
}
default_led_settings = {
"mode": "TTL",
"current_A": 0.1,
}
default_laser_settings = {
"power_mW": 1.0,
}
default_lock_in_settings = {
"time_constant_s": 10,
# "time_constant_s": 300e-3,
"sensitivity_volt": 500e-6,
"filter_slope": 12,
"sync_filter": 1,
"reserve": "Normal",
"reference": "External",
"reference_trigger": "Rising Edge",
# "frequency_Hz": 173,
"sample_rate_Hz": 512,
}
default_monochromator_settings = {
"resolution_nm": 1.0
}
[docs]
def get_time_estimate_per_wavelength(measurement_settings: PrsSettings, extra_wait_time_s=10) -> list[float]:
"""
:return: Number of seconds a measurement step will take (step = at one wavelength) for each mode.
"""
# Set wavelength
monochromator_set_time = 0
for m in [mcm, kymera]:
# TODO: measure times
if "TMC" in m.get_device_name(): # Bentham
monochromator_set_time += 5
elif "Kymera" in m.get_device_name():
monochromator_set_time += 5
else:
log.warning(f"Dummy or unknown monochromator device '{m.get_device_name()}', can not give good time estimate.")
monochromator_set_time += 5
ms: PrsSettings = measurement_settings
lockins = {
"ref": ref_lockin,
"tra": tra_lockin,
}
mode_times = []
for mode_key, mode_set in ms.modes.items():
if mode_key not in lockins:
log.warning(f"Unsupported mode '{mode_key}', can not give time prediction!")
continue
lockin = lockins[mode_key]
if lockin is None:
log.warning(f"No lock-in amplifier for mode '{mode_key}', can not give time prediction!")
continue
mode_time = 0
if measurement_settings.general.wait_time_s is not None:
wait_time_s = measurement_settings.general.wait_time_s + extra_wait_time_s
else:
wait_time_s = lockin.get_wait_time_s(mode_set.lock_in.time_constant_s, mode_set.lock_in.filter_slope) + extra_wait_time_s
measurement_time_s = ms.general.measurement_time_s
log.info(f"Wait_time_s={wait_time_s}, measurement_time_s={measurement_time_s}")
# Data Transfer + Processing
if "830" in lockin.get_device_name():
vals_per_sec = 666 # assuming two channels (Benchmark from 14.06.2025)
max_size = lockin.buffer_get_max_size(2)
elif "Model7260" in lockin.get_device_name():
vals_per_sec = 196 # assuming two or three channels (Benchmark from 14.06.2025)
max_size = lockin.buffer_get_max_size(3)
else:
log.warning(f"Dummy or unknown lock-in device '{lockin.get_device_name()}', can not give good time estimate.")
vals_per_sec = 300
max_size = 16383
srat = mode_set.lock_in.sample_rate_Hz
if srat == "max":
srat = max_size/measurement_time_s # perfect sample rate to fully fill the buffer in the measurement time
srat = lockin.get_next_valid_sample_rate_Hz(srat)
n_values = measurement_time_s * srat
processing_time = 20
get_data_time = n_values / vals_per_sec + processing_time
# Time to set wavelength and wait until stable
set_wavelength_time = monochromator_set_time + wait_time_s
mode_time += measurement_time_s + max(get_data_time, set_wavelength_time)
mode_times.append(mode_time)
return mode_times
[docs]
def add_offset_measurements(wavelengths: list[float], time_per_wavelength: float, offset_intervals_minutes:list|None=[5, 10, 15, 30, 60]) -> list[float|str]:
"""
Add offset measurements before, after and between spectral measurements based on the estimated measurement time.
:param wavelengths: The wavelengths to measure.
:param time_per_wavelength: The estimated time (in seconds) that a single measurement will take.
:param offset_intervals_minutes: A list of intervals (in minutes) to wait between taking offsets. The last interval repeatedly, until the measurement is over.
"""
measurements: list[float|str] = ["offset_begin"]
if offset_intervals_minutes:
interval_idx = 0
offset_intervals_seconds = np.array(offset_intervals_minutes) * 60
t_since_offset = 0.0
for i in range(len(wavelengths)):
measurements.append(wavelengths[i])
t_since_offset += time_per_wavelength
# add an offset if the required time has passed and the remaining time is more than half the interval time
if t_since_offset > offset_intervals_seconds[interval_idx] and (len(wavelengths) - i)*time_per_wavelength > offset_intervals_seconds[interval_idx] * 0.5:
measurements.append(f"offset_after_{wavelengths[i]}nm")
t_since_offset = 0.0
if interval_idx+1 < len(offset_intervals_seconds):
interval_idx += 1
measurements.append("offset_end")
return measurements
[docs]
def add_hardcoded_offset_measurement(wavelengths: list[int|str], after_wl: int):
idx = wavelengths.index(after_wl)
if isinstance(wavelengths[idx+1], str):
pass
else:
wavelengths.insert(idx+1, f"offset_after_{after_wl}nm")
wavelengths.insert(idx+2, f"offset_after_{after_wl}nm_2")
return wavelengths
# MEASURING
[docs]
def measure_spectrum(
measurement_settings: PrsSettings,
metadata:dict={},
do_ref=None,
do_tra=None,
turn_off_pump=True,
# pump_settingsNone,
auto_add_offset_measurements=True,
extra_wait_time_s=10,
name:str|None=None,
dirname=None,
save_spectrum=True,
only_estimate=False,
force_with_dummies=False,
auto_determine_gain=False,
) -> float:
global data, md
global ref_lockin, tra_lockin, shutter, xenon, halogen, mcm, laser, ref_amplifier
from dataclasses import asdict
time_estimates = get_time_estimate_per_wavelength(measurement_settings=measurement_settings, extra_wait_time_s=extra_wait_time_s)
time_per_wavelength = max(time_estimates)
wavelengths = measurement_settings.general.get_wavelengths_nm()
if auto_add_offset_measurements:
wavelengths = add_offset_measurements(wavelengths, time_per_wavelength)
time_estimate = len(wavelengths) * time_per_wavelength
log.debug(f"Wavelengths: {wavelengths}")
log.info(f"Time estimate: {duration_to_string(time_estimate)}")
# Check if the measurement should actually be started. Dont manipulate any devices until here!
if (ref_lockin is None or isinstance(ref_lockin, DummyDevice)) and (tra_lockin is None or isinstance(tra_lockin, DummyDevice)) and (not force_with_dummies):
log.info(f"Not starting measurement since both lock-ins are None or DummyDevice objects.")
return time_estimate
if only_estimate:
log.info(f"Not starting measurement since only_estimate==True")
return time_estimate
# Determine mode and merge settings
lockins = []
if do_ref is None:
do_ref = "ref" in measurement_settings.modes
if do_tra is None:
do_tra = "tra" in measurement_settings.modes
if not (do_ref or do_tra):
raise ValueError(f"Measurement mode (ref or tra) could not be determined from parameters")
if do_ref:
if not isinstance(ref_lockin, LockInAmp): raise RuntimeError(f"Lock-in for mode 'ref' must be a LockInAmp but is '{type(ref_lockin)}'")
measurement_settings.modes["ref"].lock_in.apply(ref_lockin)
pre_amp_settings = measurement_settings.modes["ref"].pre_amplifier
if pre_amp_settings is not None and ref_amplifier is not None:
pre_amp_settings.apply(ref_amplifier)
lockins.append(ref_lockin)
if do_tra:
if not isinstance(tra_lockin, LockInAmp): raise RuntimeError(f"Lock-in for mode 'tra' must be a LockInAmp but is '{type(tra_lockin)}'")
measurement_settings.modes["tra"].lock_in.apply(tra_lockin)
pre_amp_settings = measurement_settings.modes["tra"].pre_amplifier
if pre_amp_settings is not None and tra_amplifier is not None:
pre_amp_settings.apply(tra_amplifier)
lockins.append(tra_lockin)
# must come after lockin settings, since its using get_wait_time_s
if measurement_settings.general.wait_time_s is None:
wait_time_s = max([lockin.read_get_wait_time_s() for lockin in lockins]) + extra_wait_time_s
measurement_settings.general.wait_time_s = wait_time_s
# get the frequency, if not given
# refkey = "frequency_Hz"
# if not refkey in measurement_settings:
# measurement_settings[refkey] = lockins[0].get_frequency_Hz()
# # TODO: get both?
# APPLY SETTINGS
monochromators = [mcm, kymera]
for i, m in enumerate(measurement_settings.monochromators):
m.apply(monochromators[i])
set_pump = False
pump_source = measurement_settings.general.pump_source
if pump_source == "led":
if measurement_settings.led is None:
raise RuntimeError(f"Pump source = 'led', but there are no settings for the LED (settings.led is None)")
measurement_settings.led.apply(led)
if led: led.on()
set_pump = True
if pump_source == "laser":
measurement_settings.laser.apply(laser)
if measurement_settings.laser is None:
raise RuntimeError(f"Pump source = 'laser', but there are no settings for the Laser (settings.laser is None)")
if laser: laser.on()
set_pump = True
if not set_pump:
log.warning(f"Unknown pump source setting: '{pump_source}' -> pump source must be set up manually. (Valid are 'led' and 'laser')")
else:
log.info(f"Pump light is on!")
# METADATA
metadata["prsctrl_version"] = version # might be helpful when the data collection is changed
# if measurement_settings.led: metadata["led"] = asdict(measurement_settings.led)
# if measurement_settings.laser: metadata["laser"] = asdict(measurement_settings.laser)
# if measurement_settings.monochromators: metadata["monochromator"] = [asdict(m) for m in measurement_settings.monochromators]
if name is None:
name = config_file.get_or("default_name", default_settings["name"])
metadata["name"] = name
if dirname is None:
dirname = get_date_time_for_filename() + "_" + name
dirname = sanitize_filename(dirname)
cache_data_dir = config_file.get_or("data_dir_cache", default_settings["data_dir_cache"])
# Make sure data_path is a non-existing, valid path
data_path = get_next_filename(os.path.join(cache_data_dir, dirname))
# if do_ref and do_tra and config_file.get_or("put_ref_tra_in_subdir", True):
# makedirs(data_path)
# data_path = os.path.join(data_path, dirname)
if temp_sensor is not None and not isinstance(temp_sensor, DummyDevice):
metadata["environment"] = {}
try:
metadata["environment"]["temperature_C"] = temp_sensor.get_temperature_C()
metadata["environment"]["humidity"] = temp_sensor.get_humidity_percent()
except Exception as e:
log.error(f"Failed to get temperature or humidity: {type(e)}: {e}")
log.debug(f"Reflectance={do_ref}, Transmission={do_tra}")
log.debug(f"Data path: {data_path}")
# log.debug(f"set: {common_measurement_settings}")
# log.debug(f"set-ref: {ref_measurement_settings}")
# log.debug(f"set-tra: {tra_measurement_settings}")
# log.debug(f"set-lockin-ref: {ref_lockin_settings}")
# log.debug(f"set-amp-ref: {ref_amp_settings}")
# log.debug(f"set-lockin-tra: {tra_lockin_settings}")
# log.debug(f"set-amp-tra: {tra_amp_settings}")
# log.debug(f"set-led: {led_settings}")
# log.debug(f"set-laser: {laser_settings}")
# log.debug(f"set-monochromator: {monochromator_settings}")
# PREPARE MEASUREMENT
# data_collector.clear()
data_queue = multiprocessing.Queue()
command_queue = multiprocessing.Queue()
m_handler = MeasurementHandler(
measurement_settings=measurement_settings.general,
wavelengths=wavelengths,
command_queue=command_queue,
data_queue=data_queue,
monochromators=[mcm, kymera],
shutter=shutter,
add_measurement_info_to_metadata=True,
compress_data=config_file.get_or("compress_data", False)
)
plt.ion()
monitors = {}
monitor_y_rows = ["dI_I", "dI", "lock-in-R", "I", "lock-in-aux", "theta-corr", "lock-in-theta"]
datas = {}
if do_ref:
key = REFLECTION
ref_lockin_settings = measurement_settings.modes["ref"].lock_in
ref_md = metadata | {
# "lock-in-amp": asdict(ref_lockin_settings),
# "pre-amp": ref_amp_settings,
# "measurement_settings": common_measurement_settings | ref_measurement_settings
}
datas[key] = PrsData(data={}, metadata=ref_md, data_path=data_path + "-" + REFLECTION, data_name=name, file_mode="w", exp_mode=REFLECTION)
m_handler.add_measurement_mode(key, ref_lockin, ref_lockin_settings, datas[key], pre_amp=ref_amplifier)
monitors[key] = Monitor(r"$\lambda$ [nm]",
[dict(ax=i, ylabel=PrsData.get_column_tex_label_with_unit(key, mode=REFLECTION), color=PrsData.get_column_color(key, default=DEFAULT_COLOR)) for i, key in enumerate(monitor_y_rows)]
)
if do_tra:
key = TRANSMISSION
tra_lockin_settings = measurement_settings.modes["tra"].lock_in
tra_md = metadata | {
# "lock-in-amp": asdict(tra_lockin_settings),
# "pre-amp": asdict(tra_amp_settings),
# "measurement_settings": asdict(measurement_settings.modes["tra"]) | tra_measurement_settings
}
datas[key] = PrsData(data={}, metadata=tra_md, data_path=data_path + "-" + TRANSMISSION, data_name=name, file_mode="w", exp_mode=TRANSMISSION)
m_handler.add_measurement_mode(key, tra_lockin, tra_lockin_settings, datas[key], pre_amp=tra_amplifier)
monitors[key] = Monitor(r"$\lambda$ [nm]",
[dict(ax=i, ylabel=PrsData.get_column_tex_label_with_unit(key, mode=TRANSMISSION), color=PrsData.get_column_color(key, default=DEFAULT_COLOR)) for i, key in enumerate(monitor_y_rows)]
)
print(f"Starting measurement with: Use <C-c> to stop. You can save the data using 'data.save_csv()' afterwards.")
for mon in monitors.values(): mon.set_fig_title(f"Starting measurement") # ദ്ദി ( ᵔ ᗜ ᵔ )
m_handler.prepare()
if auto_determine_gain != False:
for mon in monitors.values(): mon.set_fig_title(f"Automatically determining optimum gain value")
if isinstance(auto_determine_gain, int) and not isinstance(auto_determine_gain, bool):
N_vals = auto_determine_gain
else:
N_vals = 8
m_handler.auto_determine_gain(N_test=N_vals)
# BEGIN
time_start = time.time()
# Argument order must match the definition
proc_measure = mt.Thread(target=m_handler.run)
proc_measure.start()
try:
while proc_measure.is_alive():
while not data_queue.empty():
# print(data_queue.qsize(), "\n\n")
msg = data_queue.get(block=False)
if msg[0] == "data": # "data", ref/tra, spectrum_data
data_for_monitor = [msg[2][0,PrsData.default_spectrum_columns.index(key)] for key in ["wl"] + monitor_y_rows]
monitors[msg[1]].update(*data_for_monitor)
monitors[msg[1]].set_ax_title(f"Waiting...") # done getting data
elif msg[0] == "offset": # msg 2 and 3 are R and theta offset
monitors[msg[1]].set_ax_title(f"Offsets: $R={msg[2]}$ V, $\\theta={msg[3]}$°")
elif msg[0] == "set_wavelength":
# for mon in monitors.values(): mon.set_ax_title("Setting wavelength...")
for mon in monitors.values(): mon.set_fig_title(f"$\\lambda = {msg[1]}$ nm")
elif msg[0] == "close_shutter":
for mon in monitors.values(): mon.set_fig_title(f"Probe light off")
for mon in monitors.values(): mon.set_ax_title(f"Closing shutter")
elif msg[0] == "wait_stable":
for mon in monitors.values(): mon.set_ax_title(f"Waiting for signal to stabilize ({msg[1]} s)")
elif msg[0] == "get_data":
monitors[msg[1]].set_ax_title("Getting data...")
elif msg[0] == "measuring":
for mon in monitors.values(): mon.set_ax_title("Measuring...")
elif msg[0] == "warning":
for mon in monitors.values(): mon.set_ax_title(f"Warning: {msg[1]}")
else:
log.error(f"Invalid tuple received from measurement thread: {msg[0]}")
time.sleep(0.01)
for mon in monitors.values(): mon.idle()
except KeyboardInterrupt:
log.info(f"KeyboardInterrupt received, measurement will stop after finishing this wavelength.")
pass
# STOP
command_queue.put("stop")
proc_measure.join(timeout=120) # TODO: Use wait_time_s + measurement_time_s
if proc_measure.is_alive():
log.critical("Timed out waiting for measurement thread to finish")
for mon in monitors.values(): mon.set_fig_title(name)
for mon in monitors.values(): mon.set_ax_title("")
print("Measurement stopped" + " "*50)
time_stop = time.time()
if turn_off_pump:
try:
if "led" in measurement_settings.general.pump_source:
led.off()
if "laser" in measurement_settings.general.pump_source:
laser.off()
except Exception as e:
log.error(f"Failed to turn off pump: {type(e)}: {e}")
# Plots and CSV
if save_spectrum:
plt.ioff()
for data in datas.values():
try:
data.remove_calculated_values() # make sure all data is calculated using correct offset correction
data.save_csv()
title = prsdata.util.get_data_name_template(data.dirname, template="<sample_type> <sample_batch> <info> <mode>")
_ = plot_data(data, path.basename(data.dirpath), data.dirpath, title=title, error_shade=True)
except Exception as e:
log.error(f"Exception while saving/plotting ({data.mode}) data: {type(e)}: {e}")
# Error report
for data in datas.values():
try:
report = prsdata.check.sanity_check_data_gen_report(data)
with open(os.path.join(data.dirpath, data.dirname + "-report.md"), "w") as file:
file.write(report)
except Exception as e:
log.error(f"Exception while generating report for ({data.mode}) data: {type(e)}: {e}")
# Archive data
for data in datas.values():
try:
n_wavelengths_threshold = config_file.get_or("archive_file_count_threshold", 50)
archive_dirs = config_file.get_or("data_dir_archive", [])
prsdata.util.archive_data(data.dirpath, archive_dirs=archive_dirs, n_wavelengths_threshold=n_wavelengths_threshold)
except Exception as e:
log.error(f"Exception while archiving data: {e}")
# Copy final data to other directories
if do_ref and do_tra:
try:
if do_ref and do_tra:
copy_data_dirs = config_file.get("data_dir_final")
norm_data_dir = config_file.get_or(f"abs-calc_reference-data-dir", None)
prsdata.util.save_data_rta(datas[REFLECTION], datas[TRANSMISSION], copy_data_dirs, norm_data_dir=norm_data_dir, error_shade=True, max_ylim=(-75, 75))
except Exception as e:
log.error(f"Exception while copying ref and tra data: {type(e)}: {e}")
else:
for data in datas.values():
try:
copy_data_dirs = config_file.get("data_dir_final")
prsdata.util.save_data(data.dirpath, copy_data_dirs)
except Exception as e:
log.error(f"Exception while copying ({data.mode}) data: {type(e)}: {e}")
duration = time_stop - time_start
log.info(f"Measurement took {duration_to_string(duration)} (estimate was {duration_to_string(time_estimate)})")
return duration
[docs]
def measure_reference_no_sample(gain=6):
global data
aux_DC = "ADC1"
measurement_settings = {
"wavelengths_nm": list(range(375, 1101, 1)),
# "wavelengths_nm": [370, 500, 700, 850],
# "wavelengths_nm": list(range(500, 550, 1)),
"monochromator": { "bandwidth_nm": 1.0, },
"pump_source": "none",
# "pump_power_mW/cm^2": 5.00,
"measurement_time_s": 30,
"wait_time_s": 0.5,
"pre-amp": {
"coupling": "DC",
"gain": gain,
},
"lock-in-amp": {
# "time_constant_s": 2 if "7260" in str(lockin) else 3,
"time_constant_s": 1,
"filter_slope": 12,
"sensitivity_volt": 500e-6,
"aux_DC": aux_DC,
"sample_rate_Hz": 50,
}
}
print(f"Using wavelengths: {get_wavelengths_from_measurement_settings(measurement_settings)}")
do_offset = False
etime = 0
time_est_per_wl = max(get_time_estimate_per_wavelength(measurement_settings=measurement_settings, tra_meas_settings=measurement_settings, extra_wait_time_s=etime))
wls = get_wavelengths_from_measurement_settings(measurement_settings)
if do_offset:
wls = add_offset_measurements(wls, time_est_per_wl, offset_intervals_minutes=None)
time_est = time_est_per_wl * len(wls)
print(f"Estimated time: {duration_to_string(time_est)}")
if tra_lockin is None:
print("Not starting measurement since lock-in is None")
return
t_start = time.time()
laser.off()
led.off()
metadata = {}
name = f"Reference_no-sample-holder"
# apply settings and set metadata
metadata["pre-amp"] = apply_settings_amplifier(tra_amplifier, None, measurement_settings)
metadata["lock-in-amp"] = apply_settings_lockin(tra_lockin, None, measurement_settings)
metadata["monochromator"] = apply_settings_monochromator(mcm, None, measurement_settings)
metadata["measurement_settings"] = measurement_settings
metadata["setup"] = setup_info
metadata["sample"] = {
"name": "No Sample",
}
metadata["prsctrl_version"] = version # might be helpful when the data collection is changed
metadata["name"] = name
dirname = get_date_time_for_filename() + "_" + name
dirname = sanitize_filename(dirname)
cache_data_dir = config_file.get_or("data_dir_cache", default_settings["data_dir_cache"])
# Make sure data_path is a non-existing, valid path
data_path = get_next_filename(os.path.join(cache_data_dir, dirname))
data = PrsData(data={}, metadata=metadata.copy(), data_path=data_path, data_name=name, file_mode="w", exp_mode=TRANSMISSION)
sample_rate_Hz = 200
n_bins = sample_rate_Hz * measurement_settings["measurement_time_s"]
data.write_metadata()
if not shutter.is_open(): shutter.open()
thread = mt.Thread()
proc_set = mt.Thread(target=mcm.set_wavelength_nm, args=(wls[0], ))
proc_set.start()
for i, wl in enumerate(wls):
print(f"{i:4}: {wl} nm")
# setup new measurement
tra_lockin.buffer_setup(what=[aux_DC], length=n_bins, sample_rate_Hz=sample_rate_Hz)
# while setting wavelength, prepare measurement
data[wl] = {}
data[wl]["lock-in-aux_raw"] = np.empty(n_bins, dtype=float)
t = 20
while t > 0:
if proc_set.is_alive():
time.sleep(0.5)
t -= 0.5
else:
break
if t < 0: raise RuntimeError(f"Timeout waiting for thread")
proc_set.join()
tra_lockin.buffer_start_fill()
t = 100
while t > 0:
t -= 0.5
time.sleep(0.5)
n_points = tra_lockin.buffer_get_n_points()
if n_points >= n_bins: break# not done
if t < 0: raise RuntimeError(f"Timed out waiting for measurement on wavelength {wl} nm to finish")
if i < len(wls) - 1:
proc_set = mt.Thread(target=mcm.set_wavelength_nm, args=(wls[i+1],))
proc_set.start()
lock_in_data = tra_lockin.buffer_get_data(start=0, n_points=n_bins) # indices in the order of the "what" param during buffer_setup
data[wl]["lock-in-aux_raw"] = lock_in_data[0,:]
R, sR = data.get_spectrum_data([wl], ["R", "sR"])[0][0,:]
print(f"lambda={wl} nm, R={R:.3f}pm{sR:.3f}")
data.write_partial_file(wl)
duration = time.time() - t_start
data.metadata["measurement_duration"] = duration_to_string(duration)
data.write_metadata()
data.write_full_file()
csv = data.to_csv(columns=["wl", "E", "I", "sI"])
with open(data_path + "/" + dirname + ".csv", "w") as f:
f.write(csv)
log.info(f"Measurement took {duration_to_string(duration)} (estimate was {duration_to_string(time_est)})")
fig, ax = plot_spectrum(data, columns=["wl", "E", "I", "sI"], what=["I"], error_shade=True)
fig.savefig(data_path + "/" + dirname + ".pdf")
[docs]
def measure_ref_tra(
param_file=None,
wls=None,
energies_eV=None,
power_mWcm2=None,
frequency=None,
suffix="",
no_ref=False,
no_tra=False,
metadata=None,
name=None,
do_offset=True,
notes=None,
measurement_time_s=None,
deltaE=0.005,
extra_wait_time_s=None,
**measure_spectrum_kw
):
"""
Measure both reflectance and transmittance
"""
mset = PrsSettings.from_dict({
"general": MeasurementSettings(
wavelengths_nm=wls,
energies_eV=energies_eV,
pump_power_mWcm2=5.0 if power_mWcm2 is None else power_mWcm2,
measurement_time_s=measurement_time_s or 30,
auto_gain=True,
auto_sensitivity=True,
pump_source="laser",
),
"monochromators": [MonochromatorSettings(resolution_eV=0.005)],
"modes": {
"ref": ModeSettings(
lock_in=LockInSettings(
time_constant_s=1,
filter_slope=12,
sensitivity_volt=50e-6,
sample_rate_Hz="max",
reference="Internal",
frequency_Hz=333 if frequency is None else frequency,
aux_DC="Aux In 4",
),
pre_amplifier=AmplifierSettings(
gain=5,
coupling="DC",
mode="Low Noise",
),
),
"tra": ModeSettings(
lock_in=LockInSettings(
time_constant_s=1,
filter_slope=12,
sensitivity_volt=50e-6,
sample_rate_Hz=50,
aux_DC="Aux In 4",
reference="External"
),
pre_amplifier=AmplifierSettings(
gain=5,
coupling="DC",
mode="Low Noise",
),
),
},
})
if param_file is not None:
param_file = os.path.expanduser(param_file)
if not os.path.isfile(param_file):
raise FileNotFoundError(f"Parameter file {param_file} not found")
with open(param_file, "r") as f:
settings_dict = yaml.load(f, yaml.Loader)
print(settings_dict)
msetload = PrsSettings.from_dict(settings_dict)
# TODO: merge with dict instead of PrsSettings, otherwise might be overridden with defaults
print(f"Loaded from file:\n",msetload)
mset |= msetload
mset.prepare(
power_density_to_laser_power_mW=target_power_mWcm2_to_laser_power_mW,
power_density_to_led_current_A=target_probe_power_mWcm2_to_led_current_A,
)
# if no_ref:
# del mset.modes["ref"]
# if no_tra:
# del mset.modes["tra"]
print(f"Combined and prepared:")
print(mset)
if suffix:
suffix = "_" + suffix
# name = "BiVO4_Q_poly" + suffix
# name = "Test_Setup_v4.0" + suffix
if name:
name += suffix
else:
# name = "O-Ta3N5_TiNTaN-1-Q-S3" + suffix
# name = "N-Ta3N5-110-S16_YSZ-111" + suffix
# name = "O-Ta3N5_TaO61-GaN-S7_v5.8" + suffix
# name = f"O-Ta3N5_TaO55-Q_{setup_info['version']}" + suffix
# name = "BiVO4-Q-poly_v5.10" + suffix
# name = "TaON_TANO37-Q_v5.10"
# name = f"BiVO4_0pIn" + suffix
# name = "Hematite-FTO_v5.2" + suffix
# name = "GaP-S1_v5.2"
name = "Unknown"
# name = "Software_test"
_metadata = {
"sample": {
# "name": "O-TaN/TiN-1 on quartz",
# "sample_nr": "3",
# "batch_nr": "TaO49 240328",
# "substrate": "Quartz"
"name": "O-Ta3N5",
"batch_nr": "TaO55",
"sample_nr": "1",
"substrate": "Quartz",
# "name": "O-Ta3N5",
# "batch_nr": "TaO61-GaN",
# "sample_nr": "7",
# "substrate": "Sapphire + 400 nm GaN",
# "date": "241126",
# "name": "Ta3N5(010)",
# "batch_nr": "2025.05.02 NSRRC",
# "sample_nr": "27",
# "substrate": "YSZ(100)",
# "fabricator": "Chang-Ming Jiang",
# "notes": "Laser was illuminating with 5 mW/cm^2 the sample for 1h before measurement",
# "notes": "Laser was not illuminating sample before measurement for multiple hours",
# "name": "Ta3N5(110)",
# "sample_nr": "16",
# "substrate": "YSZ(111)",
# "fabricator": "Chang-Ming Jiang",
# "notes": "Sample has glue residue on backside; surface is inhomogenous",
# "pump": "Laser was illuminating sample for 2 hours before the measurement",
# "name": "GaP-S1",
# "batch_nr": "250804_Connor-S1",
# "substrate": "Quartz",
# "notes": "150nm GaP on Q, measured 3 days after fabrication",
# "batch_nr": "TaO53",
# "name": "None",
# "name": "Fused Silica",
# "name": "BiVO4 poly",
# "batch_nr": "VB210831Q",
# "substrate": "Quartz",
# "name": "BiVO4",
# "batch_nr": "0% In",
# "substrate": "Q or YSZ",
# "manufacturer": "Guanda",
# "name": "Hematite",
# "substrate": "FTO",
# "batch_nr": "Ryan, middle sample from the box",
# "notes": "Laser was illuminating sample for 15 minutes before measurement",
# "name": "TaON",
# "substrate": "Fused Silica",
# "batch_nr": "TANO37",
# "annealing": "RTA 800°C 60min N_2 Prestep",
# "notes": "Sample from box that says 'Bad'",
},
# "test": "single USB cable with both lock-ins connected via GPIB",
"setup": setup_info,
# "notes": "Sample was modulated by pump light with same intensity for 60 minutes before the measurement."
# "notes": "Measurement with constant energy spacing and bandwidth, bandwidth adjusted at every wavelength to 0.005 eV. 1.0cm PLA insert"
}
if metadata:
metadata = _metadata | metadata
else:
metadata = _metadata
if notes:
metadata["notes"] = notes
print(f"Using wavelengths: {mset.general.get_wavelengths_nm()}")
extra_wait_time_s = extra_wait_time_s or 2
duration = measure_spectrum(
measurement_settings=mset,
name=f"{name}",
metadata=metadata,
auto_add_offset_measurements=do_offset,
extra_wait_time_s=extra_wait_time_s,
**measure_spectrum_kw,
)
log.info(f"Measurement took {duration_to_string(duration)}")
return duration
[docs]
def measure_ref_tra_offsets(wls=None, power_mWcm2=None, frequency=None, name=None, suffix="", ref_m=None, tra_m=None, **measure_spectrum_kw):
"""
Measure both reflectance and transmittance
"""
measurement_params = {
# "wavelengths_nm": list(range(410, 1101, 1)) if wls is None else wls,
# "wavelengths_nm": list(range(410, 721, 1)) if wls is None else wls,
"wavelengths_nm": ["offset_begin", "offset_end"],
# "wavelengths_nm": list(range(430, 600, 3)),
"monochromator": { "bandwidth_nm": 1.0, },
# "measurement_time_s": 30,
"measurement_time_s": 1800,
"pump_source": "laser",
"pump_power_mW/cm^2": 5.0 if power_mWcm2 is None else power_mWcm2,
# "led": {
# "mode": "TTL",
# # "current_A": 0.2580
# "current_A": 0.0243
# },
# "laser-manual": {
# "mode": "TTL",
# "power_mW": 7.321,
# },
"auto_gain": True,
"auto_sensitivity": True,
"notes": "Long-duration measurement pump light shining on sample. Pump laser was turned on 1.5h before measurement, but not illuminating the sample the previous 40 minutes.",
}
if measurement_params["pump_source"] == "led":
try:
current = measurement_params["led"]["current_A"]
except KeyError:
measurement_params["led"] = {
"mode": "TTL",
"current_A": target_probe_power_mWcm2_to_led_current_A(measurement_params["pump_power_mW/cm^2"])
}
elif measurement_params["pump_source"] == "laser":
try:
current = measurement_params["laser"]["power_mW"]
except KeyError:
measurement_params["laser"] = {
"mode": "TTL",
"power_mW": target_power_mWcm2_to_laser_power_mW(measurement_params["pump_power_mW/cm^2"])
}
# measurement_params["led"]["current_A"] = target_probe_power_mWcm2_to_led_current_A(measurement_params["pump_power_mW/cm^2"])
if suffix:
suffix = "_" + suffix
if name:
name += suffix
else:
# name = "O-Ta3N5_TaO53_offset" + suffix
# name = "BiVO4-Q-poly_v5.0" + suffix
# name = "Test_Setup_v4.0" + suffix
name = "Fused-Silica_offset" + suffix
# name = "Offset-Software-Test"
metadata = {
"sample": {
# "name": "O-TaN/TiN-1 on quartz #3",
# "batch_nr": "TaO49 240328",
# "name": "O-Ta3N5",
# "batch_nr": "TaO53",
# "substrate": "Quartz",
# "notes": "Sample measured directly after taking it out from being in nitrogen storage for 7 days",
# "batch_nr": "TaO53",
# "substrate": "Quartz",
# "notes": "Sample was in atmosphere for 24 hours after taking it out from being in nitrogen storage for 7 days",
# "name": "None",
"name": "Quartz",
"notes": "Fused-silica sample (usually used for checking alignment)",
# "name": "BiVO4 poly",
# "batch_nr": "VB210831Q",
# "substrate": "Quartz",
},
# "test": "single USB cable with both lock-ins connected via GPIB",
"setup": setup_info,
}
# measurement_params |= { # for testing
# # "wavelengths_nm": [450, 550, 650, 750, 850, 950, 1050],
# "wavelengths_nm": [450, 1050],
# "measurement_time_s": 1,
# # "wait_time_s": 0,
# }
# name += "-Test"
print(f"Using wavelengths: {get_wavelengths_from_measurement_settings(measurement_params)}")
common_lockin_settings = {
"time_constant_s": 1,
"filter_slope": 12,
"sensitivity_volt": 10e-6,
# "sample_rate_Hz": 'max',
}
tra_measurement_settings = {
"lock-in-amp": common_lockin_settings | {
"aux_DC": "Aux In 1",
"reference": "External",
"sample_rate_Hz": 1,
"time_constant_s": 2,
"sensitivity_volt": 50e-6,
# "sample_rate_Hz": 200,
},
"pre-amp": {
"coupling": "DC",
"gain": 7,
}
} | (tra_m or {})
ref_measurement_settings = {
"lock-in-amp": common_lockin_settings | {
"aux_DC": "Aux In 4",
"reference": "Internal",
"frequency_Hz": 333 if frequency is None else frequency,
"sample_rate_Hz": 1,
"time_constant_s": 3,
# "sample_rate_Hz": 512,
},
"pre-amp": {
"coupling": "DC",
"gain": 7,
}
} | (ref_m or {})
print(measurement_params)
print(ref_measurement_settings)
print(tra_measurement_settings)
do_offset = False
etime = 5
duration = measure_spectrum(
common_measurement_settings=measurement_params,
ref_measurement_settings=ref_measurement_settings,
tra_measurement_settings=tra_measurement_settings,
name=f"{name}",
metadata=metadata,
auto_add_offset_measurements=do_offset,
extra_wait_time_s=etime,
**measure_spectrum_kw
)
log.info(f"Measurement took {duration_to_string(duration)}")
return duration
[docs]
def measure_ref_or_tra(wls=None, energies_eV=None, power_mWcm2=None, measurement_time_s=None, frequency=None, name=None, suffix="", m_params=None, mode="ref", do_offset=True, **kw):
"""
Measure reflectance or transmittance with the primary lock-in
"""
measurement_params = {
# "wavelengths_nm": list(range(410, 721, 1)) if wls is None else wls,
# "wavelengths_nm": (list(range(390, 720, 1)) + list(range(720, 1101, 2))) if wls is None else wls,
"energies_eV": energies_eV,
# "monochromator": { "bandwidth_nm": 1.0, },
"monochromator": {"bandwidth_eV": 0.005},
# "measurement_time_s": 30,
"measurement_time_s": measurement_time_s or 60,
"pump_source": "laser",
"pump_power_mW/cm^2": 5.0 if power_mWcm2 is None else power_mWcm2,
# "led": {
# "mode": "TTL",
# "current_A": 0.0717, # 5 mW 4.1
# # "current_A": 0.6010, # 40 mW 4.1
# },
# "pump_source": "laser-manual",
# "pump_power_mW/cm^2": 5.0 if power is None else power,
# "laser-manual": {
# "mode": "TTL",
# "power_mW": 155.0,
# "name": "Cobolt 0405-06-01-0150-130"
# },
"auto_gain": True,
"auto_sensitivity": True,
"lock-in-amp": {
# "time_constant_s": 1,
"filter_slope": 12,
"sensitivity_volt": 100e-6,
"aux_DC": "Aux In 4",
"reference": "Internal",
"frequency_Hz": 333 if frequency is None else frequency,
# "sample_rate_Hz": 512,
"sample_rate_Hz": 128,
"time_constant_s": 1,
},
"pre-amp": {
"coupling": "DC",
"gain": 6,
}
}
if measurement_params["pump_source"] == "led":
try:
current = measurement_params["led"]["current_A"]
except KeyError:
measurement_params["led"] = {
"mode": "TTL",
"current_A": target_probe_power_mWcm2_to_led_current_A(measurement_params["pump_power_mW/cm^2"])
}
# automatically set power if none is given
elif measurement_params["pump_source"] == "laser":
try:
current = measurement_params["laser"]["power_mW"]
except KeyError:
measurement_params["laser"] = {
"mode": "TTL",
"power_mW": target_power_mWcm2_to_laser_power_mW(measurement_params["pump_power_mW/cm^2"])
}
# measurement_params["led"]["current_A"] = target_probe_power_mWcm2_to_led_current_A(measurement_params["pump_power_mW/cm^2"])
if suffix:
suffix = "_" + suffix
# name = "BiVO4_Q_poly" + suffix
# name = "Test_Setup_v4.0" + suffix
if name:
name += suffix
else:
name = "Unnamed-Sample" + suffix
# name = "InP" + suffix
metadata = {
"sample": {
# "name": "O-TaN/TiN-1 on quartz #3",
# "batch_nr": "TaO49 240328",
# "name": "O-Ta3N5",
# "batch_nr": "TaO 06",
"name": "O-Ta3N5",
"substrate": "Si",
"batch_nr": "TaO53",
# "name": "InP"
},
# "test": "single USB cable with both lock-ins connected via GPIB",
"setup": setup_info,
}
# measurement_params |= { # for testing
# # "wavelengths_nm": [450, 550, 650, 750, 850, 950, 1050],
# "wavelengths_nm": [450, 1050],
# "measurement_time_s": 1,
# # "wait_time_s": 0,
# }
# name += "-Test"
if m_params:
measurement_params = merge_dicts(measurement_params, m_params)
print(measurement_params)
print(f"Using wavelengths: {get_wavelengths_from_measurement_settings(measurement_params)}")
etime = 5
global tra_lockin, tra_amplifier
tra_lockin = ref_lockin
tra_amplifier = ref_amplifier
duration = measure_spectrum(
common_measurement_settings=measurement_params,
ref_measurement_settings=measurement_params if mode == REFLECTION else None,
tra_measurement_settings=measurement_params if mode == TRANSMISSION else None,
name=f"{name}",
metadata=metadata,
auto_add_offset_measurements=do_offset,
extra_wait_time_s=etime,
**kw
)
return duration
[docs]
def measure_ref_tra_wheel(do_pos=[1,2,3,4,5,6], dont_measure=False):
global wheel
sample_slots = [
# "TaO53-Q", "TaO55-Q", "TaM20-Q", "TaN203-Q", "TiNTaN-1-Q-S3", "TaO06-Q",
# "TaN176-Q", "TaM03-Q", "BiVO4-Q-poly", None, None, None
# "TaO55-Q", "TaM20-Q", "TaN203-Q", "TaO56-Q", "TiNTaN-1-Q-S3", "TaM03-Q"
"GdTON07-Q-1000RTA", "NdTON07-Q-1000RTA", "CeTON33-Q-1000RTA", "TbTON10-Q-1000RTA", "PrTON03-Q-1000RTA", "LaTON08-Q-1000RTA"
]
ref_tra_kws = {
"GdTON07-Q-1000RTA": {
"metadata": {
"sample": {
"name": "GdTON07_1000RTA",
"substrate": "Quartz",
"PLA_diameter": "1cm",
},
},
"name": "GdTON07-Q-1000RTA",
},
"NdTON07-Q-1000RTA": {
"metadata": {
"sample": {
"name": "NdTON07_1000RTA",
"substrate": "Quartz",
"PLA_diameter": "1cm",
},
},
"name": "NdTON07-Q-1000RTA",
},
"CeTON33-Q-1000RTA": {
"metadata": {
"sample": {
"name": "CeTON33_1000RTA",
"substrate": "Quartz",
"PLA_diameter": "1cm",
},
},
"name": "CeTON33-Q-1000RTA",
},
"TbTON10-Q-1000RTA": {
"metadata": {
"sample": {
"name": "TbTON07_1000RTA",
"substrate": "Quartz",
"PLA_diameter": "1cm",
},
},
"name": "TbTON07-Q-1000RTA",
},
"PrTON03-Q-1000RTA": {
"metadata": {
"sample": {
"name": "PrTON03-Q-1000RTA",
"substrate": "Quartz",
"PLA_diameter": "1cm",
},
},
"name": "TbTON07-Q-1000RTA",
},
"LaTON08-Q-1000RTA": {
"metadata": {
"sample": {
"name": "LaTON08-Q-1000RTA",
"substrate": "Quartz",
"PLA_diameter": "0.5cm",
},
},
"name": "LaTON08-Q-1000RTA",
},
}
# ref_tra_kws = {
# "TaO53-Q": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TaO53",
# "substrate": "Quartz",
# },
# },
# "name": "O-Ta3N5_TaO53-Q",
# },
# "TaO55-Q": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TaO55",
# "substrate": "Quartz",
# },
# },
# "name": "O-Ta3N5_TaO55-Q",
# },
# "TaO56-Q": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TaO56",
# "substrate": "Quartz",
# "notes": "Sample does not have metallic backlayer"
# },
# },
# "name": "O-Ta3N5_TaO56-Q",
# },
# "TaN203-Q": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TaN203",
# "substrate": "Quartz",
# },
# },
# "name": "N-Ta3N5_TaN203-Q",
# },
# "TaM20-Q": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TaM20",
# "substrate": "Quartz",
# },
# },
# "name": "M-Ta3N5_TaM20-Q",
# },
# "TiNTaN-1-Q-S3": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TiNTaN-1",
# "substrate": "Quartz",
# "sample_nr": "3",
# },
# },
# "name": "O-Ta3N5_TiNTaN-1-Q-S3",
# },
# "TaO06-Q": {
# "metadata": {
# "sample": {
# "name": "O-Ta3N5",
# "batch_nr": "TaO06",
# "substrate": "Quartz",
# },
# },
# "name": "O-Ta3N5_TaO06-Q",
# },
# "TaN176-Q": {
# "metadata": {
# "sample": {
# "name": "N-Ta3N5",
# "batch_nr": "TaN176",
# "substrate": "Quartz",
# },
# },
# "name": "N-Ta3N5_TaN176-Q",
# },
# "TaM03-Q": {
# "metadata": {
# "sample": {
# "name": "N-Ta3N5",
# "batch_nr": "TaM03",
# "substrate": "Quartz",
# },
# },
# "name": "M-Ta3N5_TaM03-Q",
# },
# }
test_meas_settings = {
"lock-in-amp": {
"time_constant_s": 1,
}
}
suffix = "v5.12"
default_ref_tra_kw = {
"param_file": r"C:/Users/Administrator/Desktop/2026-02-16_PMS.yaml",
"turn_off_pump": False,
"suffix": suffix,
"auto_determine_gain": True,
# "energies_eV": np.arange(round(nm_to_eV(800), 3), round(nm_to_eV(420), 3), deltaE).tolist(),
# "measurement_time_s": 60,
# "notes": "LED power density could not be calculated, but should be roughly around 2 mV/cm^2, according to last measurement",
# "notes": "Bandwidth is 5meV >= 500nm and 10meV < 500nm, for increased DC amplitude below band gap. Samples remained in same position in filter wheel after v5.9 wl=275 nm and wl=340 nm measurements",
# TESTING
# "energies_eV": ["offset_begin"] + [nm_to_eV(wl) for wl in [800, 700, 600, 500, 400]],
# "suffix": "v5.9_wl=275nm_Test",
# "do_offset": False,
# "measurement_time_s": 3,
# "ref_m": test_meas_settings,
# "tra_m": test_meas_settings,
# "extra_wait_time_s": 0,
}
ir_ref_tra_kw = {
"param_file": r"C:/Users/Administrator/Desktop/2026-02-16_PMS_IR.yaml",
"suffix": suffix + "_IR",
# TESTING
# "energies_eV": ["offset_begin"] + [nm_to_eV(wl) for wl in [1100, 1000, 900, 800]],
# "suffix": "v5.9_wl=275nm_IR_Test",
}
# Check for configuration errors
for i, pos in enumerate(do_pos):
slot = sample_slots[pos-1]
if slot is None:
print(f"Position {pos} will be skipped since slot is None")
continue
if not slot in ref_tra_kws:
answer = input(f"Slot '{slot}' does not have associated keywords! Do you want to continue? (y/*): ")
if answer == "y": continue
else: return
if (isinstance(wheel, DummyDevice) or wheel is None) and not isinstance(mcm, DummyDevice):
from devctrl.devices.wheel.impl.thorlabs_fw import ThorlabsFW
wheel = ThorlabsFW(port="COM17", slots=sample_slots)
wheel.slots = sample_slots
if dont_measure:
return
total_time = 0
i = 0
for i, pos in enumerate(do_pos):
slot = sample_slots[pos-1]
if slot is None:
log.info(f"Skipping pos {pos} since slot is None")
continue
log.debug(f"#{i+1} Setting wheel to {pos} with slot='{slot}'")
wheel.set_position(pos)
kw = default_ref_tra_kw | (ref_tra_kws[slot] if slot in ref_tra_kws else {})
kw_IR = default_ref_tra_kw | (ref_tra_kws[slot] if slot in ref_tra_kws else {}) | ir_ref_tra_kw
# sam.off()
# log.debug(f"Slot='{slot}', kw={kw_IR}")
# total_time += measure_ref_tra(**kw_IR)
sam.on()
log.debug(f"Slot='{slot}', kw={kw}")
total_time += measure_ref_tra(**kw)
plt.close('all')
# gain6 = { "pre-amp": { "coupling": "DC", "gain": 6 }}
# for slot in ["TaO53-Q", "TaO55-Q"]:
# i += 1
# wheel.set_slot(slot)
# kw = default_ref_tra_kw | (ref_tra_kws[slot] if slot in ref_tra_kws else {}) | dict(ref_m=gain6, tra_m=gain6, suffix="gain=6")
# log.debug(f"Slot='{slot}', kw={kw}")
# total_time += measure_ref_tra(**kw)
log.info(f"Performed {i+1} measurements in {duration_to_string(total_time)}")
shutter.close()
sam.off()
led.off()
[docs]
def merge_dicts(dict1, dict2, remove_None=True):
"""
Recursively merge the dictionaries. Values from dict2 take precedence over dict1.
If dict2 contains a key with value None, this value will be removed from dict1.
:param dict1:
:param dict2:
:return:
"""
dictf = dict1.copy()
keys = dict2.keys()
for key in keys:
if key not in dictf:
if remove_None and dict2[key] is None:
pass
else:
dictf[key] = dict2[key]
else:
if isinstance(dictf[key], dict) and isinstance(dict2[key], dict):
d = merge_dicts(dictf[key], dict2[key])
dictf[key] = d
else:
dictf[key] = dict2[key]
return dictf
[docs]
def frequency_scan():
default_ref_tra_kw = {
"wls": ["offset_begin", 436, 455, 515, 555, 580, 670],
"power_mWcm2": 5,
"turn_off_pump": False,
"do_offset": False,
# TESTING
# "wls": ["offset_begin", 420, 520, 550, 580, 690],
# "suffix": "TEST",
# "do_offset": False,
}
# use longer time constants and measurement times when not measuring in the sweet spot frequency range
# also adjust sensitivities
super_high_f_kw = {
# "ref_m": { "lock-in-amp": { "filter_slope": 18, "sensitivity_volt": 20e-6 } },
# "tra_m": { "lock-in-amp": { "filter_slope": 18, "sensitivity_volt": 20e-6 } },
"ref_m": {"lock-in-amp": {"time_constant_s": 10, "sample_rate_Hz": "max", "sensitivity_volt": 20e-6}},
"tra_m": {"lock-in-amp": {"time_constant_s": 10, "sample_rate_Hz": "max", "sensitivity_volt": 20e-6}},
"common_m": {"measurement_time_s": 60}
}
high_f_kw = {
"ref_m": { "lock-in-amp": { "time_constant_s": 10, "sample_rate_Hz": "max","sensitivity_volt": 50e-6 } },
"tra_m": { "lock-in-amp": { "time_constant_s": 10, "sample_rate_Hz": "max","sensitivity_volt": 50e-6 } },
"common_m": {"measurement_time_s": 60}
}
medium_f_kw = {
"ref_m": { "lock-in-amp": { "time_constant_s": 10, "sample_rate_Hz": "max", "sensitivity_volt": 100e-6 } },
"tra_m": { "lock-in-amp": { "time_constant_s": 10, "sample_rate_Hz": "max", "sensitivity_volt": 100e-6 } },
"common_m": { "measurement_time_s": 60 }
}
low_f_kw = {
"ref_m": { "lock-in-amp": { "time_constant_s": 10, "sample_rate_Hz": "max", "sensitivity_volt": 100e-6 } },
"tra_m": { "lock-in-amp": { "time_constant_s": 10, "sample_rate_Hz": "max", "sensitivity_volt": 100e-6 } },
"common_m": { "measurement_time_s": 100 }
}
super_low_f_kw = {
# will have higher signal, use higher initial sensitivity since every auto-increase will cost the wait time of 4 minutes
"ref_m": { "lock-in-amp": { "time_constant_s": 30, "sample_rate_Hz": "max", "sensitivity_volt": 200e-6 } },
"tra_m": { "lock-in-amp": { "time_constant_s": 20, "sample_rate_Hz": "max", "sensitivity_volt": 200e-6 } },
"common_m": { "measurement_time_s": 300 }
}
# These logspace values are chosen since they dont contain double values and 49, 50, 51 and 99, 100, 101 Hz
N_freq = 94
frequencies = np.logspace(1, 5, N_freq, dtype=int)
# frequencies = [823]
# N_freq = len(frequencies)
log.info(f"Frequencies: {frequencies}")
total_time = 0
i = 0
for i, f in enumerate(frequencies):
f = int(f) # make sure its an int and not a np.ing
log.info(f"Measurement {i+1} Frequency={f:06} Hz")
kw = default_ref_tra_kw | dict(frequency=f, suffix=f"f={f:06}Hz")
if f <= 50:
kw |= super_low_f_kw
elif f <= 100:
kw |= low_f_kw
elif f <= 1000:
kw |= medium_f_kw
elif f <= 10000:
kw |= high_f_kw
else:
kw |= super_high_f_kw
if i == N_freq-1:
kw["wls"].append("offset_end")
log.debug(f"kw={kw}")
total_time += measure_ref_tra(**kw)
plt.close("all")
# Time estimate from running this with dummy devices + 10s/wl for plotting and saving
# time_estimate = 17*3600+5*60 + N_freq * 10
time_estimate = 32*3600+48*60 + N_freq * 20
log.info(f"Performed {i+1} measurements in {duration_to_string(total_time)}, estimate was {duration_to_string(time_estimate)}")
[docs]
def mittwoch():
if halogen: halogen.on()
measure_ref_or_tra(mode=REFLECTION)
full_shutdown()
log.info(f"Waiting until 6 AM")
while True:
if time.localtime().tm_hour == 6:
log.info(f"Its 6 AM, yay! Starting Xenon Lamp.")
break
time.sleep(30)
xenon.on()
mcm.reset()
[docs]
def wochenende_251017():
i = 1
t = 0
for power, T, f in zip([0.5, 1, 0.5], [60, 60, 60], [333, 333, 32]):
log.info(f"WOCHENENDE MESSUNG #{i}, power={power}mW/cm2"); i += 1
t += measure_ref_tra(wls=(410, 800, 1), power_mWcm2=power, suffix=f"_v5.5_f={f}Hz_power={power}mWcm2", turn_off_pump=False, measurement_time_s=T, frequency=f)
log.info(f"Total time: {duration_to_string(t)}")
# full_shutdown()
shutter.close()
[docs]
def const_dE_test_251106(**kw):
t = 0
ref_m_1 = { "lock-in-amp": {"time_constant_s": 1} }
tra_m_1 = { "lock-in-amp": {"time_constant_s": 1} }
ref_m_3 = { "lock-in-amp": {"time_constant_s": 3} }
tra_m_2 = { "lock-in-amp": {"time_constant_s": 2} }
log.info(f"MESSUNG #1")
# t += measure_ref_tra(suffix="slow_dE=5meV_bw=5meV_T=60_tc=2-3s", turn_off_pump=False, measurement_time_s=60, ref_m=ref_m_3, tra_m=tra_m_2, **kw)
# log.info(f"MESSUNG #2")
t += measure_ref_tra(suffix="fast_dE=5meV_bw=5meV_T=20_tc=1s", turn_off_pump=True, measurement_time_s=20, ref_m=ref_m_1, tra_m=tra_m_1, **kw)
log.info(f"Total time: {duration_to_string(t)}")
# full_shutdown()
shutter.close()
[docs]
def measure_GaN(long=True):
if long:
return measure_ref_tra(measurement_time_s=120, power_mWcm2=5, frequency=333, suffix="dE=3meV_bw=5meV", turn_off_pump=True, deltaE=0.003)
else:
ref_m_1 = {"lock-in-amp": {"time_constant_s": 1}}
tra_m_1 = {"lock-in-amp": {"time_constant_s": 1}}
return measure_ref_tra(suffix="dE=4meV_bw=5meV", turn_off_pump=True, measurement_time_s=35, ref_m=ref_m_1, tra_m=tra_m_1, deltaE=0.004)
[docs]
def measure_Si_samples_both_lamps(**kw):
t = 0
deltaE = 0.005
energies_eV_xe = np.arange(round(nm_to_eV(800), 3), round(nm_to_eV(420), 3), deltaE).tolist()
energies_eV_ha = np.arange(round(nm_to_eV(1100), 3), round(nm_to_eV(750), 3), deltaE).tolist()
ref_xe = { "lock-in-amp": {"time_constant_s": 1}, "pre-amp": {"gain": 6}, "energies_eV": energies_eV_xe}
ref_ha = { "lock-in-amp": {"time_constant_s": 1}, "pre-amp": {"gain": 7}, "energies_eV": energies_eV_ha}
T = 60
name = f"TaON_TANO36-Q_v5.10_dE=5meV_bw=5meV_T={T}s"
# HA
log.info(f"MESSUNG Halogen")
sam.off()
t += measure_ref_or_tra(name=name, suffix="IR", turn_off_pump=False, measurement_time_s=T, m_params=ref_ha, **kw)
# XE
log.info(f"MESSUNG Xenon")
sam.on()
t += measure_ref_or_tra(name=name, suffix="", turn_off_pump=False, measurement_time_s=T, m_params=ref_xe, **kw)
log.info(f"Total time: {duration_to_string(t)}")
# full_shutdown()
sam.off()
shutter.close()
[docs]
def measure_Vis_IR(**kw):
deltaE = 0.005
name = f"TaON_TANO37-Q_v5.10"
suffix = ""
default_ref_tra_kw = {
"name": name,
"power_mWcm2": 5,
"turn_off_pump": False,
# "wls": (420, 800, 1),
"suffix": suffix,
"energies_eV": np.arange(round(nm_to_eV(800), 3), round(nm_to_eV(420), 3), deltaE).tolist(),
# "energies_eV": np.arange(round(nm_to_eV(600), 3), round(nm_to_eV(420), 3), deltaE).tolist(),
"measurement_time_s": 60,
# "notes": "LED power density could not be calculated, but should be roughly around 2 mV/cm^2, according to last measurement",
"notes": "Bandwidth is 5meV >= 500nm and 10meV < 500nm, for increased DC amplitude below band gap.",
# TESTING
# "energies_eV": ["offset_begin"] + [nm_to_eV(wl) for wl in [800, 700, 600, 500, 400]],
# "suffix": "v5.9_wl=275nm_Test",
# "do_offset": False,
# "measurement_time_s": 3,
# "ref_m": test_meas_settings,
# "tra_m": test_meas_settings,
# "extra_wait_time_s": 0,
}
ir_ref_tra_kw = {
"energies_eV": np.arange(round(nm_to_eV(1100), 3), round(nm_to_eV(750), 3), deltaE).tolist(),
"suffix": suffix + ("_" if suffix else "") + "IR",
"ref_m":{
"pre-amp": {
"gain": 7,
},
# "lock-in-amp": {
# "time_constant_s": 1, # TESTING!
# }
},
"tra_m": {
"pre-amp": {
"gain": 6,
},
# "lock-in-amp": {
# "time_constant_s": 1, # TESTING!
# }
},
# TESTING
# "energies_eV": ["offset_begin"] + [nm_to_eV(wl) for wl in [1100, 1000, 900, 800]],
# "suffix": "v5.9_wl=275nm_IR_Test",
}
t = 0
kw = default_ref_tra_kw
kw_IR = default_ref_tra_kw | ir_ref_tra_kw
# XE
log.info(f"MESSUNG Xenon")
sam.on()
t += measure_ref_tra(**kw)
# HA
# sam.off()
# log.info(f"MESSUNG Halogen")
# t += measure_ref_tra(**kw_IR)
# full_shutdown()
sam.off()
log.info(f"Total time: {duration_to_string(t)}")
shutter.close()
[docs]
def measure_temp(*args, **kwargs):
try:
_measure_temp(*args, **kwargs)
except Exception as e:
log.error(f"An exception occurred. Turning off the temperature controller before re-raising it.")
temp_controller.disable()
raise e
def _measure_temp(T=30):
# Total time: 03d 13h 56m 25s
t = 0
f = 333
power = 5
temp_controller.enable()
# set to save temperature
temp_controller.set_temperature_C(28)
i = 0
laser.set_power_mW(5)
laser.on()
for temp, wait_time_m in [
(30, 25),
# (31, 25),
# (32, 25),
# (33, 25),
# (34, 25),
# (35, 25),
(40, 25),
# (45, 25),
(50, 25),
(60, 25),
(70, 25),
]:
log.info(f"WOCHENENDE MESSUNG #{i+1}, temperature={temp} C")
suffix = f"v5.7_{i:02}_temp={temp}C"
temp_controller.set_temperature_C(temp)
log.info(f"Waiting for {wait_time_m} minutes for temperature to stabilize")
wait_time_s = wait_time_m*60
if not isinstance(temp_controller, DummyDevice):
time.sleep(wait_time_s)
t += wait_time_s
t += measure_ref_tra(wls=(420, 800, 1), power_mWcm2=power, suffix=suffix, turn_off_pump=False, measurement_time_s=T, frequency=f)
plt.close("all")
i += 1
# wait for below 30°
temp_controller.disable()
t += temp_controller.wait_for_temperature_C(29, n_digits=0, sleep_interval=10)
# measure at 30°C again (wait 25 minutes again)
temp = 30
suffix = f"v5.7_{i:02}_temp={temp}C"
temp_controller.set_temperature_C(temp)
temp_controller.enable()
wait_time_s = 25*60
if not isinstance(temp_controller, DummyDevice):
time.sleep(wait_time_s)
t += wait_time_s
t += measure_ref_tra(wls=(420, 800, 1), power_mWcm2=power, suffix=suffix, turn_off_pump=False, measurement_time_s=T, frequency=f)
log.info(f"Total time: {duration_to_string(t)}")
# full_shutdown()
shutter.close()
temp_controller.disable()
[docs]
def test_measurement(offsets=True, name="Test", **kw):
aux = "Aux In 4"
try: # import may fail
if isinstance(ref_lockin, mod_lock_in.impl.model7260.Model7260):
aux = "Aux In 1"
except: pass
measurement_params = {
"wavelengths_nm": list(range(530, 535, 1)),
"measurement_time_s": 1,
"auto_gain": False,
"auto_sensitivity": False,
}
measurement_params["wavelengths_nm"].reverse()
ref_meas_params = {
"pre-amp": {"gain": 7},
"lock-in-amp": {
"time_constant_s": 1,
"filter_slope": 12,
"sensitivity_volt": 10e-6,
"aux_DC": aux,
"sample_rate_Hz": 512,
}
}
laser.set_power_mW(1)
laser.on()
return measure_spectrum(common_measurement_settings=measurement_params, ref_measurement_settings=ref_meas_params, name=name, auto_add_offset_measurements=offsets, extra_wait_time_s=-5, save_spectrum=True, **kw)
[docs]
def measure_test(**kw):
mset = PrsSettings.from_dict(dict(modes={"ref": ModeSettings(), "tra": ModeSettings()}, general=MeasurementSettings(energies_eV=(3.0, 2.0, 0.05)), led=LedSettings()))
paramfile = os.path.expanduser(r"~/Desktop/prsctrl-test-params.yaml")
import yaml
if os.path.isfile(paramfile):
with open(paramfile, "r") as f:
settings = yaml.load(f, yaml.Loader)
msetload = PrsSettings.from_dict(settings)
print(f"Loaded from file:\n",msetload)
mset |= msetload
print(f"Combined:\n", mset)
print(mset)
print("MCM type:", type(mset.monochromators[0]))
print("mode type:", type(mset.modes["ref"]))
measure_spectrum(mset, **kw)
# ZEITABHÄNGIG
from devctrl.utility.file_io import get_date_time_for_filename
# from .RTA4004 import RTA4004
# def measure_waveform(osci: RTA4004, f_Hz=333, wl=580, n_avg=10000):
# basedir = r"D:\MQ_heseliges_zeit_PR"
# T_s = 1/f_Hz
# dataname = get_date_time_for_filename() + "_" + f"O-Ta3N5_TaO55-Q_wl={wl}_f={f_Hz}Hz"
# datadir = os.path.join(basedir, dataname)
# os.makedirs(datadir, exist_ok=False)
# n_data = 0
#
# print(f"MAKE SURE THE PUMP IS ON!")
# gain = 8
# ref_amplifier.set_gain(gain)
# tra_amplifier.set_gain(gain)
# time.sleep(3)
# print(f"MAKE SURE FEMTO OR OSCI COUPLING IS AC")
# mcm.set_wavelength_nm(wl)
# ref_lockin.set_frequency_Hz(f_Hz)
# shutter.open()
# range_s = T_s*1.3
# osci.dev.write_float("TIMebase:RANGe", range_s)
# range_s = osci.dev.query_float("TIMebase:RANGe?")
#
# metadata = {
# "osci_avg": n_avg,
# "frequency [Hz]": f_Hz,
# "time range [s]": range_s,
# "femto_gain": gain,
# }
# plt.ion()
# mon = Monitor(r"Index",
# [
# dict(ax=i, ylabel=ylabel, color=color) for i, (ylabel, color) in enumerate(zip(["CH1", "CH2 (Ref)", "CH3 (Tra)"], ["yellow", "green", "orange"]))
# ]
# )
# import pickle
# try:
# while True:
# log.info(f"Starting measurement nr {n_data}")
# mon.set_ax_title(f"Measurement Nr. {n_data}")
# osci.dev.write_int("ACQ:AVER:COUNt", n_avg)
# osci.dev.write("ACQ:AVER:RESET")
# # osci.dev.write_int("ACQuire:NSINgle:COUNt", n_avg)
# # osci.dev.write("RUNSingle")
# # while not (osci.dev.query("ACQ:STATe?").startswith("COMP") and osci.dev.query_int("ACQuire:AVER:COMP?") == 1):
# osci.dev.write("RUN")
# while osci.dev.query_int("ACQuire:AVER:COMP?") != 1:
# mon.idle()
# time.sleep(0.5)
# osci.dev.write("STOP")
# print(osci.dev.query("ACQ:AVER:COMP?"))
# data1 = osci.get_data(1)
# data2 = osci.get_data(2)
# data3 = osci.get_data(3)
# full_data = (np.vstack((data1, data2, data3)).T, metadata | {"timestamp end": time.time()})
# log.debug(f"Data {n_data} has size {data1.size}, {data2.size}, {data3.size}")
# xdata = np.arange(data1.size)
# mon.add_average(xdata, data1, data2, data3)
# datafilename = dataname + f"_{n_data:06}.pkl"
# datapath = os.path.join(datadir, datafilename)
# with open(datapath, "wb") as file:
# pickle.dump(full_data, file)
# n_data += 1
#
# except KeyboardInterrupt:
# print(f"Received keyboard interrupt - stopping")
#
# shutter.close()
# return mon
# POWER
[docs]
def full_shutdown():
global shutter, xenon, halogen, power_switch_PRS, power_switch_TAS
shutter.close()
xenon.off()
halogen.off()
power_switch_PRS.off()
power_switch_TAS.off()
[docs]
def help(topic=None):
if topic == None:
print("""
Functions:
*TODO* :(
Run 'help(function)' to see more information on a function
Available topics:
imports
configuration
Run 'help("topic")' to see more information on a topic""")
elif topic in [config_file, "configuration"]:
print(f"""Configuration:
Settings are stored in a configuration file, which is managed by the config_file object with the following methods:
config_file.set("setting", value) - get the value of a setting
config_file.get("setting") - set a setting to a value
config_file.save() - store the settings in the config file
config_file.load_settings() - load settings from a file
Other Functions:
name("<name>") - short for set("name", "<name>")
Settings:
name: str - name of the measurement, determines filename
data_dir_cache: str - output directory for the partial files during the measurement
data_dir_final: str - output directory for data directories containing only the final important files (full-data.pkl, plots, csv)
data_dir_archive: str - output directory for zip archives of the entire data directory
Upon startup, settings are loaded from the config file.
The global variable 'config_path' determines the path used by save/load_settings. Use -c '<path>' to set another path.
The search path is:
<working-dir>/{cfilename}
$XDG_CONFIG_HOME/{cfilename}
~/.config/photoreflectance/{cfilename}
The current file path is:
{config_path}
""")
elif topic == "imports":
print("""Imports:
numpy as np
pandas as pd
matplotlib.pyplot as plt
os.path """)
else:
print(topic.__doc__.strip(" ").strip("\n"))
[docs]
def connect_device(device_key: str):
"""
Connect one of the global devices interactively, and store the selection in the config file.
:param device_key: Device key, must be a device key managed by the device manager
"""
global config_file
dev = None
# get more helpful error message
if not device_key in devmg:
raise ValueError(f"Invalid device key: '{device_key}'. Valid are {devmg.get_keys()}")
devmg.connect_from_prompt(device_key)
globals().update(devmg.get_globals())
[docs]
def init():
global devmg, lockin, shutter, xenon, halogen, mcm, laser, power_switch_PRS, power_switch_TAS, ref_amplifier, led
global default_settings, config_path, config_file
print(r""" __ .__
_____________ ______ _____/ |________| |
\____ \_ __ \/ ___// ___\ __\_ __ \ |
| |_> > | \/\___ \\ \___| | | | \/ |__
| __/|__| /____ >\___ >__| |__| |____/
|__| \/ \/ """ + f"""{version}
Interactive Shell for Photoreflectance measurements
---
Enter 'help()' for a list of commands""")
parser = argparse.ArgumentParser(
prog="prsctrl",
description="measure photoreflectance",
)
backend_group = parser.add_mutually_exclusive_group(required=False)
parser.add_argument("-c", "--config", action="store", help="alternate path to config file")
parser.add_argument("--no-devices", action="store_true", help="do not initialize the device connections")
parser.add_argument("--dummy-devices", action="store_true", help="connect all devices as dummy devices")
args = vars(parser.parse_args())
from os import environ
# Load config file
if path.isfile(cfilename):
config_path = cfilename
elif 'XDG_CONFIG_HOME' in environ.keys():
config_path = path.join(environ["XDG_CONFIG_HOME"], "prsctrl", cfilename)
else:
config_path = path.join(path.expanduser("~/.config/prsctrl"), cfilename)
if args["config"]:
config_path = args["config"]
config_file = ConfigFile(config_path, init_values=default_settings)
# setup logging
log_format = "%(asctime)s [%(levelname)s] [%(name)s/%(funcName)s] %(message)s"
log_dir = path.expanduser(config_file.get("log_dir"))
makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, get_date_for_filename() + "_prsctrl-interactive.log")
log_stdout = logging.StreamHandler()
log_stdout.setFormatter(ColorFormatter(log_format))
PackageFilter.DEBUG_FOR_PKG = ["prsctrl", "devctrl", "__main__", "mqutil"]
log_stdout.addFilter(PackageFilter())
log_file = logging.FileHandler(log_path)
log_file.addFilter(PackageFilter())
log_file.setFormatter(logging.Formatter(log_format))
logger = logging.getLogger() # root logger
logger.addHandler(log_stdout)
logger.addHandler(log_file)
logger.setLevel(logging.DEBUG)
if not path.isdir(config_file.get("data_dir_cache")):
makedirs(config_file.get("data_dir_cache"))
# Register device roles with device manager, will be connected later
devmg = DeviceManager(config_file=config_file, prompt_choic_func=select_device_interactive)
devmg.add_device(key="power_switch-PRS", name="Power Switch (PRS)", global_varname="power_switch_PRS",
device_type=Switch, connect_priority=100)
devmg.add_device(key="power_switch-TAS", name="Power Switch (TAS)", global_varname="power_switch_TAS",
device_type=Switch, connect_priority=99)
devmg.add_device(key="shutter", name="Shutter", global_varname="shutter", device_type=Shutter)
devmg.add_device(key="lamp-xenon", name="Xenon lamp", global_varname="xenon", device_type=Lamp)
devmg.add_device(key="lamp-halogen", name="Halogen lamp", global_varname="halogen", device_type=Lamp)
devmg.add_device(key="monochromator", name="Monochromator", global_varname="mcm", device_type=Monochromator)
devmg.add_device(key="monochromator-detector", name="Monochromator-Detector", global_varname="kymera",
device_type=Monochromator)
devmg.add_device(key="ref_lock-in", name="Lock-In (REF)", global_varname="ref_lockin", device_type=LockInAmp)
devmg.add_device(key="tra_lock-in", name="Lock-In (TRA)", global_varname="tra_lockin", device_type=LockInAmp)
devmg.add_device(key="laser", name="Laser", global_varname="laser", device_type=Laser)
devmg.add_device(key="ref_amplifier", name="Amplifier (REF)", global_varname="ref_amplifier", device_type=Amplifier)
devmg.add_device(key="tra_amplifier", name="Amplifier (TRA)", global_varname="tra_amplifier", device_type=Amplifier)
devmg.add_device(key="led_controller", name="LED Controller", global_varname="led", device_type=LedController)
devmg.add_device(key="temperature-sensor", name="Temperature Sensor", global_varname="temp_sensor", device_type=TemperatureSensor)
devmg.add_device(key="wheel-sample", name="Sample Wheel", global_varname="wheel", device_type=Wheel)
devmg.add_device(key="lamp-mirror", name="Lamp Mirror (SAM)", global_varname="sam", device_type=Switch)
# init the devices
if not (args["no_devices"] or args["dummy_devices"]):
for i, key in enumerate(devmg):
# assign to global by variable name as string
dev = devmg.connect_from_config_then_prompt(key, n_prompt_tries=2)
if key == "power_switch-PRS" and (not isinstance(dev, DummyDevice)) and (not dev.is_on()):
dev.on()
print("Waiting for devices to power up")
time.sleep(10)
elif args["dummy_devices"]:
log.info("Connecting dummy devices")
for k in devmg:
devmg.connect_dummy(k)
globals().update(devmg.get_globals())
try:
plt.style.use(["mqutil.plot.styles.origin", "mqutil.plot.styles.font-sans"]), # "mqutil.plot.styles.tex-font-sans", not using tex font since its slow AF
except Exception as e:
log.debug(f"Failed to set matplotlib styles: {e}")
# atexit.register(_backend.exit, dev)
if __name__ == "__main__":
init()