From 5894b5c778f9cc0f3e1061905276b62e66af4fb8 Mon Sep 17 00:00:00 2001 From: JohannesDittloff Date: Tue, 6 May 2025 15:45:38 +0200 Subject: [PATCH] improve everything --- Bentham.py | 7 + devices/Shutter.py | 5 + devices/__pycache__/Shutter.cpython-311.pyc | Bin 4047 -> 4859 bytes .../impl/__pycache__/sr830.cpython-311.pyc | Bin 14592 -> 21800 bytes measurement_device/impl/sr830.py | 126 +++++++++- test.py | 215 ++++++++++++++++-- update_funcs.py | 15 ++ 7 files changed, 343 insertions(+), 25 deletions(-) diff --git a/Bentham.py b/Bentham.py index 69758b3..124a283 100644 --- a/Bentham.py +++ b/Bentham.py @@ -2,6 +2,13 @@ import pyBen from os import path +class DummyBentham: + def __init__(self): + pass + def return_parameters(self): return {} + def park(self): print("Parked") + def drive(self, wl): print(f"Set to {wl} nm") + class Bentham(): """ Controls the Bentham TMC300 monochromator. diff --git a/devices/Shutter.py b/devices/Shutter.py index 99fce9e..2108f95 100644 --- a/devices/Shutter.py +++ b/devices/Shutter.py @@ -4,6 +4,11 @@ class Shutter(): def __init__(self): self.daq_name = 'Dev1' +class DummyShutter(Shutter): + def __init__(self): + super().__init__() + def open_(self): print("Dummy shutter open") + def close_(self): print("Dummy shutter close") class ShutterPump(Shutter): """ diff --git a/devices/__pycache__/Shutter.cpython-311.pyc b/devices/__pycache__/Shutter.cpython-311.pyc index ad0ec6960159a53ed115c6343c4206e8b31e39f8..f91093763bc1ce00eb9d8d17c893efb20d946d06 100644 GIT binary patch delta 1536 zcmbtU&1(}u6rb6A>^8}!_9LP8%dNJyX|W$sC{!t01TQLuTCo9HYIaMjNfRb1*dU~b zLVFQf2I;|zSg3@0k@{D7&=h)*J&A%KqEss)dhxxPrr&sRclNhCZ|42x&F{V0Hz)qI z1fS`;kHEDRtR2s&i@_TDez7%vQz2J~rOgtn%vM%hgsj3Nub9X7TE2S<_E?8SEPp~x zsxg%kh1lu}SXbplz?f!hcgsrMCqDkd)lwE1A{IOmeewGoE$|$Dtp{LAfiN9j%+H6F zKC1HtrLngi40B|O*d#_R5>w{M7`5oE7IRt3EQz@-7ocaJ+@~=wFuI+<<*8(Hns?C# z{?Ij|30{Ty_*YlB$&c)4$%VwMq4B9~)@GfiIWakrHBF5-yW8>@L!1JXF5EaiTg?DV zB)ezKqASo_-LX(&5eP%Mo~slq7#X>QkxJW>1?qUxY+^FYP>>8d84P1`JUDDhpZs(w zEMdgdWWoS5*44|I9e<<5o|Oq#eWA8gcw#AO#GM3NomeH?MXx9PSy`)jGh*Yl+`6E23 zpSXQAm4Q@MCG`ZAlusyk9^Fwsb`Ig7D!ph#IK@Bd+oJUHTS0^R`E;;hSd<5fCML-Q z^h4KCtfZrF7loO|Rhfa)DiGvv5x#zF7m_J!Dq;6CEPMQgWT41C1_wGcH$A&a4#Pt4 RH?;oP9SZPB`BOJ?{sr`_N3s9_ delta 832 zcmZ{i&x_MQ6vs1(P1B@_Y3bVTy8R)p-EJ$Tq9ClY_#Y^&ds&dNO#+*mHg(c2Y%lfZ zxjgW^xZqOsA{7t*0bT_4B$A_`XFaZ7oR_RnK|}J%%y-`R@!n&8UHVWqj&)rX`1AFh zx%Wn%8U^xpx^8Vc30rv};sI;ef}Qf@p1dm)K@=SM8CRdxJlCRLM+XHBpL*|lCDGnB1^$UI#qFw7Yy{DIS_IGP`))`)hV2eMT}5k9G5le&t=B6Q92e~fiFGz-nk7lOtGYuZ1F_ z1mDcl_FAx$sZ7++92|J{2ER;XSFPvnq5EXW^PtTzBHx(JOr~m)} diff --git a/measurement_device/impl/__pycache__/sr830.cpython-311.pyc b/measurement_device/impl/__pycache__/sr830.cpython-311.pyc index 3a2f972db51f90e5487b316c4577d2506727c785..af25bee80d183aca6db84133c4b06df706c836f4 100644 GIT binary patch delta 7421 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 YqK+(jdwkaz$804S!z1ONa4 delta 1638 zcma)*SxlT&6oCKxGykj%44q+}g}pe?0z-{#k`gG`c3^;+v6``z4&EP13e0dB!cr70 zV@skjkz=e6wr;UX)ih*Yjj4@^uYGCKOl;B@AMgnbCPq!;IscTVHkx?z=R3w=m%)EwMaNfeR(aPplq?P9&b9gQ?cUUd+ARg`p+9mY1S*qAWw{;^mm-Rz19q zS0>qYyvSE{Ic-Er(O;F6p7NIQYRoC;`60=BD6NEosjKp^g)OE2w2sThv|gx*IWZ0I zmKVT7XCIvL7%iJ}S{a*+MH8X{W?T+f_EfS__}JrxQQK>593I#_&oDTe4ltEX9a))>JeuT2TXoqluk^Hiw@{@P@O> z)JqHG)67k0HCuzbPLM?l9>&L#@R{o!c33-DuiMVnA>!`S_fSHw=Dg*uvAu=ntO2nq z-7{vd>tL@jUNw$3jt57#8495wwsiy}nO;IIBz@)C1EdZi0^;S=mhZM=NGlW5TwQ>L z=7S|^^Qq|MoZ2HqOn8l%%MVk=S=eemD{n|Cd&{1zZzNZ)cB1n3I{8}(0`1pX5B%2t zqWrxCea~;0uypklMOC&}h#2Zi#hHSQ&W(+!Vq{!Rj7*QjV-wSf*&n1-e#cL8@p(#l zg+MphWNC#b7UQZ2g1NJ;ISkIwvd=Yag9 zl)B$p&sZ>(-F2F^9;L-+5leE%mx3|iM7 zY4Vyf&$33LNrXYXN$LgYIbPXz3DsE>qHKR;8g>y~O9h$wk38`JJer7ZQ%*r@^SFbV z@8#OpEn0V`nofqXYH-;<;HFxnbwZ7D?Y9v05ZZpPrsdLBWV9)H)$^rP7wTrenO^8NQ)9C2wDS7p_bN- z#$uBqPEM_psfEQd;WB|LDge$57Fs?e^%|iURtH^jmJHVhqpmWh)P69xBcXU2`i-GV zlcc!r=$!b$siAtNhl{}qri0Ie8xE`D*pdDT5BNhfdLM1F3b#WwY#x3IJ!kuf9CZY( jeS4uXT*SVC6XB~}ojO*yBmFz?WV?5@slERQ(4OFL;v9y7 diff --git a/measurement_device/impl/sr830.py b/measurement_device/impl/sr830.py index 1f3c92c..767598b 100644 --- a/measurement_device/impl/sr830.py +++ b/measurement_device/impl/sr830.py @@ -95,8 +95,6 @@ class SR830(MeasurementDevice): except pyvisa.VisaIOError as e: raise RuntimeError(f"VisaIOError raised while writing command(s):\n'{cmd}'\n\nVisaIOError:\n{e}") - def get_frequency(self) -> float: - return float(self.query("FREQ?")) def query(self, query): return self.instr.query(query, delay=0.05).strip("\n") @@ -127,8 +125,37 @@ class SR830(MeasurementDevice): vals = self.query(f"SNAP? {what}").split(",") vals = map(float, vals) return vals - - def measureTODO(): pass + + def try_recover_from_communication_error(self, original_error): + """ + Try to get into a normal state by flushing the output queue + and reading the instrument status + """ + log.warning(f"Trying to recover from communication error: {original_error}") + # flush whatever is in the queue + try: + self.instr.read_raw() + except pyvisa.VisaIOError as e: + pass + try: + status = int(self.query("*ESR?")) + if status & 0b00110101 > 0: # check if INP, QRY, EXE or CMD is set + raise RuntimeError(f"Failed to recover from exception, device returned status {status:08b}:\n{original_error}") + except Exception as e: + raise RuntimeError(f"Failed to recover from the following exception:\n{original_error}\nThe following exception occurred while querying the device status:\n{e}") + log.info(f"Recovered from error") + + def check_overloads(self) -> bool: + status_lia = int(self.query("LIAS?")) + if status_lia & (1 << 0): # input amplifier overload + return True + elif status_lia & (1 << 1): # time constant filter overlaid + return True + elif status_lia & (1 << 2): # output overload + return True + return False + + def measureTODO(self): pass def read_value(self): """Read the value of R""" return float(self.query("OUTP? 3")) @@ -139,10 +166,94 @@ class SR830(MeasurementDevice): def test_connection(self): pass + # PARAMETERS + def get_frequency(self) -> float: + return float(self.query("FREQ?")) + + SENS = [ + 2e-9, 5e-9, 10e-9, 20e-9, 50e-9, 100e-9, 200e-9, 500e-9, + 1e-6, 2e-6, 5e-6, 10e-6, 20e-6, 50e-6, 100e-6, 200e-6, 500e-6, + 1e-3, 2e-3, 5e-3, 10e-3, 20e-3, 50e-3, 100e-3, 200e-3, 500e-3, + 1 + ] + def set_sensitivity_volt(self, volt): + if volt not in SR830.SENS: + raise ValueError(f"Invalid sensitivity voltage value: {volt}") + sens = SR830.SENS.index(volt) + self.run(f"SENS {sens}") + + def get_sensitivity_volt(self): + sens = int(self.query(f"SENS")) + return SR830.SENS[sens] + + OFLT =[ + 10e-6, 30e-6, 100e-6, 300e-6, + 1e-3, 3e-3, 10e-3, 30e-3, 100e-3, 300e-3, + 1, 3, 10, 30, 100, 300, + 1e3, 3e3, 10e3, 30e3, + ] + def set_time_constant_s(self, dt): + if dt not in SR830.OFLT: + raise ValueError(f"Invalid time constant value: {dt}. Must be one of {SR830.OFLT}") + oflt = SR830.OFLT.index(dt) + self.run(f"OFLT {oflt}") + + def get_time_constant_s(self): + oflt = int(self.query("OFLT?")) + return SR830.OFLT[oflt] + + + OFSL = [6, 12, 18, 24] + def set_filter_slope(self, slope_db_oct): + if slope_db_oct not in SR830.OFSL: + raise ValueError(f"Invalid filter slope value: {slope_db_oct}. Must be one of {SR830.OFSL}") + ofsl = SR830.OFSL.index(slope_db_oct) + self.run(f"OFSL {ofsl}") + + def get_filter_slope(self): + ofsl = int(self.query("OFSL?")) + return SR830.OFSL[ofsl] + + def get_wait_time_s(self): + """ + Get the wait time required to reach 99% of the final value. + See Manual 3-21 + :return: + """ + time_const = self.get_time_constant_s() + filter_slope = self.get_filter_slope() + if filter_slope == 6: return 5 * time_const + elif filter_slope == 12: return 7 * time_const + elif filter_slope == 18: return 9 * time_const + elif filter_slope == 24: return 10 * time_const + else: + raise ValueError(f"Invalid filter slope value: {filter_slope}") + + def set_sync_filter(self, sync): + if sync not in [0, 1]: + raise ValueError(f"Invalid sync value: {sync}, must be 0 (off) or 1 (on)") + self.run(f"SYNC {sync}") + + def get_sync_filter(self): + sync = int(self.query("SYNC?")) + return sync + + RMOD = ["High Reserve", "Normal", "Low Noise"] + def set_reserve(self, reserve): + if not reserve in SR830.RMOD: + raise ValueError(f"Invalid reserve value: {reserve}. Must be one of {SR830.RMOD}") + rmod = SR830.RMOD.index(reserve) + self.run(f"RMOD {rmod}") + + def get_reserve(self): + rmod = int(self.query("RMOD?")) + return SR830.RMOD[rmod] + + + CH1 = ["X", "R", "X Noise", "Aux In 1", "Aux In 2"] CH2 = ["Y", "Theta", "Y Noise", "Aux In 3", "Aux In 4"] SRAT = [62.5e-3, 125e-3, 250e-3, 500e-3, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, "Trigger"] - def buffer_setup(self, CH1="R", CH2="Theta", length=None, sample_rate=512): """ Prepare the device for a buffer (curve) measurement @@ -243,6 +354,11 @@ class SR830(MeasurementDevice): # struct.unpack("=f", self.instr.read_bytes(4))[0] if remainder > 0: data[chunks * CHUNK_SIZE:] = np.frombuffer(self.instr.read_bytes(remainder * 4), dtype=np.float32) + try: + read = self.instr.read_raw() + log.warning(f"Unexpected read from device: {read}") + except pyvisa.VisaIOError: + pass return data def auto_gain(self): diff --git a/test.py b/test.py index e891e04..23b4aa4 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,5 @@ +import pyvisa + if __name__ == "__main__": import sys if __package__ is None: @@ -11,9 +13,9 @@ from time import sleep import numpy as np import scipy as scp -from Bentham import Bentham +from Bentham import Bentham, DummyBentham from devices.Xenon import Xenon -from devices.Shutter import ShutterProbe +from devices.Shutter import ShutterProbe, DummyShutter from .update_funcs import Monitor from .measurement_device.impl.sr830 import SR830 @@ -28,7 +30,147 @@ logging.basicConfig( 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 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]", [ @@ -40,34 +182,56 @@ def _measure_both(monochromator: Bentham, lockin: SR830, shutter: ShutterProbe, 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="Phase", color="orange", lim=(-180, 180)) + dict(ax=5, ylabel=r"$\theta$", color="orange", lim=(-180, 180)), + dict(ax=6, ylabel=r"$\sigma_\theta$", color="orange") ]) - N_bins = 512 shutter.open_() data_raw = [] data_wl = {} - sample_rate = 512 + # 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 werte pro sekunde + n_bins_DC = 10 * sample_rate_DC timeout_s = 60 timeout_interval = 0.5 - # lockin.run("SENS 17") # 1 mV/nA - lockin.run("SENS 20") # 10 mV/nA + # 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 large time constants and aggresive filter slope + # 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("OFLT 8") # 100 ms - lockin.run("OFSL 3") # 24dB/Oct ms - lockin.run("SYNC 0") # test without sync filter + # 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)): - lockin.buffer_setup(CH1="R", CH2="Theta", length=N_bins, sample_rate=sample_rate) + 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) - sleep(1.5) # need to wait until lock-in R signal is stable + 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: @@ -85,14 +249,19 @@ def _measure_both(monochromator: Bentham, lockin: SR830, shutter: ShutterProbe, 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, stheta) + 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)): - lockin.buffer_setup(CH1="Aux In 1", CH2="Theta", length=N_bins, sample_rate=sample_rate) + 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: @@ -113,9 +282,11 @@ def _measure_both(monochromator: Bentham, lockin: SR830, shutter: ShutterProbe, # 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) + 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) + 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 @@ -126,9 +297,8 @@ def _measure(monochromator: Bentham, lamp: Xenon, lockin: SR830, shutter: Shutte # 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=3, ylabel="R", color="blue"), dict(ax=2, ylabel="Phase", color="orange", lim=(-180, 180)) - + # dict(ax=3, ylabel="R", color="blue"), ]) N_bins = 100 dt = 0.01 @@ -189,10 +359,15 @@ def measure(wl_range=(400, 500, 2)): 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]) - shutter = ShutterProbe() \ No newline at end of file + # mcm = DummyBentham() + # shutter = DummyShutter() diff --git a/update_funcs.py b/update_funcs.py index b60b262..74ea752 100644 --- a/update_funcs.py +++ b/update_funcs.py @@ -32,6 +32,21 @@ class Monitor: if opt["ylabel"]: ax.set_ylabel(opt["ylabel"]) + def set_ax_title(self, title): + self.ax[0].set_title(title) + self.fig1.canvas.draw() + self.fig1.canvas.flush_events() + + def set_fig_title(self, title): + self.fig1.suptitle(title) + self.fig1.canvas.draw() + self.fig1.canvas.flush_events() + + + def reset(self): + self.xdata = [] + self.ydatas = [[] for _ in range(len(self.ydatas))] + def override(self, xval, *yvals): idx = self.xdata.index(xval) for i, y in enumerate(yvals):