"""
Measurement settings for photoreflectance implemented as classes.
This new structure is "more strongly typed" than the old dictionary approach, and makes for less magic values (settings keys in the code).
It is also easier to apply the settings using the `apply(device)` method.
Every setting will also always be present and have a default value when not set explcitly.
Loading and saving from a file is also easier.
You can use the `dataclasses.asdict(<settings class>)` to convert a settings object to a dictionary.
"""
from devctrl import devices as d
from devctrl.data.spectrum.functions import nm_to_eV, eV_to_nm
from devctrl.utility.settings import SettingsClass
from dataclasses import dataclass, field
from typing import Any, Optional, Callable
import numpy as np
import logging
log = logging.getLogger(__name__)
[docs]
def convert_range_to_list(wl_range: tuple | list | np.ndarray) -> list:
"""
:param wl_range:
if tuple, return list(range(*wl_range))
if list or np.ndarray, return copy of wl_range
"""
if isinstance(wl_range, tuple):
wavelengths = np.arange(*wl_range).tolist()
elif isinstance(wl_range, list):
wavelengths = wl_range.copy()
elif isinstance(wl_range, np.ndarray):
wavelengths = wl_range.tolist()
else:
raise ValueError(f"Invalid type for : {type(wl_range)}")
return wavelengths
[docs]
@dataclass
class MeasurementSettings(SettingsClass):
measurement_time_s: float = field(default=30, metadata={"doc": "Time in seconds where data is recorded at each wavelength.", "valid": (0, 9999999)})
wait_time_s: Optional[float] = field(default=None, metadata={"doc": "Time in seconds to wait for the signal to stabilize after changing the wavelength.\nSet to `0` for no wait time, and to `None` to automatically choose a value based on the lock-in time constant.", "valid": (0, 999999)})
additional_wait_time_s: float = field(default=0, metadata={"doc": "Time in seconds to wait for the signal to stabilize after changing the wavelength.\nSet to `0` for no wait time, and to `None` to automatically choose a value based on the lock-in time constant.", "valid": (0, 999999)})
pump_source: str = field(default="none", metadata={"doc": "String describing which pump source is used.\nIf 'led', then the values from `PrsSettings.led` are applied. If 'laser', then the values from `PrsSettings.laser` are applied.", "valid": ["laser", "led", "laser-manual", "led-manual", "none", "manual"]})
auto_gain: bool = field(default=False, metadata={"doc": "Whether to automatically adjust the gain during the measurement to stay in the optimum DC region. Not current implemented yet."})
auto_sensitivity: bool = field(default=True, metadata={"doc": "Whether to automatically increase the lock-in sensitivity on overloads."})
wavelengths_nm: Optional[tuple|list] = field(default=None, metadata={"doc": "Wavelength range in nm. Must be either a list of values or a tuple (start, stop, step). Only one of `wavelengths_nm` or `energies_eV` may be set."})
energies_eV: Optional[tuple|list] = field(default=None, metadata={"doc": "Energy range in eV. Must be either a list of values or a tuple (start, stop, step). Only one of `wavelengths_nm` or `energies_eV` may be set."})
pump_power_mWcm2: Optional[float] = field(default=None, metadata={"doc": "Power in mW/cm^2. If `pump_source` is set and a conversion table exists for the source then the `power_mW` setting for lasers or the `current_A` setting for LEDs can be overwritten with the value corresponding to the `power_mWcm2` setting."})
[docs]
def get_wavelengths_nm(self, round_to_digits: Optional[int]=2, update_field: bool=True) -> list[float]:
"""
Generate a list of wavelengths according to either the 'wavelengths_nm' or 'energies_eV' field.
Either field may already be a list of values, or a tuple (start, stop, step size).
:param round_to_digits: Optional number of digits to round the values to.
:param update_field: If True, overwrite the value of 'wavelengths_nm' with the returned array and 'energies_eV' with None.
:return: list of wavelengths in nm
"""
if self.wavelengths_nm is not None and self.energies_eV is not None:
raise KeyError(f"Only one of 'wavelengths_nm' or 'energies_eV' must be set, but both are.")
if self.wavelengths_nm is not None:
wavelengths = convert_range_to_list(self.wavelengths_nm)
elif self.energies_eV is not None:
wavelengths = [(v if type(v) == str else eV_to_nm(v)) for v in convert_range_to_list(self.energies_eV)]
else:
raise KeyError(f"One of 'wavelengths_nm' or 'energies_eV' must be set, but both are None")
if round_to_digits is not None:
wavelengths = [(v if type(v) == str else round(v, round_to_digits)) for v in wavelengths]
if update_field:
self.wavelengths_nm = wavelengths.copy()
self.energies_eV = None
return wavelengths
[docs]
@dataclass
class AmplifierSettings(SettingsClass):
coupling: str = "DC"
gain: float = 5
mode: Optional[str] = None
def apply(self, device: d.Amplifier):
device.set_coupling(self.coupling)
if self.mode is not None:
if not hasattr(device, "set_mode"):
raise RuntimeError(f"Can not apply amplifier mode, since amplifier of type {device.get_device_name()} has no attribute 'set_mode'")
device.set_mode(self.mode)
device.set_gain(self.gain)
[docs]
@dataclass
class LaserSettings(SettingsClass):
power_mW: float = 1.0
mode: str = "TTL"
def apply(self, device: d.Laser):
device.set_mode(self.mode)
device.set_power_mW(self.power_mW)
[docs]
@dataclass
class LedSettings(SettingsClass):
mode: str = "TTL"
current_A: float = 0.1
def apply(self, device: d.LedController):
device.set_mode(self.mode)
device.set_current_A(self.current_A)
[docs]
@dataclass
class LockInSettings(SettingsClass):
time_constant_s: float = 1
sensitivity_volt: float = 50e-6
filter_slope: float = 12
sync_filter: float = 1
reserve: str = "Normal"
reference: str = "External"
"""
The source of the reference frequency
"""
reference_trigger: str = "Rising Edge"
frequency_Hz: Optional[float] = None
sample_rate_Hz: float = 128
aux_DC: str = "Aux In 1"
"""
The name of the auxiliary port that is used to measure the DC intensity
"""
[docs]
def apply(self, device: d.LockInAmp):
"""
Set
- time constant
- sensitivity
- filter slope
- sync filter
- reserve
- reference source
- reference trigger setting
- frequency
"""
device.set_time_constant_s(self.time_constant_s)
device.set_sensitivity_volt(self.sensitivity_volt)
device.set_filter_slope(self.filter_slope)
device.set_sync_filter(self.sync_filter)
if hasattr(device, "set_reserve"):
device.set_reserve(self.reserve)
device.set_reference(self.reference)
device.set_reference_trigger(self.reference_trigger)
if self.frequency_Hz is not None:
device.set_frequency_Hz(self.frequency_Hz)
[docs]
@dataclass
class MonochromatorSettings(SettingsClass):
"""
Monochromator settings, to apply a bandwidth/resolution
"""
resolution_nm: float = 1.0
"""
Resolution in nm. Will be applied if `resolution_eV` < 0.
"""
resolution_eV: float = -1
"""
Resolution in eV. Will be applied if > 0.
"""
def apply(self, device: d.Monochromator):
if self.resolution_eV > 0:
device.set_resolution_eV(self.resolution_eV)
else:
device.set_resolution_nm(self.resolution_nm)
[docs]
@dataclass
class ModeSettings(SettingsClass):
pre_amplifier: Optional[AmplifierSettings] = None
lock_in: LockInSettings = field(default_factory=LockInSettings)
if __name__ == "__main__":
# Demonstrate how settings can be merged
settings = PrsSettings(modes={"ref": ModeSettings(pre_amplfier=AmplifierSettings())})
print(settings)
loaded_from_file = {"general": {"measurement_time_s": 42}, "modes": {"tra": {"lock_in": {"time_constant_s": 5}}}}
settings |= loaded_from_file
print(settings)