Implement a Device

Important

If you have implementations of components, please consider sharing them to help expand and improve COHESIVM. You may also simply propose the implementation of a specific device or interface.

Read the Contributing Guidelines for more information.

This tutorial will guide you through the process of implementing a new device following the Device abstract base class. To simulate a realistic use case, the tutorials are based on the measurement of the sheet resistance and resistivity of materials using a four-point probe.

Channel Classes

To begin with, child classes of the Channel must be defined. The four-point resistivity measurement requires to source a specific current between two terminals and measure the resulting voltage across two other terminals. Therefore, we need two channels to act as a current source and voltmeter, respectively:

from abc import ABC
from typing import Any
from cohesivm.channels import Channel
from cohesivm.channels import CurrentSource, Voltmeter


class FPPChannel(Channel, ABC):
    """Abstract class which implements the properties and methods which all FPP channels have in common."""

    def set_property(self, name: str, value: Any) -> None:
        self.connection.set(self.identifier, name, value)

    def get_property(self, name: str) -> Any:
        return self.connection.get(self.identifier, name)

    def enable(self) -> None:
        self.set_property('ENABLE', True)

    def disable(self) -> None:
        self.set_property('DISABLE', True)


class CurrentSourceChannel(FPPChannel, CurrentSource):
    """A current source channel of the mimetic four point probe.

    :param auto_range: Setting if the current range should be set automatically.
    :param max_voltage: Limit for the voltage which is passed through the measured device.
    :raises TypeError: If the types of the parameters are wrong.
    :raises ValueError: If the ``max_voltage`` is not between 1e-4 and 10 V.
    """

    def __init__(self, auto_range: bool = True, max_voltage: float = 10.) -> None:
        identifier = 'CS'
        settings = {
            'AR': auto_range,
            'MV': max_voltage
        }
        super().__init__(identifier, settings)

    def _check_settings(self) -> None:
        if type(self.settings['AR']) is not bool:
            raise TypeError
        if type(self.settings['MV']) is not float:
            raise TypeError
        if not 1e-4 <= self.settings['MV'] <= 10.:
            raise ValueError

    def enable(self) -> None:
        self.source_current(0.)
        super().enable()

    def source_current(self, current: float) -> None:
        if type(current) is not float:
            raise TypeError
        if abs(current) > 0.2:
            raise ValueError
        self.set_property('SOURCE', current)


class VoltmeterChannel(FPPChannel, Voltmeter):
    """A voltmeter channel of the mimetic four point probe."""

    def __init__(self) -> None:
        identifier = 'VM'
        settings = None
        super().__init__(identifier, settings)

    def _check_settings(self) -> None:
        return None

    def measure_voltage(self) -> float:
        return self.get_property('MEASURE')

Here we consider an actual physical device which can be controlled through a simple Python API. This mimetic module provides only two methods set() and get() which allows us to perform all necessary tasks.

Firstly, we define a general FPPChannel which implements the abstract methods that are required by the Channel. The resource of the device connection is put into the connection by the connect() contextmanager (see below). On this, we simply call in get_property() and set_property() the methods from our mimetic API and specify which channel we are referring to. The enable() and disable() methods are self-explanatory.

The CurrentSourceChannel inherits, next to this general channel class, a specific trait class CurrentSource which includes the source_current() abstract method. Its implementation ensures through type and value checking that the current can be safely sent to the device. Additionally, the _check_settings() ensures that the settings which are specified in the constructor comply with the equipment.

In the same way, the VoltmeterChannel inherits the measure_voltage() through the Voltmeter trait class. However, the implementation is much simpler because no safety measures are required in this case.

Device Class

For the actual Device itself only a single abstract method must be implemented but we also define a constructor which allows us to do some parameter checking:

from typing import TypeVar, List
from cohesivm.devices import Device
import fpp_connect  # our mimetic API


TChannel = TypeVar('TChannel', bound='FPPChannel')


class FPPDevice(Device):
    """Implements the mimetic four point probe.

    :param com_port: The COM port where the FPP device is connected.
    :param channels: List of channels which are subclasses of the :class:`FPPChannel`. Could be a single channel or
    both. Duplicates are not allowed.
    :raises TypeError: If a channel is not a :class:`FPPChannel`.
    :raises ValueError: If duplicate channels are provided.
    """

    def __init__(self, com_port: str, channels: List[TChannel] = None) -> None:
        if channels is None:
            channels = [CurrentSourceChannel(), VoltmeterChannel()]
        else:
            channel_identifiers = set()
            for channel in channels:
                if not isinstance(channel, FPPChannel):
                    raise TypeError
                channel_identifiers.add(channel.identifier)
            if len(channels) != len(channel_identifiers):
                raise ValueError
        self.com_port = com_port
        super().__init__(channels)

    def _establish_connection(self) -> fpp_connect.Device:
        return fpp_connect.Device(self.com_port)

We check, if the provided channels are subclasses of the FPPChannel and if there are no duplicate channels. Since we hardcoded the identifier, only a single CurrentSourceChannel and a single VoltmeterChannel are allowed but we may use one or both. Additionally, we have to provide a com_port in the constructor which is used by the _establish_connection().

Example Usage

In order to test the implemented device, we build part of the fpp_connect.py mimetic module:

from typing import Any


_resistance = 100.


def get_resistance() -> float:
    global _resistance
    return _resistance


class Device:

    def __init__(self, com_port: str) -> None:
        self.com_port = com_port
        self.cs = False
        self.vm = False
        self.current = 0.
        self.auto_range = True
        self.max_voltage = 10.

    def set(self, identifier: str, name: str, value: Any) -> None:
        if name in ['ENABLE', 'DISABLE']:
            if identifier == 'CS':
                self.cs = name == 'ENABLE'
            elif identifier == 'VM':
                self.vm = name == 'ENABLE'
        elif identifier != 'CS':
            return
        elif name == 'SOURCE':
            voltage = get_resistance() * value
            self.current = value if voltage <= self.max_voltage else 0.
        elif name == 'AR':
            self.auto_range = value
        elif name == 'MV':
            self.max_voltage = value

    def get(self, identifier: str, name: str) -> Any:
        if name == 'MEASURE':
            if identifier == 'CS' and self.cs:
                return self.current
            if identifier == 'VM' and self.vm:
                voltage = get_resistance() * self.current
                return voltage
        elif identifier != 'CS':
            return
        elif name == 'AR':
            return self.auto_range
        elif name == 'MV':
            return self.max_voltage

Here, we simulate the behaviour of a simple resistor and depending on the applied current we will measure a specific voltage which corresponds to the predefined resistance after Ohm’s Law.

Finally, let’s initialize the channels and device and run a measurement:

>>> cs = CurrentSourceChannel(False, 5.)
>>> vm = VoltmeterChannel()
>>> device = FPPDevice('4', [cs, vm])
>>> with device.connect():
...     device.channels[0].source_current(0.02)
...     print(device.channels[1].measure_voltage())
2.0
>>> with device.connect():
...     device.channels[0].source_current(0.051)
...     print(device.channels[1].measure_voltage())
0.0

The first print statement outputs the expected value since we set a resistance of 100 Ω in the class which results in 2 V at 20 mA. In the second case, since the resulting voltage would exceed the max_voltage of 5 V, the current is set to 0 A and 0 V are measured. This example also confirms that the settings, as provided in the constructor, are correctly sent to the device.