diff --git a/prsctrl/__pycache__/measurement.cpython-311.pyc b/prsctrl/__pycache__/measurement.cpython-311.pyc index e82a8e4..3944542 100644 Binary files a/prsctrl/__pycache__/measurement.cpython-311.pyc and b/prsctrl/__pycache__/measurement.cpython-311.pyc differ diff --git a/prsctrl/__pycache__/test_measurement.cpython-311.pyc b/prsctrl/__pycache__/test_measurement.cpython-311.pyc index fabeff8..8de3b3e 100644 Binary files a/prsctrl/__pycache__/test_measurement.cpython-311.pyc and b/prsctrl/__pycache__/test_measurement.cpython-311.pyc differ diff --git a/prsctrl/devices/lock_in/__init__.py b/prsctrl/devices/lock_in/__init__.py index 7a5aeed..da58514 100644 --- a/prsctrl/devices/lock_in/__init__.py +++ b/prsctrl/devices/lock_in/__init__.py @@ -1,4 +1,4 @@ -from .base import Lock_In_Amp +from .base import LockInAmp TYPENAME_DUMMY = "Dummy" TYPENAME_SR830 = "SR830" @@ -16,7 +16,7 @@ def list_devices() -> dict[str,list[str]]: pass return devices -def connect_device(type_name: str, device_name: str) -> Lock_In_Amp: +def connect_device(type_name: str, device_name: str) -> LockInAmp: if type_name == TYPENAME_DUMMY: return DummyLockInAmp() elif type_name == TYPENAME_SR830: diff --git a/prsctrl/devices/lock_in/__pycache__/__init__.cpython-311.pyc b/prsctrl/devices/lock_in/__pycache__/__init__.cpython-311.pyc index 9becb02..b858705 100644 Binary files a/prsctrl/devices/lock_in/__pycache__/__init__.cpython-311.pyc and b/prsctrl/devices/lock_in/__pycache__/__init__.cpython-311.pyc differ diff --git a/prsctrl/devices/lock_in/__pycache__/base.cpython-311.pyc b/prsctrl/devices/lock_in/__pycache__/base.cpython-311.pyc index 383f9b1..e6aef64 100644 Binary files a/prsctrl/devices/lock_in/__pycache__/base.cpython-311.pyc and b/prsctrl/devices/lock_in/__pycache__/base.cpython-311.pyc differ diff --git a/prsctrl/devices/lock_in/base.py b/prsctrl/devices/lock_in/base.py index 5814f6f..13a7dc2 100644 --- a/prsctrl/devices/lock_in/base.py +++ b/prsctrl/devices/lock_in/base.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Callable -class Lock_In_Amp(ABC): +class LockInAmp(ABC): @abstractmethod def test_connection(self) -> None: """ @@ -19,16 +19,15 @@ class Lock_In_Amp(ABC): pass @abstractmethod - def read_value(self) -> tuple[float, float]: + def read_value(self, which:str) -> float: """ Read a single value - Returns - ------- - [timestamp, voltage] + :param which: X, Y, R, Theta """ pass + @abstractmethod def __str__(self): pass \ No newline at end of file diff --git a/prsctrl/devices/lock_in/impl/__pycache__/dummy.cpython-311.pyc b/prsctrl/devices/lock_in/impl/__pycache__/dummy.cpython-311.pyc index c8ed839..5d88276 100644 Binary files a/prsctrl/devices/lock_in/impl/__pycache__/dummy.cpython-311.pyc and b/prsctrl/devices/lock_in/impl/__pycache__/dummy.cpython-311.pyc differ diff --git a/prsctrl/devices/lock_in/impl/__pycache__/sr830.cpython-311.pyc b/prsctrl/devices/lock_in/impl/__pycache__/sr830.cpython-311.pyc index 67af417..8257615 100644 Binary files a/prsctrl/devices/lock_in/impl/__pycache__/sr830.cpython-311.pyc and b/prsctrl/devices/lock_in/impl/__pycache__/sr830.cpython-311.pyc differ diff --git a/prsctrl/devices/lock_in/impl/dummy.py b/prsctrl/devices/lock_in/impl/dummy.py index 1ef62d1..6e572ac 100644 --- a/prsctrl/devices/lock_in/impl/dummy.py +++ b/prsctrl/devices/lock_in/impl/dummy.py @@ -1,9 +1,9 @@ -from ..base import Lock_In_Amp +from ..base import LockInAmp from typing import Callable from time import time as now import numpy as np -class DummyLockInAmp(Lock_In_Amp): +class DummyLockInAmp(LockInAmp): def __init__(self): super().__init__() @@ -42,9 +42,8 @@ class DummyLockInAmp(Lock_In_Amp): def check_overloads(self) -> bool | str: return False - def read_value(self): - """Read the value of R""" - return float(self.query("OUTP? 3")) + def read_value(self, which: str): + return -1.0 def reset(self): pass diff --git a/prsctrl/devices/lock_in/impl/model7260.py b/prsctrl/devices/lock_in/impl/model7260.py index 5bd74b2..ce648cd 100644 --- a/prsctrl/devices/lock_in/impl/model7260.py +++ b/prsctrl/devices/lock_in/impl/model7260.py @@ -1,7 +1,7 @@ import pyvisa # import pkg_resources -from ..base import Lock_In_Amp +from ..base import LockInAmp from prsctrl.util.visa import enumerate_devices import logging @@ -9,7 +9,7 @@ log = logging.getLogger(__name__) import numpy as np -class Model7260(Lock_In_Amp): +class Model7260(LockInAmp): """ Wrapper class for the Model 7260 DSP Lock-In controlled via pyvisa """ diff --git a/prsctrl/devices/lock_in/impl/sr830.py b/prsctrl/devices/lock_in/impl/sr830.py index 8da29e3..d1e446b 100644 --- a/prsctrl/devices/lock_in/impl/sr830.py +++ b/prsctrl/devices/lock_in/impl/sr830.py @@ -4,13 +4,13 @@ import struct # for converting bytes to float import numpy as np -from ..base import Lock_In_Amp +from ..base import LockInAmp from prsctrl.utility.visa import enumerate_devices import logging log = logging.getLogger(__name__) -class SR830(Lock_In_Amp): +class SR830(LockInAmp): """ Wrapper class for the SR830 controlled via pyvisa """ @@ -152,10 +152,12 @@ class SR830(Lock_In_Amp): return "Output" return False - def measureTODO(self): pass - def read_value(self): - """Read the value of R""" - return float(self.query("OUTP? 3")) + OUTP = ["X", "Y", "R", "theta"] + def read_value(self, which: str): + if which not in self.OUTP: + raise ValueError(f"Invalid output: {which}. Must be one of {self.OUTP}") + outp = self.OUTP.index(which) + 1 + return float(self.query(f"OUTP? {outp}")) def reset(self): self.instr.write("*RST") @@ -230,14 +232,16 @@ class SR830(Lock_In_Amp): ofsl = int(self.query("OFSL?")) return SR830.OFSL[ofsl] - def get_wait_time_s(self): + def get_wait_time_s(self, time_const: float|None=None, filter_slope: int|None=None): """ Get the wait time required to reach 99% of the final value. + :param time_const: If not passed, the value will be read from the device + :param filter_slop: If not passed, the value will be read from the device See Manual 3-21 :return: """ - time_const = self.get_time_constant_s() - filter_slope = self.get_filter_slope() + if not time_const: time_const = self.get_time_constant_s() + if not filter_slope: 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 @@ -327,22 +331,22 @@ class SR830(Lock_In_Amp): """ Get the data from the buffer. - :return: np.ndarray - Returns a numpy of shape (<1 if one channel, 2 if both channels>, n_points) + :return: np.ndarray | tuple[np.ndarray, np.ndarray] """ if self._buffer_length is None: raise RuntimeError(f"Buffer not set up, call buffer_setup() first.") self.run("PAUS") take_n_points = min(self.buffer_get_n_points(), self._buffer_length) # there might be more points stored then was required - if CH1 and CH2: - data = np.empty((2, take_n_points), dtype=float) - elif CH1 or CH2: - data = np.empty((1, take_n_points), dtype=float) - else: + # if CH1 and CH2: + # data = (np.empty((2, take_n_points), dtype=float) for _ in range(2)) + # elif CH1 or CH2: + # data = np.empty((1, take_n_points), dtype=float) + if (not CH1) and (not CH2): raise ValueError("Either CH1 or CH2 must be set True.") + data = [] if CH1: - data[ 0, :] = self._buffer_get_data(1, 0, take_n_points)[:] + data.append(self._buffer_get_data(1, 0, take_n_points)) if CH2: - data[-1, :] = self._buffer_get_data(2, 0, take_n_points)[:] + data.append(self._buffer_get_data(2, 0, take_n_points)) return data def _buffer_get_data_slow(self, CH=1, start=0, n_points=None): @@ -402,3 +406,20 @@ class SR830(Lock_In_Amp): def __str__(self): return "SR830" + +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, + "frequency_Hz": lockin.set_frequency_Hz, + "reference": lockin.set_reference, + "reference_trigger": lockin.set_reference_trigger, + } + 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) diff --git a/prsctrl/devices/monochromator/impl/__pycache__/dummy.cpython-311.pyc b/prsctrl/devices/monochromator/impl/__pycache__/dummy.cpython-311.pyc index 3686fd1..874cdf2 100644 Binary files a/prsctrl/devices/monochromator/impl/__pycache__/dummy.cpython-311.pyc and b/prsctrl/devices/monochromator/impl/__pycache__/dummy.cpython-311.pyc differ diff --git a/prsctrl/devices/monochromator/impl/dummy.py b/prsctrl/devices/monochromator/impl/dummy.py index ee5caf4..b756e36 100644 --- a/prsctrl/devices/monochromator/impl/dummy.py +++ b/prsctrl/devices/monochromator/impl/dummy.py @@ -13,7 +13,7 @@ class DummyMonochromator(Monochromator): self.wavelength_nm = -1 def set_wavelength_nm(self, wavelength_nm): - log.info("Dummy-Monochromator set to {wl} nm") + log.info(f"Dummy-Monochromator set to {wavelength_nm} nm") self.wavelength_nm = wavelength_nm def get_wavelength_nm(self): diff --git a/prsctrl/measurement.py b/prsctrl/measurement.py index d929ae0..526a827 100644 --- a/prsctrl/measurement.py +++ b/prsctrl/measurement.py @@ -4,133 +4,125 @@ Created on Fri Jan 24 15:18:31 2025 @author: Matthias Quintern """ -from .measurement_device.base import VoltageMeasurementDevice -from .led_control_device.base import LedControlDevice -from .led_script import LedScript -from .utility.prsdata import DataCollector +from .devices.lock_in.base import LockInAmp +from .devices.shutter.base import Shutter +from .devices.monochromator import Monochromator +from .utility.prsdata import PrsData import time import datetime from queue import Queue +import numpy as np import logging log = logging.getLogger(__name__) -def measure( - vm_dev: VoltageMeasurementDevice, - led_dev: LedControlDevice, - led_script: LedScript, - data: DataCollector, - delta_t: float=0.1, - flush_after:int|None=None, - use_buffer=False, - max_measurements: int=None, - stop_on_script_end: bool=False, - verbose: bool=False, - command_queue: None|Queue=None, - data_queue: None|Queue=None, - add_measurement_info_to_metadata=True +def get_wavelengths_values(wl_range: tuple | list): + """ + :param wl_range: + if tuple, return list(range(*wl_range)) + if list, return copy of wl_range + """ + if isinstance(wl_range, tuple): + wavelengths = list(range(*wl_range)) + elif isinstance(wl_range, list) or isinstance(wl_range, np.ndarray): + wavelengths = wl_range.copy() + else: + raise ValueError(f"Invalid type for 'wavelengths_nm': {type(wl_range)}") + return wavelengths + + +def measure_spectrum( + monochromator: Monochromator, + lockin: LockInAmp, + shutter: Shutter, + data: PrsData, + measurement_params:dict, + aux_DC="Aux In 4", + command_queue: None | Queue = None, + data_queue: None | Queue = None, + add_measurement_info_to_metadata=True ): - """ - Perform a measurement - Parameters - ---------- - vm_dev : VoltageMeasurementDevice - DESCRIPTION. - led_dev : LedControlDevice - DESCRIPTION. - led_script : LedScript - DESCRIPTION. - data : DataCollector - DESCRIPTION. - delta_t : float, optional - Target interval between measurements and led updates. The default is 0.1. - flush_after : int|None, optional - If int, flush values to disk after . The default is None. - use_buffer : TYPE, optional - If True, use the buffer measurement mode. The default is False. - max_measurements : int, optional - Number of measurements to perform before returning. - Note: If use_buffer=True, a few more than max_measurements might be performed - The default is None. - stop_on_script_end : bool, optional - Stop when the script end is reached. - verbose : bool, optional - If True, print some messages. The default is False. - command_queue : None|Connection, optional - A queue to receive to commands from. - Commands may be: - "stop" -> stops the measurement - ("led_script", ) a new led script to use - The default is None. - data_queue : None|Queue, optional - A queue to put data in. The default is None. - add_measurement_info_to_metadata : bool, optional - If True, add measurement info to the metadata: - time, measurement_interval, measurement_use_buffer, measurement_voltage_device, measurement_led_device - The default is True. - Returns - ------- - None. + import pyvisa + 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: + # TODO: retry if status bit is set + lockin.try_recover_from_communication_error(e) + com_success -= 1 + raise e + + default_measurement_params = { + "measurement_time_s": 30, + "sample_rate_Hz": 512, + "wait_time_s": 1, + "wavelengths_nm": (390, 720, 1), + } + measurement_params = default_measurement_params | measurement_params + wait_time_s = measurement_params["wait_time_s"] + sample_rate_Hz = measurement_params["sample_rate_Hz"] + measurement_time_s = measurement_params["measurement_time_s"] + n_bins = sample_rate_Hz * measurement_time_s + wavelengths = get_wavelengths_values(measurement_params["wavelengths_nm"]) + print(wavelengths) + + timeout_s = 3 * measurement_time_s + timeout_interval = 0.5 - """ get_time = lambda: datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") if add_measurement_info_to_metadata: - data.metadata["measurement_interval"] = str(delta_t) + " s" - data.metadata["measurement_use_buffer"] = str(use_buffer) - data.metadata["measurement_voltage_measurement_device"] = str(vm_dev) - data.metadata["measurement_led_control_device"] = str(led_dev) - led_name = led_dev.get_led_name() - if led_name: data.metadata["measurement_led_lamp"] = led_name + data.metadata["device_lock-in"] = str(lockin) + data.metadata["device_monochromator"] = str(monochromator) data.metadata["measurement_time_start"] = get_time() # write metadata to disk data.write_metadata() - vm_dev.reset(True) - if use_buffer: - vm_dev.buffer_measure(delta_t, verbose=True) - # allow 0 instead of None - if max_measurements == 0: max_measurements = None - if flush_after == 0: flush_after = None + print(wait_time_s) + data.metadata["messages"] = [] + try: - i = 0 - led_val = led_script.start() - try: - led_dev.set_level(led_val) - except Exception as e: - log.error(f"Error setting led to {led_val:03}%: {e}") - raise e - t_iter_start = time.time() - while True: - # using while True and if, to be able to log the stop reason - if max_measurements is not None and i >= max_measurements: - log.info(f"Reached maximum number of measurements ({i}{max_measurements}), stopping measurement") - break - # 1) read value(s) - if use_buffer: - try: - values = vm_dev.buffer_read_new_values() - except ValueError as e: - # print(f"Error in buffer measurement {i}:", e) - values = [] - else: - values = [vm_dev.read_value()] - # print(values) - # 2) process value(s) - for (tval, vval) in values: - if i == 0: - t0 = tval - tval -= t0 - current_data = (i, tval, vval, led_val) - data.add_data(*current_data) - # 3) write data - if verbose: print(f"n = {i:6d}, t = {tval: .2f} s, U = {vval: .5f} V, LED = {led_val:03}%" + " "*10, end='\r') - if flush_after is not None and (i+1) % flush_after == 0: - data.flush(verbose=verbose) - # if a queue was given, put the data - if data_queue is not None: - data_queue.put(current_data) - i += 1 + shutter.open() + for i_wl, wl in enumerate(wavelengths): + log.info(f"Measuring at lambda={wl} nm") + run_lockin_cmd(lambda: lockin.buffer_setup(CH1="R", CH2=aux_DC, length=n_bins, sample_rate=sample_rate_Hz)) + + data_queue.put(("set_wavelength", wl)) + monochromator.set_wavelength_nm(wl) + data_queue.put(("wait_stable", )) + # wait the wait time + time.sleep(wait_time_s) + overload = run_lockin_cmd(lambda: lockin.check_overloads()) + if overload: + msg = f"Overload of {overload} at {wl} nm" + log.warning(msg) + data.metadata["messages"].append(msg) + theta = [] + measure_phase = lambda: theta.append(run_lockin_cmd(lambda: lockin.read_value("theta"))) + data_queue.put(("measuring", )) + measure_phase() + run_lockin_cmd(lambda: lockin.buffer_start_fill()) + # check if its done + t = timeout_s + while t > 0: + t -= timeout_interval + time.sleep(timeout_interval) + measure_phase() + if run_lockin_cmd(lambda: lockin.buffer_is_done()): + break + if t < 0: raise RuntimeError("Timed out waiting for buffer measurement to finish") + # done + dR_raw, R_raw = run_lockin_cmd(lambda: lockin.buffer_get_data(CH1=True, CH2=True)) + data[wl] = {} + data[wl]["dR_raw"] = dR_raw * 2 * np.sqrt(2) # convert RMS to Peak-Peak + data[wl]["R_raw"] = R_raw + data[wl]["theta_raw"] = np.array(theta) + spec_data = data.get_spectrum_data([wl]) + data.write_partial_file(wl) + data_queue.put(("data", spec_data)) # if a pipe was given, check for messages if command_queue is not None and command_queue.qsize() > 0: @@ -138,49 +130,56 @@ def measure( if recv == "stop": log.info(f"Received 'stop', stopping measurement") break - elif type(recv) == tuple and recv[0] == "led_script": - log.info(f"Received 'led_script', replacing script") - led_script = recv[1] elif type(recv) == tuple and recv[0] == "metadata": log.info(f"Received 'metadata', updating metadata") data.metadata |= recv[1] data.write_metadata() else: log.error(f"Received invalid message: '{recv}'") - - # 4) sleep - # subtract the execution time from the sleep time for a more - # accurate frequency - dt_sleep = delta_t - (time.time() - t_iter_start) - if dt_sleep > 0: - # print(f"Sleeping for {dt_sleep}") - time.sleep(dt_sleep) - t_iter_start = time.time() - # 5) update LED - if stop_on_script_end and led_script.is_done(t_iter_start): - log.info("Reached led script end, stopping measurement") - break - new_led_val = led_script.get_state(t_iter_start) - if new_led_val != led_val: - try: - led_dev.set_level(new_led_val) - led_val = new_led_val - except Exception as e: - log.error(f"Error setting led to {new_led_val:03}%: {e}") - raise e - except KeyboardInterrupt: log.info("Keyboard interrupt, stopping measurement") except Exception as e: log.critical(f"Unexpected error, stopping measurement. Error: {e}") if command_queue is not None: command_queue.put(("exception", e)) + raise e + if add_measurement_info_to_metadata: data.metadata["measurement_time_stop"] = get_time() # Write again after having updated the stop time data.write_metadata() - data.flush() - led_dev.off() + data.write_full_file() - - + +def set_offsets_laser_only( + lockin: LockInAmp, + shutter: Shutter, + wait_time_s, + R=True, + phase=True, + data_queue: None | Queue = None, + ): + """ + Set the R offset from the signal when only the laser is on. + This signal should be stray laser light and laser induced PL + :param phase: If True, use the Auto-Phase function to offset the phase + :param R: If True, use the Auto-Offset function to offset R + :return: Offset as percentage of the full scale R, Phase offset in degrees + """ + log.info("Setting offset when the lamp is off.") + shutter.close() + if data_queue: + data_queue.put(("wait_stable", )) + time.sleep(wait_time_s + 10) + # TODO: generalize for other lock-ins + if data_queue: + data_queue.put(("set_offsets", )) + lockin.run("AOFF 3") # auto offset R + # R must come before phase, because after auto-phase the signal needs to stabilize again + if R: + R_offset_fs = float(lockin.query("OEXP? 3").split(",")[0]) # returns R offset and expand + if phase: + lockin.run("APHS") + phase_offset_deg = float(lockin.query("PHAS? 3")) # returns R offset and expand + if data_queue: data_queue.put(("offsets", R_offset_fs, phase_offset_deg)) + return R_offset_fs, phase_offset_deg diff --git a/prsctrl/prsctrl_interactive.py b/prsctrl/prsctrl_interactive.py index a1c6f03..730ee97 100644 --- a/prsctrl/prsctrl_interactive.py +++ b/prsctrl/prsctrl_interactive.py @@ -2,10 +2,13 @@ run this before using this library: ipython -i prctrl_interactive.py """ +from toolz import frequencies + version = "0.1" import numpy as np import matplotlib.pyplot as plt +import time from datetime import datetime as dtime from os import path, makedirs @@ -30,13 +33,14 @@ from .devices import lock_in as mod_lock_in from .devices import lamp as mod_lamp from .devices import monochromator as mod_monochromator # import base classes -from .devices.lock_in import Lock_In_Amp +from .devices.lock_in import LockInAmp from .devices.shutter import Shutter from .devices.lamp import Lamp from .devices.monochromator import Monochromator -# from .measurement import measure as _measure -from .utility.data_collector import PrsDataCollector +from .devices.lock_in.impl.sr830 import set_measurement_params +from .measurement import measure_spectrum as _measure_spectrum, set_offsets_laser_only, get_wavelengths_values +from .utility.prsdata import PrsData, plot_spectrum from .utility.config_file import ConfigFile from .utility.device_select import select_device_interactive, connect_device_from_config_or_interactive from .update_funcs import Monitor @@ -64,12 +68,11 @@ test = False # DEVICES # global variable for the instrument/client returned by pyvisa/bleak -lockin: Lock_In_Amp|None = None +lockin: LockInAmp | None = None shutter: Shutter|None = None lamp: Lamp|None = None mcm: Monochromator|None = None -data_collector = PrsDataCollector(data_path=settings["datadir"], data_name="interactive", dirname="interactive_test", add_number_if_dir_exists=True) t0 = 0 data = None md = None @@ -78,97 +81,179 @@ from .test_measurement import _measure_both_sim def measure_both_sim(**kwargs): return _measure_both_sim(mcm, lockin, shutter, **kwargs) -def monitor(script: str|int=0, interval: float|None=None, metadata:dict={}, flush_after: int|None=None, use_buffer: bool|None=None, max_measurements=None, stop_on_script_end: bool=False, max_points_shown=None): - """ - Monitor the voltage with matplotlib. - - Opens a matplotlib window and takes measurements depending on settings["interval"] - - Waits for the user to press a key - - If use_buffer=False, uses python's time.sleep() for waiting the interval, which is not very precise. - With use_buffer=True, the timing of the voltage data readings will be very precise, however, - the led updates may deviate up to . - - The data is automatically saved to "_