# Copyright (c) ETH Zurich, SIS ID and HVL D-ITET
#
import logging
from abc import ABC, abstractmethod
from aenum import StrEnum
from hvl_ccb.dev.keysightb298xx.comm import KeysightB2985AVisaCommunication
from hvl_ccb.dev.keysightb298xx.modules.submodules.base import _BaseModule
from hvl_ccb.dev.keysightb298xx.modules.submodules.format import FormatElements
from hvl_ccb.utils.enum import RangeEnum
from hvl_ccb.utils.validation import validate_bool, validate_number
logger = logging.getLogger(__name__)
[docs]
class ApertureMode(StrEnum):
"""
Enum for the different aperture auto modes.
"""
OFF = "OFF"
SHORT = "SHOR"
MEDIUM = "MED"
LONG = "LONG"
[docs]
class CurrentRange(RangeEnum):
"""
Enum for the different current measurement ranges.
"""
TWO_pA = 2e-12
TWENTY_pA = 2e-11
TWO_HUNDRED_pA = 2e-10
TWO_nA = 2e-9
TWENTY_nA = 2e-8
TWO_HUNDRED_nA = 2e-7
TWO_uA = 2e-6
TWENTY_uA = 2e-5
TWO_HUNDRED_uA = 2e-4
TWO_mA = 2e-3
TWENTY_mA = 2e-2
[docs]
class ChargeRange(RangeEnum):
"""
Enum for the different charge measurement ranges.
"""
TWO_nC = 2e-9
TWENTY_nC = 2e-8
TWO_HUNDRED_nC = 2e-7
TWO_uC = 2e-6
[docs]
class VoltageRange(RangeEnum):
"""
Enum for the different voltage measurement ranges.
"""
TWO_VOLT = 2
TWENTY_VOLT = 20
[docs]
class ResistanceRange(RangeEnum):
"""
Enum for the different resistance measurement ranges.
"""
ONE_M_OHM = 1e6
TEN_M_OHM = 1e7
ONE_HUNDRED_M_OHM = 1e8
ONE_G_OHM = 1e9
TEN_G_OHM = 1e10
ONE_HUNDRED_G_OHM = 1e11
ONE_T_OHM = 1e12
TEN_T_OHM = 1e13
ONE_HUNDRED_T_OHM = 1e14
ONE_P_OHM = 1e15
class _SenseBase(_BaseModule):
"""
Base class for the sense module.
"""
@property
def aperture(self) -> float:
"""
Get the measurement aperture.
:return: float with the aperture in s
"""
return float(self._com.query(f"{self._base_command}:APER?"))
@aperture.setter
def aperture(self, value: float) -> None:
"""
Set the measurement aperture in s
:param value: aperture duration in s.
"""
validate_number("Aperture Time", value, (1e-5, 2), (int, float), logger)
logger.info(f"Aperture time: {value} s")
self._com.write(f"{self._base_command}:APER {value}")
@property
def aperture_mode(self) -> ApertureMode:
"""
Returns the aperture mode
:return: aperture mode as StrEnum value
"""
status, mode = self._com.query_multiple(
f"{self._base_command}:APER:AUTO?", f"{self._base_command}:APER:AUTO:MODE?"
)
if not int(status):
return ApertureMode.OFF # type: ignore[return-value]
return ApertureMode(mode)
@aperture_mode.setter
def aperture_mode(self, value: ApertureMode) -> None:
"""
Set the aperture mode
:param value: Aperture mode in ApertureMode StrEnum
"""
value = ApertureMode(value)
logger.info(f"Aperture mode: {value}")
if value == ApertureMode.OFF:
self._com.write(f"{self._base_command}:APER:AUTO {value}")
else:
self._com.write(f"{self._base_command}:APER:AUTO ON")
self._com.write(f"{self._base_command}:APER:AUTO:MODE {value}")
class _SensValue(_BaseModule, ABC):
"""
Base class for all sense modules.
"""
def __init__(
self,
com: KeysightB2985AVisaCommunication,
base_command: str,
name: str,
unit: str,
) -> None:
super().__init__(com, base_command, name)
self._unit = unit
@property
def auto_range(self) -> bool:
"""
Get the current set range mode of the module
:return: bool indicating the auto range mode
"""
return bool(int(self._com.query(f"{self._base_command}:RANG:AUTO?")))
@auto_range.setter
def auto_range(self, value: bool) -> None:
"""
Enable or disable the auto range mode
:param value: bool of the desired state
"""
validate_bool("auto range", value, logger)
logger.info(f"Ruto range active: {value}")
self._com.write(f"{self._base_command}:RANG:AUTO {int(value)}")
@property
def measurement_range(self) -> float:
"""
Get the current range of a measurement system.
:return: float with the range of a measurement system
"""
return float(self._com.query(f"{self._base_command}:RANG?"))
@measurement_range.setter
def measurement_range(self, value: float) -> None:
"""
Set the range of a measurement system.
:param value: float with the desired value. Will be checked with a RangeEnum.
"""
value = self._validate_range(value)
logger.info(f"{self._name} range: {value} {self._unit}")
self._com.write(f"{self._base_command}:RANG {value}")
@abstractmethod
def _validate_range(self, value) -> float: ...
@property
def range(self) -> None:
msg = "The range property is deprecated, use measurement_range instead."
raise DeprecationWarning(msg)
@range.setter # noqa: A003
def range(self, value: float) -> None: # noqa: ARG002
msg = "The range property is deprecated, use measurement_range instead."
raise DeprecationWarning(msg)
[docs]
class SensCurrent(_SensValue):
"""
Current measurement class.
"""
def __init__(self, com: KeysightB2985AVisaCommunication) -> None:
super().__init__(com, ":SENS:CURR", "current", "A")
def _validate_range(self, value) -> float:
return CurrentRange(abs(value))
[docs]
class SensVoltage(_SensValue):
"""
Voltage measurement class.
"""
def __init__(self, com: KeysightB2985AVisaCommunication) -> None:
super().__init__(com, ":SENS:VOLT", "voltage", "V")
def _validate_range(self, value) -> float:
return VoltageRange(value)
[docs]
class SensResistance(_SensValue):
"""
Resistance measurement class.
"""
def __init__(self, com: KeysightB2985AVisaCommunication) -> None:
super().__init__(com, ":SENS:RES", "resistance", "Ω")
def _validate_range(self, value) -> float:
return ResistanceRange(value)
[docs]
class SensCharge(_SensValue):
"""
Charge measurement class.
"""
def __init__(self, com: KeysightB2985AVisaCommunication) -> None:
super().__init__(com, ":SENS:CHAR", "charge", "C")
def _validate_range(self, value) -> float:
return ChargeRange(value)
class _SensDataBase(_BaseModule):
"""
Base class for data module.
"""
def __init__(self, com: KeysightB2985AVisaCommunication) -> None:
super().__init__(com, ":SENS:DATA", "sense_data")
def clear(self) -> None:
"""
Clear the data in the output buffer.
"""
self._com.write(f"{self._base_command}:CLE")
@staticmethod
def _clean_data(
data: str, form: list[FormatElements]
) -> dict[FormatElements, list]:
"""
Method to clean the data string from the following placeholders and transform
it in a usable dict:
- +9.91e37 --> NaN
- +9.90e37 --> +inf
- -9.90e37 --> -inf
The keys are FormatElements and the values are lists containing the
actual data e.g. timestamps and voltages/currents.
:return: Dict with transformed data
"""
data_list = list(map(float, data.split(",")))
data_list = [float(" NaN") if x == +9.91e37 else x for x in data_list]
data_list = [float("+inf") if x == +9.90e37 else x for x in data_list]
data_list = [float("-inf") if x == -9.90e37 else x for x in data_list]
data_dict = {}
for row in range(len(form)):
data_dict[form[row]] = data_list[row :: len(form)]
return data_dict