"""This module contains the :class:`Channel` Abstract Base Class which should be inherited by device channels. The main
responsibility is to execute actions on the physical device channels for which the device
:attr:`~cohesivm.channels.Channel.connection` is stored.
Furthermore, all possible channel methods are defined in the :class:`Channel` in order for a
:class:`~cohesivm.measurements.Measurement` to recognize them. Trait classes, e.g., :class:`Voltmeter`, will inherit
these methods and set the ones that must be implemented abstract. Then they can be used to be implemented in a device
module and for the compatibility check of the :class:`~cohesivm.experiment.Experiment`."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Tuple, Any, TypeVar, Union
from cohesivm.database import DatabaseDict
TChannel = TypeVar('TChannel', bound='Channel')
[docs]
class Channel(ABC):
"""Contains the properties and methods to operate a physical device channel. The implementation of a child class
must define the :meth:`get_property` and :meth:`set_property` methods which are used to configure the channel.
Practically, the child class will inherit a trait class (child class of this class) which additionally defines the
specific methods which must be implemented.
:param identifier: String identifier of the channel.
:param settings: Dictionary of channel settings."""
@abstractmethod
def __init__(self, identifier: str = None, settings: DatabaseDict = None) -> None:
self._identifier = identifier
if settings is None or len(settings) == 0:
settings = {'default': 0}
self._settings = dict(sorted(settings.items()))
self._connection = None
self._check_settings()
@property
def identifier(self) -> str:
"""String identifier of the channel."""
return self._identifier
@property
def settings(self) -> DatabaseDict:
"""Dictionary of channel settings."""
return self._settings
@property
def connection(self) -> Union[Any, None]:
"""Holds the resource of the device connection while a connection is established through using
the :meth:`~cohesivm.devices.Device.connect` contextmanager."""
if self._connection is None:
raise RuntimeError('A device connection must be established in order to communicate with the channel!')
return self._connection
[docs]
@abstractmethod
def set_property(self, name: str, value: Any) -> None:
"""Sets a property/device-setting to the provided value.
:param name: The name of the property/device-setting to be set.
:param value: The value to which the property/device-setting should be set.
"""
[docs]
@abstractmethod
def get_property(self, name: str) -> Any:
"""Retrieves the current value of a property/device-setting.
:param name: The name of the property/device-setting to get.
:returns: The value of the property/device-setting.
"""
@abstractmethod
def _check_settings(self) -> None:
"""Validates the values in the `settings` dictionary before they are applied on the device."""
[docs]
def apply_settings(self) -> None:
"""Applies the settings."""
for name, value in self._settings.items():
self.set_property(name, value)
[docs]
def change_setting(self, setting, value) -> None:
"""Modifies the :attr:`settings` and overwrites the settings on the device.
:param setting: String key of the setting in the :attr:`settings`.
:param value: New value of the setting.
:raises KeyError: If ``setting`` is not a valid setting identifier string.
"""
if setting not in self._settings.keys():
raise KeyError(f"'{setting}' is not a valid setting identifier string. "
f"Valid keys are: {list(self._settings.keys())}")
old_value = self._settings[setting]
self._settings[setting] = value
try:
self._check_settings()
except Exception as exc:
self._settings[setting] = old_value
raise exc
self.apply_settings()
[docs]
@abstractmethod
def enable(self) -> None:
"""Enables the channel. Must be executed before any channel method can be run."""
[docs]
@abstractmethod
def disable(self) -> None:
"""Disables the channel."""
[docs]
def measure_voltage(self) -> float:
"""Measures the voltage.
:returns: Measurement result in V.
"""
raise NotImplementedError
[docs]
def measure_current(self) -> float:
"""Measures the current.
:returns: Measurement result in A.
"""
raise NotImplementedError
[docs]
def source_voltage(self, voltage: float) -> None:
"""Sets the DC output voltage to the defined value.
:param voltage: Output voltage of the DC power source in V.
"""
raise NotImplementedError
[docs]
def source_current(self, current: float) -> None:
"""Sets the DC output current to the defined value.
:param current: Output current of the DC power source in A.
"""
raise NotImplementedError
[docs]
def source_voltage_and_measure(self, voltage: float) -> Tuple[float, float]:
"""Sets the DC output voltage to the defined value and measures the current.
:param voltage: Output voltage of the power source in V.
:returns: Measurement result: (voltage (V), current (A)).
"""
raise NotImplementedError
[docs]
def source_current_and_measure(self, current: float) -> Tuple[float, float]:
"""Sets the DC output current to the defined value and measures the voltage.
:param current: Output current of the current source in A.
:returns: Measurement result: (current (A), voltage (V)).
"""
raise NotImplementedError
[docs]
def sweep_voltage_and_measure(self, start_voltage: float, end_voltage: float, voltage_step: float, hysteresis: bool
) -> list[tuple[float, float]]:
"""Sweeps over the specified voltage range and measures the current for each voltage step.
:param start_voltage: Begin of the measurement range in V.
:param end_voltage: End of the measurement range in V.
:param voltage_step: Voltage change for each datapoint in V. Must be larger than 0.
:param hysteresis: Flags if the voltage range should be measured a second time in reverse order directly after
the initial measurement.
:returns: Measurement results: [(voltage1 (V), current1 (A)), (voltage2 (V), current2 (A)), ...]
"""
raise NotImplementedError
[docs]
def set_oscillator_frequency(self, frequency: float) -> None:
"""Sets the AC frequency of the oscillator to the defined value.
:param frequency: Oscillator frequency in Hz.
"""
raise NotImplementedError
[docs]
def set_oscillator_voltage(self, voltage: float) -> None:
"""Sets the AC voltage of the oscillator to the defined value.
:param voltage: Oscillator voltage level in V.
"""
raise NotImplementedError
[docs]
def set_oscillator_current(self, current: float) -> None:
"""Sets the AC current of the oscillator to the defined value.
:param current: Oscillator current level in A.
"""
raise NotImplementedError
[docs]
def measure_impedance(self) -> Tuple[float, float]:
"""Performs an impedance measurement and returns the magnitude and phase angle of the complex impedance vector.
:returns: Measurement result as tuple: (magnitude (1), phase (deg))
"""
raise NotImplementedError
[docs]
class Voltmeter(Channel):
"""Measures the electric potential difference between two points in a circuit."""
[docs]
@abstractmethod
def measure_voltage(self) -> float:
pass
[docs]
class Amperemeter(Channel):
"""Measures the current flowing through a circuit."""
[docs]
@abstractmethod
def measure_current(self) -> float:
pass
[docs]
class VoltageSource(Channel):
"""Supplies a constant DC voltage."""
[docs]
@abstractmethod
def source_voltage(self, voltage: float) -> None:
pass
[docs]
class CurrentSource(Channel):
"""Supplies a constant DC current."""
[docs]
@abstractmethod
def source_current(self, current: float) -> None:
pass
[docs]
class VoltageSMU(Channel):
"""Combines the functions of a voltage source and current measurement device."""
[docs]
@abstractmethod
def source_voltage_and_measure(self, voltage: float) -> Tuple[float, float]:
pass
[docs]
class CurrentSMU(Channel):
"""Combines the functions of a current source and voltage measurement device."""
[docs]
@abstractmethod
def source_current_and_measure(self, current: float) -> Tuple[float, float]:
pass
[docs]
class SweepVoltageSMU(Channel):
"""Implements hardware sweep capabilities of a voltage SMU channel."""
[docs]
@abstractmethod
def sweep_voltage_and_measure(self, start_voltage: float, end_voltage: float, voltage_step: float, hysteresis: bool
) -> list[tuple[float, float]]:
pass
[docs]
class LCRMeter(Channel):
"""Enables the measurement of the inductance (L), capacitance (C), and resistance (R) of an electronic component."""
[docs]
@abstractmethod
def source_voltage(self, voltage: float) -> None:
pass
[docs]
@abstractmethod
def source_current(self, current: float) -> None:
pass
[docs]
@abstractmethod
def set_oscillator_frequency(self, frequency: float):
pass
[docs]
@abstractmethod
def set_oscillator_voltage(self, voltage: float) -> None:
pass
[docs]
@abstractmethod
def set_oscillator_current(self, current: float) -> None:
pass
[docs]
@abstractmethod
def measure_impedance(self) -> Tuple[float, float]:
pass