2025-05-06 15:45:38 +02:00

374 lines
15 KiB
Python

import pyvisa
if __name__ == "__main__":
import sys
if __package__ is None:
# make relative imports work as described here: https://peps.python.org/pep-0366/#proposed-change
__package__ = "photoreflectance"
from os import path
filepath = path.realpath(path.abspath(__file__))
sys.path.insert(0, 'C:\\Users\Administrator\Desktop\Software\Python\Python\github')
from time import sleep
import numpy as np
import scipy as scp
from Bentham import Bentham, DummyBentham
from devices.Xenon import Xenon
from devices.Shutter import ShutterProbe, DummyShutter
from .update_funcs import Monitor
from .measurement_device.impl.sr830 import SR830
from .measurement_device.impl.model7260 import Model7260
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
handlers=[
# logging.FileHandler(log_path),
logging.StreamHandler()
]
)
log = logging.getLogger(__name__)
def set_measurement_params(lockin: SR830, p: dict={}, **kwargs):
params = p | kwargs
key_to_setter = {
"time_constant_s": lockin.set_time_constant_s,
"filter_slope": lockin.set_filter_slope,
"sync_filter": lockin.set_sync_filter,
"reserve": lockin.set_reserve,
"sensitivity_volt": lockin.set_sensitivity_volt,
}
for k, v in params.items():
if k not in key_to_setter.keys():
raise KeyError(f"Invalid parameter {k}")
key_to_setter[k](v)
def set_offset_laser_only(lockin: SR830, shutter: ShutterProbe, wait_time_s):
"""
Set the R offset from the signal when only the laser is on.
This signal should be stray laser light and laser induced PL
:return: Offset as percentage of the full scale R
"""
log.info("Setting offset when the lamp is off.")
shutter.close_()
sleep(wait_time_s + 10)
lockin.run("AOFF 3") # auto offset R
R_offset_fs = float(lockin.query("OEXP? 3").split(",")[0]) # returns R offset and expand
return R_offset_fs
def _measure_both_sim(monochromator: Bentham, lockin: SR830, shutter: ShutterProbe, wl_range=(400, 750, 25), aux_DC="Aux In 4", offset_with_laser_only=True, monitor=None):
data = {}
lockin_params = {
"time_constant_s": 10,
# "time_constant_s": 100e-3,
"sensitivity_volt": 50e-6,
"filter_slope": 12,
"sync_filter": 1,
"reserve": "Normal",
}
measurement_params = {
"measurement_time_s": 30,
"sample_rate_Hz": 512,
}
set_measurement_params(lockin, lockin_params)
measurement_time_s = measurement_params["measurement_time_s"]
sample_rate_AC = measurement_params["sample_rate_Hz"]
n_bins_AC = measurement_time_s * sample_rate_AC # x sec messen mit <sample_rate> werte pro sekunde
timeout_s = 60
timeout_interval = 0.5
# trigger on the falling edge, since the light comes through when the ref signal is low
# could of course also trigger on rising and apply 180° shift
lockin.run("RSLP 2")
# since we dont expect changes in our signal, we can use larger time constants and aggressive filter slope
# for better signal to noise
wait_time_s = lockin.get_wait_time_s()
def run_lockin_cmd(cmd, n_try=2):
com_success = n_try
e = None
while com_success > 0:
try:
return cmd()
except pyvisa.VisaIOError as e:
lockin.try_recover_from_communication_error(e)
com_success -= 1
raise e
# 5s for setting buffer,
# 5s for get values and plot
print(f"Time estimate {(measurement_time_s + wait_time_s + 5 + 5)/60 * ((wl_range[1]-wl_range[0])/wl_range[2])} minutes")
input("Make sure the laser is turned on and press enter > ")
mon = monitor if monitor is not None else Monitor(r"$\lambda$ [nm]", [
dict(ax=0, ylabel=r"$\Delta R$", color="green"),
dict(ax=1, ylabel=r"$\sigma_{\Delta R}$", color="green"),
dict(ax=2, ylabel=r"$R$", color="blue"),
dict(ax=3, ylabel=r"$\sigma_R$", color="blue"),
dict(ax=4, ylabel=r"$\Delta R/R$", color="red"),
dict(ax=5, ylabel=r"$\sigma_{\Delta R/R}$", color="red"),
])
mon.set_fig_title(f"Turn on laser and plug detector into A and {aux_DC} ")
data["lock-in-params"] = lockin_params
data["measurement-params"] = measurement_params
full_scale_voltage = lockin_params["sensitivity_volt"]
if offset_with_laser_only:
mon.set_fig_title(f"Measuring baseline with lamp off")
R_offset_volt = set_offset_laser_only(lockin, shutter, wait_time_s) * full_scale_voltage
data["R_offset_volt_before"] = R_offset_volt
print(f"R_offset_volt_before {R_offset_volt}")
data["reference_freq_Hz_before"] = lockin.get_frequency()
shutter.open_()
for i_wl, wl in enumerate(range(*wl_range)):
mon.set_ax_title(f"$\\lambda = {wl}$ nm")
run_lockin_cmd(lambda: lockin.buffer_setup(CH1="R", CH2=aux_DC, length=n_bins_AC, sample_rate=sample_rate_AC))
mon.set_fig_title(f"Setting wavelength to {wl} nm")
monochromator.drive(wl)
mon.set_fig_title(f"Waiting for signal to stabilize")
# wait the wait time
sleep(wait_time_s)
mon.set_fig_title(f"Measuring...")
run_lockin_cmd(lambda: lockin.buffer_start_fill())
t = timeout_s
while t > 0:
t -= timeout_interval
sleep(timeout_interval)
if run_lockin_cmd(lambda: lockin.buffer_is_done()):
break
if t < 0: raise RuntimeError("Timed out waiting for buffer measurement to finish")
arr = run_lockin_cmd(lambda: lockin.buffer_get_data(CH1=True, CH2=True))
data[wl] = {}
data[wl]["raw"] = arr
# calculate means
means = np.mean(arr, axis=1)
errs = np.std(arr, axis=1)
dR = means[0]
R = means[1]
sdR = errs[1]
sR = errs[1]
data[wl] |= {"dR": dR, "sdR": sdR, "R": R, "sR": sR}
dR_R = dR / R
sdR_R = np.sqrt((sdR / R) + (dR * sR/R**2))
data[wl] |= {"dR_R": dR_R, "sdR_R": sdR_R}
mon.update(wl, dR, sdR, R, sR, dR_R, sdR_R)
# if it fails, we still want the data returned
try:
if offset_with_laser_only:
mon.set_fig_title(f"Measuring baseline with lamp off")
R_offset_volt = set_offset_laser_only(lockin, shutter, wait_time_s) * full_scale_voltage
data["R_offset_volt_after"] = R_offset_volt
print(f"R_offset_volt_after {R_offset_volt}")
data["reference_freq_Hz_after"] = lockin.get_frequency()
except Exception as e:
print(e)
mon.set_fig_title("Photoreflectance")
mon.set_ax_title("")
return data, mon
def _measure_both(monochromator: Bentham, lockin: SR830, shutter: ShutterProbe, wl_range=(400, 750, 25), AC=True, DC=True, monitor=None):
mon = monitor if monitor is not None else Monitor(r"$\lambda$ [nm]", [
# dict(ax=0, ylabel="Wavelength [nm]", color="red"),
# dict(ax=1, ylabel="Ref", color="blue", lim=(0, 5)),
dict(ax=0, ylabel=r"$\Delta R$", color="green"),
dict(ax=1, ylabel=r"$\sigma_{\Delta R}$", color="green"),
dict(ax=2, ylabel=r"$R$", color="blue"),
dict(ax=3, ylabel=r"$\sigma_R$", color="blue"),
dict(ax=4, ylabel=r"$\Delta R/R$", color="red"),
# dict(ax=3, ylabel="R", color="blue"),
dict(ax=5, ylabel=r"$\theta$", color="orange", lim=(-180, 180)),
dict(ax=6, ylabel=r"$\sigma_\theta$", color="orange")
])
shutter.open_()
data_raw = []
data_wl = {}
# TODO these are only printed, not set!
time_constant = 30e-3
filter_slope = 24
sensitivity = 1.0
SYNC = 1
sample_rate_AC = 64
sample_rate_DC = 512
n_bins_AC = 3 * sample_rate_AC # x sec messen mit <sample_rate> werte pro sekunde
n_bins_DC = 10 * sample_rate_DC
timeout_s = 60
timeout_interval = 0.5
# lockin.run("SENS 17") # 1 mV
# lockin.run("SENS 20") # 10 mV
# lockin.run("SENS 21") # 20 mV
lockin.run("SENS 26") # 1 V
# trigger on the falling edge, since the light comes through when the ref signal is low
# could of course also trigger on rising and apply 180° shift
lockin.run("RSLP 2")
# since we dont expect changes in our signal, we can use larger time constants and aggressive filter slope
# for better signal to noise
# lockin.run("OFLT 5") # 3 ms
lockin.run("OFLT 7") # 30 ms
# lockin.run("OFLT 8") # 100 ms
# lockin.run("OFLT 10") # 1 s
lockin.run("RMOD 2") # low noise (small reserve)
# lockin.run("OFSL 0") # 6dB/Oct
lockin.run("OFSL 3") # 24dB/Oct
lockin.run(f"SYNC {SYNC}") # sync filter
print(f"Time estimate {40 * (wl_range[1]-wl_range[0])/(wl_range[2]*60)} minutes")
if AC:
input("Plug the detector into lock-in port 'A/I' (front panel) and press enter > ")
input("Make sure the laser is turned on and press enter > ")
mon.set_fig_title("Turn on laser and plug detector into A")
for i_wl, wl in enumerate(range(*wl_range)):
mon.set_ax_title(f"$\\lambda = {wl}$ nm")
lockin.buffer_setup(CH1="R", CH2="Theta", length=n_bins_AC, sample_rate=sample_rate_AC)
mon.set_fig_title(f"Setting wavelength to {wl} nm")
monochromator.drive(wl)
mon.set_fig_title(f"Waiting for signal to stabilize")
# wait time depends on filter and time constant, for 24dB/Oct and Sync on the minimum is ~12 time constants
sleep(1.0)
mon.set_fig_title(f"Measuring...")
lockin.buffer_start_fill()
t = timeout_s
while t > 0:
t -= timeout_interval
sleep(timeout_interval)
if lockin.buffer_is_done():
break
if t < 0: raise RuntimeError("Timed out waiting for buffer measurement to finish")
arr = lockin.buffer_get_data(CH1=True, CH2=True)
data_raw.append([wl, arr])
# calculate means, for theta use circular mean
dR = np.mean(arr[0,:])
sdR = np.std(arr[0,:])
theta = scp.stats.circmean(arr[1,:], low=-180, high=180)
stheta = scp.stats.circstd(arr[1,:], low=-180, high=180)
data_wl[wl] = {"dR": dR, "Theta": theta, "sdR": sdR, "sTheta": stheta}
# wl - dR, sdR, R, sR, dR/R, Theta
mon.update(wl, dR, sdR, None, None, None, theta, stheta)
if DC:
mon.set_ax_title("")
mon.set_fig_title("Turn off laser and plug detector into Aux 1")
input("Turn off the laser and press enter > ")
input("Plug the detector into lock-in port 'Aux In 1' (rear panel) and press enter > ")
for i_wl, wl in enumerate(range(*wl_range)):
mon.set_ax_title(f"$\\lambda = {wl}$ nm")
lockin.buffer_setup(CH1="Aux In 1", CH2="Theta", length=n_bins_DC, sample_rate=sample_rate_DC)
mon.set_fig_title(f"Setting wavelength to {wl} nm")
monochromator.drive(wl)
sleep(0.5)
mon.set_fig_title(f"Measuring...")
lockin.buffer_start_fill()
t = timeout_s
while t > 0:
t -= timeout_interval
sleep(timeout_interval)
if lockin.buffer_is_done():
break
if t < 0: raise RuntimeError("Timed out waiting for buffer measurement to finish")
arr = lockin.buffer_get_data(CH1=True, CH2=False)
if AC:
data_raw[i_wl].append(arr)
else:
data_raw.append([wl, arr])
means = np.mean(arr, axis=1)
errs = np.std(arr, axis=1)
if not wl in data_wl: data_wl[wl] = {}
data_wl[wl] |= {"R": means[0], "sR": errs[0]}
# wl - dR, sdR, R, sR, dR/R, Theta
if AC:
dR_R = data_wl[wl]["dR"] / data_wl[wl]["R"]
mon.override(wl, None, None, data_wl[wl]["R"], data_wl[wl]["sR"], dR_R, None, None)
else:
mon.update(wl, None, None, data_wl[wl]["R"], data_wl[wl]["sR"], None, None, None)
mon.set_fig_title(f"Time constant = {time_constant} s\nFilter slope = {filter_slope} dB/oct\nSync Filter = {SYNC}\nSensitivity = {sensitivity*1e3} mV")
mon.set_ax_title("")
return data_wl, data_raw, mon
def _measure(monochromator: Bentham, lamp: Xenon, lockin: SR830, shutter: ShutterProbe, wl_range=(400, 750, 25)):
data = []
mon = Monitor(r"$\lambda$ [nm]", [
# dict(ax=0, ylabel="Wavelength [nm]", color="red"),
# dict(ax=1, ylabel="Ref", color="blue", lim=(0, 5)),
dict(ax=0, ylabel=r"$\Delta R$", color="green"),
dict(ax=1, ylabel=r"$\sigma_{\Delta R}$", color="green"),
dict(ax=2, ylabel="Phase", color="orange", lim=(-180, 180))
# dict(ax=3, ylabel="R", color="blue"),
])
N_bins = 100
dt = 0.01
i = 0
shutter.open_()
if isinstance(lockin, SR830):
lockin.run("SENS 17") # 1 mV/nA
lockin.run("OFLT 5")
for wl in range(*wl_range):
arr = np.empty((N_bins, 2))
monochromator.drive(wl)
# lockin.auto_gain()
for j in range(N_bins):
dR, theta = lockin.snap(what="3,4")
arr[j,:] = (dR, theta)
i += 1
# sleep(dt)
means = np.mean(arr, axis=0)
errs = np.std(arr, axis=0)
mon.update(wl, means[0], errs[0], means[1])
data.append((wl, arr))
elif isinstance(lockin, Model7260):
# lockin.run("SEN 21") # 10 mV/nA
lockin.run("SEN 24") # 100 mV/nA
for wl in range(*wl_range):
monochromator.drive(wl)
lockin.buffer_setup(MAG=1, PHASE=1, ADC1=1, ADC2=1, interval_ms=5, length=1000)
lockin.buffer_start_fill()
timeout_s = 60
timeout_interval = 0.5
while timeout_s > 0:
timeout_s -= timeout_interval
sleep(timeout_interval)
if lockin.buffer_is_done():
break
arr = lockin.buffer_get_data()
length = arr.shape[0]
# for j in range(length):
# # wl, ref, dR, R, theta
# mon.update(i*length+j, wl, arr[j][3], arr[j][0], arr[j][2], arr[j][1])
mon.update_array(range(i * length, (i+1)*length), [wl for _ in range(length)], arr[:,3], arr[:,0], arr[:,2], arr[:,1])
data.append((wl, arr))
i += 1
shutter.close_()
return data, mon
lockin = None
lamp = None
mcm = None
shutter = None
def measure(wl_range=(400, 500, 2)):
return _measure(mcm, lamp, lockin, shutter, wl_range=wl_range)
def measure_both(**kwargs):
return _measure_both(mcm, lockin, shutter, **kwargs)
def measure_both_sim(**kwargs):
return _measure_both_sim(mcm, lockin, shutter, **kwargs)
if __name__ == "__main__":
mcm = Bentham()
shutter = ShutterProbe()
# mcm.park()
lamp = Xenon()
lockin = SR830.connect_device(SR830.enumerate_devices()[0])
# lockin = Model7260.connect_device(Model7260.enumerate_devices()[0])
# mcm = DummyBentham()
# shutter = DummyShutter()