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.