Source code for PyExpLabSys.drivers.bio_logic

# pylint: disable=too-many-lines,too-few-public-methods
# pylint: disable=too-many-lines,star-args,too-many-arguments,
# pylint: disable=too-many-public-methods,

"""This module is a Python implementation of a driver around the
EC-lib DLL. It can be used to control at least the SP-150 potentiostat
from Bio-Logic under 32 bit Windows.

.. toctree::
   :maxdepth: 2

.. note :: If it is desired to run this driver and the EC-lab development DLL
 on **Linux**, this can be **achieved with Wine**. This will require
 installing both the EC-lab development package AND Python inside
 Wine. Getting Python installed is easiest, if it is a 32 bit Wine
 environment, so before starting, it is recommended to set such an environment
 up. **NOTE:** In a cursory test, it appears that also EClab itself runs under
 Wine.

.. note :: When using the different techniques with the EC-lib DLL, different
 technique files must be passed to the library, depending on **which series
 the instrument is in (VMPW series or SP-300 series)**. However, the
 definition of which instruments are in which series was not clear from the
 specification, so instead it was copied from one of the examples. The
 definition used is that if the device id of your instrument (see
 :data:`DEVICE_CODES` for the full list of device ids) is in the
 :data:`SP300SERIES` list, then it is regarded as a SP-300 series device. If
 problems are encountered when loading the technique, then this might be the
 issues and it will posible be necessary to customize :data:`SP300SERIES`.

.. note :: On **64-bit Windows systems**, you should use the ``EClib64.dll``
 instead of the ``EClib.dll``. If the EC-lab development package is installed
 in the default location, this driver will try and load the correct DLL
 automatically, if not, the DLL path will need to passed explicitely and the
 user will need to take 32 vs. 64 bit into account. **NOTE:** The relevant 32
 vs. 64 bit status is that of Windows, not of Python.

.. note:: All methods mentioned in the documentation are implemented unless
 mentioned in the list below:

 * (General) BL_GetVolumeSerialNumber (Not implemented)
 * (Communications) BL_TestCommSpeed (Not implemented)
 * (Communications) BL_GetUSBdeviceinfos (Not implemented)
 * (Channel information) BL_GetHardConf (N/A, only available w. SP300 series)
 * (Channel information) BL_SetHardConf (N/A, only available w. SP300 series)
 * (Technique) BL_UpdateParameters (Not implemented)
 * (Start stop) BL_StartChannels (Not implemented)
 * (Start stop) BL_StopChannels (Not implemented)
 * (Data) BL_GetFCTData (Not implemented)
 * (Misc) BL_SetExperimentInfos (Not implemented)
 * (Misc) BL_GetExperimentInfos (Not implemented)
 * (Misc) BL_SendMsg (Not implemented)
 * (Misc) BL_LoadFlash (Not implemented)

"""

from __future__ import print_function
import os
import sys
import inspect
from collections import namedtuple
from ctypes import c_uint8, c_uint32, c_int32
from ctypes import c_float, c_double, c_char
from ctypes import Structure
from ctypes import create_string_buffer, byref, POINTER, cast

try:
    from ctypes import WinDLL
except ImportError:
    RUNNING_SPHINX = False
    for module in sys.modules:
        if 'sphinx' in module:
            RUNNING_SPHINX = True
    # Let the module continue after this fatal import error, if we are running
    # on read the docs or we can detect that sphinx is imported
    if not (os.environ.get('READTHEDOCS', None) == 'True' or RUNNING_SPHINX):
        raise

# Numpy is optional and is only required if it is resired to get the data as
# numpy arrays
try:
    import numpy

    GOT_NUMPY = True
except ImportError:
    GOT_NUMPY = False

# Conversion of data types:
# In doc    | ctypes
# ====================
# int8      | c_int8
# int16     | c_int16
# int32     | c_int32
# uint8     | c_uint8
# unit16    | c_uint16
# uint32    | c_uint32
# boolean   | c_uint8 (FALSE=0, TRUE=1)
# single    | c_float
# double    | c_double


### Named tuples

#:A named tuple used to defined a return data field for a technique
DataField = namedtuple('DataField', ['name', 'type'])
#:The TechniqueArgument instance, that are used as args arguments, are named
#:tuples with the following fields (in order):
#:
#: * label (str): the argument label mentioned in the :ref:`specification
#:   <specification>`
#: * type (str): the type used in the :ref:`specification <specification>`
#:   ('bool', 'single' and 'integer') and possibly wrap ``[]`` around to
#:   indicate an array e.g. ``[bool]```
#: * value: The value to be passed, will usually be forwarded from ``__init__``
#:   args
#: * check (str): The bounds check to perform (if any), possible values are
#:   '>=', 'in' and 'in_float_range'
#: * check_argument: The argument(s) for the bounds check. For 'in' should be a
#:   float or int, for 'in' should be a sequence and for 'in_float_range'
#:   should be a tuple of two floats
TechniqueArgument = namedtuple(
    'TechniqueArgument', ['label', 'type', 'value', 'check', 'check_argument']
)


########## Instrument classes
[docs]class GeneralPotentiostat(object): # pylint: disable=too-many-public-methods """General driver for the potentiostats that can be controlled by the EC-lib DLL A driver for a specific potentiostat type will inherit from this class. Raises: ECLibError: All regular methods in this class use the EC-lib DLL communications library to talk with the equipment and they will raise this exception if this library reports an error. It will not be explicitly mentioned in every single method. """
[docs] def __init__(self, type_, address, EClib_dll_path): """Initialize the potentiostat driver Args: type_ (str): The device type e.g. 'KBIO_DEV_SP150' address (str): The address of the instrument, either IP address or USB0, USB1 etc EClib_dll_path (str): The path to the EClib DLL. The default directory of the DLL is C:\\EC-Lab Development Package\\EC-Lab Development Package\\ and the filename is either EClib64.dll or EClib.dll depending on whether the operating system is 64 of 32 Windows respectively. If no value is given the default location will be used and the 32/64 bit status inferred. Raises: WindowsError: If the EClib DLL cannot be found """ self._type = type_ if type_ in SP300SERIES: self.series = 'sp300' else: self.series = 'vmp3' self.address = address self._id = None self._device_info = None # Load the EClib dll if EClib_dll_path is None: EClib_dll_path = ( 'C:\\EC-Lab Development Package\\EC-Lab Development Package\\' ) # Appearently, this is the way to check whether this is 64 bit # Windows: http://stackoverflow.com/questions/2208828/ # detect-64bit-os-windows-in-python. NOTE: That it is not # sufficient to use platform.architecture(), since that will return # the 32/64 bit value of Python NOT the OS if 'PROGRAMFILES(X86)' in os.environ: EClib_dll_path += 'EClib64.dll' else: EClib_dll_path += 'EClib.dll' self._eclib = WinDLL(EClib_dll_path)
@property def id_number(self): # pylint: disable=C0103 """Return the device id as an int""" if self._id is None: return None return self._id.value @property def device_info(self): """Return the device information. Returns: dict or None: The device information as a dict or None if the device is not connected. """ if self._device_info is not None: out = structure_to_dict(self._device_info) out['DeviceCode(translated)'] = DEVICE_CODES[out['DeviceCode']] return out # General functions
[docs] def get_lib_version(self): """Return the version of the EClib communications library. Returns: str: The version string for the library """ size = c_uint32(255) version = create_string_buffer(255) ret = self._eclib.BL_GetLibVersion(byref(version), byref(size)) self.check_eclib_return_code(ret) return version.value
[docs] def get_error_message(self, error_code): """Return the error message corresponding to error_code Args: error_code (int): The error number to translate Returns: str: The error message corresponding to error_code """ message = create_string_buffer(255) number_of_chars = c_uint32(255) ret = self._eclib.BL_GetErrorMsg( error_code, byref(message), byref(number_of_chars) ) # IMPORTANT, we cannot use, self.check_eclib_return_code here, since # that internally use this method, thus we have the potential for an # infinite loop if ret < 0: err_msg = ( 'The error message is unknown, because it is the ' 'method to retrieve the error message with that fails. ' 'See the error codes sections (5.4) of the EC-Lab ' 'development package documentation to get the meaning ' 'of the error code.' ) raise ECLibError(err_msg, ret) return message.value
# Communications functions
[docs] def connect(self, timeout=5): """Connect to the instrument and return the device info. Args: timeout (int): The connect timeout Returns: dict or None: The device information as a dict or None if the device is not connected. Raises: ECLibCustomException: If this class does not match the device type """ address = create_string_buffer(self.address) self._id = c_int32() device_info = DeviceInfos() ret = self._eclib.BL_Connect( byref(address), timeout, byref(self._id), byref(device_info) ) self.check_eclib_return_code(ret) if DEVICE_CODES[device_info.DeviceCode] != self._type: message = ( 'The device type ({}) returned from the device ' 'on connect does not match the device type of ' 'the class ({})'.format( DEVICE_CODES[device_info.DeviceCode], self._type ) ) raise ECLibCustomException(-9000, message) self._device_info = device_info return self.device_info
[docs] def disconnect(self): """Disconnect from the device""" ret = self._eclib.BL_Disconnect(self._id) self.check_eclib_return_code(ret) self._id = None self._device_info = None
[docs] def test_connection(self): """Test the connection""" ret = self._eclib.BL_TestConnection(self._id) self.check_eclib_return_code(ret)
# Firmware functions
[docs] def load_firmware(self, channels, force_reload=False): """Load the library firmware on the specified channels, if it is not already loaded Args: channels (list): List with 1 integer per channel (usually 16), (0=False and 1=True), that indicates which channels the firmware should be loaded on. NOTE: The length of the list must correspond to the number of channels supported by the equipment, not the number of channels installed. In most cases it will be 16. force_reload (bool): If True the firmware is forcefully reloaded, even if it was already loaded Returns: list: List of integers indicating the success of loading the firmware on the specified channel. 0 is success and negative values are errors, whose error message can be retrieved with the get_error_message method. """ c_results = (c_int32 * len(channels))() p_results = cast(c_results, POINTER(c_int32)) c_channels = (c_uint8 * len(channels))() for index in range(len(channels)): c_channels[index] = channels[index] p_channels = cast(c_channels, POINTER(c_uint8)) ret = self._eclib.BL_LoadFirmware( self._id, p_channels, p_results, len(channels), False, force_reload, None, None, ) self.check_eclib_return_code(ret) return list(c_results)
# Channel information functions
[docs] def is_channel_plugged(self, channel): """Test if the selected channel is plugged. Args: channel (int): Selected channel (0-15 on most devices) Returns: bool: Whether the channel is plugged """ result = self._eclib.BL_IsChannelPlugged(self._id, channel) return result == 1
[docs] def get_channels_plugged(self): """Get information about which channels are plugged. Returns: (list): A list of channel plugged statusses as booleans """ status = (c_uint8 * 16)() pstatus = cast(status, POINTER(c_uint8)) ret = self._eclib.BL_GetChannelsPlugged(self._id, pstatus, 16) self.check_eclib_return_code(ret) return [result == 1 for result in status]
[docs] def get_channel_infos(self, channel): """Get information about the specified channel. Args: channel (int): Selected channel, zero based (0-15 on most devices) Returns: dict: Channel infos dict. The dict is created by conversion from :class:`.ChannelInfos` class (type :py:class:`ctypes.Structure`). See the documentation for that class for a list of available dict items. Besides the items listed, there are extra items for all the original items whose value can be converted from an integer code to a string. The keys for those values are suffixed by (translated). """ channel_info = ChannelInfos() self._eclib.BL_GetChannelInfos(self._id, channel, byref(channel_info)) out = structure_to_dict(channel_info) # Translate code to strings out['FirmwareCode(translated)'] = FIRMWARE_CODES[out['FirmwareCode']] out['AmpCode(translated)'] = AMP_CODES.get(out['AmpCode']) out['State(translated)'] = STATES.get(out['State']) out['MaxIRange(translated)'] = I_RANGES.get(out['MaxIRange']) out['MinIRange(translated)'] = I_RANGES.get(out['MinIRange']) out['MaxBandwidth'] = BANDWIDTHS.get(out['MaxBandwidth']) return out
[docs] def get_message(self, channel): """ Return a message from the firmware of a channel """ size = c_uint32(255) message = create_string_buffer(255) ret = self._eclib.BL_GetMessage(self._id, channel, byref(message), byref(size)) self.check_eclib_return_code(ret) return message.value
# Technique functions:
[docs] def load_technique(self, channel, technique, first=True, last=True): """Load a technique on the specified channel Args: channel (int): The number of the channel to load the technique onto technique (Technique): The technique to load first (bool): Whether this technique is the first technique last (bool): Thether this technique is the last technique Raises: ECLibError: On errors from the EClib communications library """ if self.series == 'sp300': filename, ext = os.path.splitext(technique.technique_filename) c_technique_file = create_string_buffer(filename + '4' + ext) else: c_technique_file = create_string_buffer(technique.technique_filename) # Init TECCParams c_tecc_params = TECCParams() # Get the array of parameter structs c_params = technique.c_args(self) # Set the len c_tecc_params.len = len(c_params) # pylint:disable=W0201 p_params = cast(c_params, POINTER(TECCParam)) c_tecc_params.pParams = p_params # pylint:disable=W0201,C0103 ret = self._eclib.BL_LoadTechnique( self._id, channel, byref(c_technique_file), c_tecc_params, first, last, False, ) self.check_eclib_return_code(ret)
[docs] def define_bool_parameter(self, label, value, index, tecc_param): """Defines a boolean TECCParam for a technique This is a library convinience function to fill out the TECCParam struct in the correct way for a boolean value. Args: label (str): The label of the parameter value (bool): The boolean value for the parameter index (int): The index of the parameter tecc_param (TECCParam): An TECCParam struct """ c_label = create_string_buffer(label) ret = self._eclib.BL_DefineBoolParameter( byref(c_label), value, index, byref(tecc_param) ) self.check_eclib_return_code(ret)
[docs] def define_single_parameter(self, label, value, index, tecc_param): """Defines a single (float) TECCParam for a technique This is a library convinience function to fill out the TECCParam struct in the correct way for a single (float) value. Args: label (str): The label of the parameter value (float): The float value for the parameter index (int): The index of the parameter tecc_param (TECCParam): An TECCParam struct """ c_label = create_string_buffer(label) ret = self._eclib.BL_DefineSglParameter( byref(c_label), c_float(value), index, byref(tecc_param), ) self.check_eclib_return_code(ret)
[docs] def define_integer_parameter(self, label, value, index, tecc_param): """Defines an integer TECCParam for a technique This is a library convinience function to fill out the TECCParam struct in the correct way for a integer value. Args: label (str): The label of the parameter value (int): The integer value for the parameter index (int): The index of the parameter tecc_param (TECCParam): An TECCParam struct """ c_label = create_string_buffer(label) ret = self._eclib.BL_DefineIntParameter( byref(c_label), value, index, byref(tecc_param) ) self.check_eclib_return_code(ret)
# Start/stop functions:
[docs] def start_channel(self, channel): """Start the channel Args: channel (int): The channel number """ ret = self._eclib.BL_StartChannel(self._id, channel) self.check_eclib_return_code(ret)
[docs] def stop_channel(self, channel): """Stop the channel Args: channel (int): The channel number """ ret = self._eclib.BL_StopChannel(self._id, channel) self.check_eclib_return_code(ret)
# Data functions
[docs] def get_current_values(self, channel): """Get the current values for the spcified channel Args: channel (int): The number of the channel (zero based) Returns: dict: A dict of current values information """ current_values = CurrentValues() ret = self._eclib.BL_GetCurrentValues(self._id, channel, byref(current_values)) self.check_eclib_return_code(ret) # Convert the struct to a dict and translate a few values out = structure_to_dict(current_values) out['State(translated)'] = STATES[out['State']] out['IRange(translated)'] = I_RANGES[out['IRange']] return out
[docs] def get_data(self, channel): """Get data for the specified channel Args: channel (int): The number of the channel (zero based) Returns: :class:`.KBIOData`: A :class:`.KBIOData` object or None if no data was available """ # Raw data is retrieved in an array of integers c_databuffer = (c_uint32 * 1000)() p_data_buffer = cast(c_databuffer, POINTER(c_uint32)) c_data_infos = DataInfos() c_current_values = CurrentValues() ret = self._eclib.BL_GetData( self._id, channel, p_data_buffer, byref(c_data_infos), byref(c_current_values), ) self.check_eclib_return_code(ret) # The KBIOData will ask the appropriate techniques for which data # fields they return data in data = KBIOData(c_databuffer, c_data_infos, c_current_values, self) if data.technique == 'KBIO_TECHID_NONE': data = None return data
[docs] def convert_numeric_into_single(self, numeric): """Convert a numeric (integer) into a float The buffer used to get data out of the device consist only of uint32s (most likely to keep its layout simple). To transfer a float, the EClib library uses a trick, wherein the value of the float is saved as a uint32, by giving the uint32 the integer values, whose bit-representation corresponds to the float that it should describe. This function is used to convert the integer back to the corresponding float. NOTE: This trick can also be performed with ctypes along the lines of: ``c_float.from_buffer(c_uint32(numeric))``, but in this driver the library version is used. Args: numeric (int): The integer that represents a float Returns: float: The float value """ c_out_float = c_float() ret = self._eclib.BL_ConvertNumericIntoSingle(numeric, byref(c_out_float)) self.check_eclib_return_code(ret) return c_out_float.value
[docs] def check_eclib_return_code(self, error_code): """Check a ECLib return code and raise the appropriate exception""" if error_code < 0: message = self.get_error_message(error_code) raise ECLibError(message, error_code)
[docs]class SP150(GeneralPotentiostat): """Specific driver for the SP-150 potentiostat"""
[docs] def __init__(self, address, EClib_dll_path=None): """Initialize the SP150 potentiostat driver See the __init__ method for the GeneralPotentiostat class for an explanation of the arguments. """ super(SP150, self).__init__( type_='KBIO_DEV_SP150', address=address, EClib_dll_path=EClib_dll_path )
########## Auxillary classes
[docs]class KBIOData(object): """Class used to represent data obtained with a get_data call The data can be obtained as lists of floats through attributes on this class. The time is always available through the 'time' attribute. The attribute names for the rest of the data, are the same as their names as listed in the field_names attribute. E.g: * kbio_data.Ewe * kbio_data.I Provided that numpy is installed, the data can also be obtained as numpy arrays by appending '_numpy' to the attribute name. E.g: * kbio_data.Ewe.numpy * kbio_data.I_numpy """
[docs] def __init__(self, c_databuffer, c_data_infos, c_current_values, instrument): """Initialize the KBIOData object Args: c_databuffer (Array of :py:class:`ctypes.c_uint32`): ctypes array of c_uint32 used as the data buffer c_data_infos (:class:`.DataInfos`): Data information structure c_current_values (:class:`CurrentValues`): Current values structure instrument (:class:`GeneralPotentiostat`): Instrument instance, should be an instance of a subclass of :class:`GeneralPotentiostat` Raises: ECLibCustomException: Where the error codes indicate the following: * -20000 means that the technique has no entry in :data:`TECHNIQUE_IDENTIFIERS_TO_CLASS` * -20001 means that the technique class has no ``data_fields`` class variable * -20002 means that the ``data_fields`` class variables of the technique does not contain the right information """ technique_id = c_data_infos.TechniqueID self.technique = TECHNIQUE_IDENTIFIERS[technique_id] # Technique 0 means no data, get_data checks for this, so just return if technique_id == 0: return # Extract the process index, used to seperate data field classes for # techniques that support that, self.process = 1 also means no_time # variable in the beginning self.process = c_data_infos.ProcessIndex # Init the data_fields self.data_fields = self._init_data_fields(instrument) # Extract the number of points and columns self.number_of_points = c_data_infos.NbRaws self.number_of_columns = c_data_infos.NbCols self.starttime = c_data_infos.StartTime # Init time property, if the measurement process index indicates that # it has a special time variable if self.process == 0: self.time = [] # Make lists for the data in properties named after the field_names for data_field in self.data_fields: setattr(self, data_field.name, []) # Parse the data self._parse_data(c_databuffer, c_current_values.TimeBase, instrument)
[docs] def _init_data_fields(self, instrument): """Initialize the data fields property""" # Get the data_fields class variable from the corresponding technique # class if self.technique not in TECHNIQUE_IDENTIFIERS_TO_CLASS: message = ( 'The technique \'{}\' has no entry in ' 'TECHNIQUE_IDENTIFIERS_TO_CLASS. The is required to be able ' 'to interpret the data'.format(self.technique) ) raise ECLibCustomException(message, -20000) technique_class = TECHNIQUE_IDENTIFIERS_TO_CLASS[self.technique] if 'data_fields' not in technique_class.__dict__: message = ( 'The technique class {} does not defined a ' '\'data_fields\' class variable, which is required for ' 'data interpretation.'.format(technique_class.__name__) ) raise ECLibCustomException(message, -20001) data_fields_complete = technique_class.data_fields if self.process == 1: # Process 1 means no special time field try: data_fields_out = data_fields_complete['no_time'] except KeyError: message = ( 'Unable to get data_fields from technique class. ' 'The data_fields class variable in the technique ' 'class must have either a \'no_time\' key when ' 'returning data with process index 1' ) raise ECLibCustomException(message, -20002) else: try: data_fields_out = data_fields_complete['common'] except KeyError: try: data_fields_out = data_fields_complete[instrument.series] except KeyError: message = ( 'Unable to get data_fields from technique class. ' 'The data_fields class variable in the technique ' 'class must have either a \'common\' or a \'{}\' ' 'key'.format(instrument.series) ) raise ECLibCustomException(message, -20002) return data_fields_out
[docs] def _parse_data(self, c_databuffer, timebase, instrument): """Parse the data Args: timebase (float): The timebase for the time calculation See :meth:`.__init__` for information about remaining args """ # The data is written as one long array of points with a certain # amount of colums. Get the index of the first item of each point by # getting the range from 0 til n_point * n_columns in jumps of # n_columns for index in range( 0, self.number_of_points * self.number_of_columns, self.number_of_columns ): # If there is a special time variable if self.process == 0: # Calculate the time t_high = c_databuffer[index] t_low = c_databuffer[index + 1] # NOTE: The documentation uses a bitshift operation for the: # ((t_high * 2 ** 32) + tlow) operation as # ((thigh << 32) + tlow), but I could not be bothered to # figure out exactly how a bitshift operation is defined for # an int class that can change internal representation, so I # just do the explicit multiplication self.time.append( self.starttime + timebase * ((t_high * 2 ** 32) + t_low) ) # Only offset reading the rest of the variables if there is a # special conversion time variable time_variable_offset = 2 else: time_variable_offset = 0 # Get remaining fields as defined in data fields for field_number, data_field in enumerate(self.data_fields): value = c_databuffer[index + time_variable_offset + field_number] # If the type is supposed to be float, convert the numeric to # float using the convinience function if data_field.type is c_float: value = instrument.convert_numeric_into_single(value) # Append the field value to the appropriate list in a property getattr(self, data_field.name).append(value) # Check that the rest of the buffer is blank for index in range(self.number_of_points * self.number_of_columns, 1000): assert c_databuffer[index] == 0
def __getattr__(self, key): """Return generated numpy arrays for the data instead of lists, if the requested property in on the form field_name + '_numpy' """ # __getattr__ is only called after the check of whether the key is in # the instance dict, therefore it is ok to raise attribute error at # this points if the key does not have the special form we expect if key.endswith('_numpy'): # Get the requested field name e.g. Ewe requested_field = key.split('_numpy')[0] if requested_field in self.data_field_names or requested_field == 'time': if GOT_NUMPY: # Determin the numpy type to convert to dtype = None if requested_field == 'time': dtype = float else: for field in self.data_fields: if field.name == requested_field: if field.type is c_float: dtype = float elif field.type is c_uint32: dtype = int if dtype is None: message = ( 'Unable to infer the numpy data type for ' 'requested field: {}'.format(requested_field) ) raise ValueError(message) # Convert the data and return the numpy array return numpy.array( # pylint: disable=no-member getattr(self, requested_field), dtype=dtype ) else: message = ( 'The numpy module is required to get the data ' 'as numpy arrays' ) raise RuntimeError(message) message = '{} object has no attribute {}'.format(self.__class__, key) raise AttributeError(message) @property def data_field_names(self): """Return a list of extra data fields names (besides time)""" return [data_field.name for data_field in self.data_fields]
[docs]class Technique(object): """Base class for techniques All specific technique classes inherits from this class. Properties available on the object: * technique_filename (str): The name of the technique filename * args (tuple): Tuple containing the Python version of the parameters (see :meth:`.__init__` for details) * c_args (array of :class:`.TECCParam`): The c-types array of :class:`.TECCParam` A specific technique, that inherits from this class **must** overwrite the **data_fields** class variable. It describes what the form is, of the data that the technique can receive. The variable should be a dict on the following form: * Some techniques, like :class:`.OCV`, have different data fields depending on the series of the instrument. In these cases the dict must contain both a 'wmp3' and a 'sp300' key. * For cases where the instrument class distinction mentioned above does not exist, like e.g. for :class:`.CV`, one can simply define a 'common' key. * All three cases above assume that the first field of the returned data is a specially formatted ``time`` field, which must not be listed directly. * Some techniques, like e.g. :class:`.SPEIS` returns data for two different processes, one of which does not contain the ``time`` field (it is assumed that the process that contains ``time`` is 0 and the one that does not is 1). In this case there must be a 'common' and a 'no-time' key (see the implementation of :class:`.SPEIS` for details). All of the entries in the dict must point to an list of :class:`.DataField` named tuples, where the two arguments are the name and the C type of the field (usually :py:class:`c_float <ctypes.c_float>` or :py:class:`c_uint32 <ctypes.c_uint32>`). The list of fields must be in the order the data fields is specified in the :ref:`specification <specification>`. """ data_fields = None
[docs] def __init__(self, args, technique_filename): """Initialize a technique Args: args (tuple): Tuple of technique arguments as TechniqueArgument instances technique_filename (str): The name of the technique filename. .. note:: This must be the vmp3 series version i.e. name.ecc NOT name4.ecc, the replacement of technique file names are taken care of in load technique """ self.args = args # The arguments must be converted to an array of TECCParam self._c_args = None self.technique_filename = technique_filename
[docs] def c_args(self, instrument): """Return the arguments struct Args: instrument (:class:`GeneralPotentiostat`): Instrument instance, should be an instance of a subclass of :class:`GeneralPotentiostat` Returns: array of :class:`TECCParam`: An ctypes array of :class:`TECCParam` Raises: ECLibCustomException: Where the error codes indicate the following: * -10000 means that an :class:`TechniqueArgument` failed the 'in' test * -10001 means that an :class:`TechniqueArgument` failed the '>=' test * -10002 means that an :class:`TechniqueArgument` failed the 'in_float_range' test * -10010 means that it was not possible to find a conversion function for the defined type * -10011 means that the value cannot be converted with the conversion function """ if self._c_args is None: self._init_c_args(instrument) return self._c_args
[docs] def _init_c_args(self, instrument): """Initialize the arguments struct Args: instrument (:class:`GeneralPotentiostat`): Instrument instance, should be an instance of a subclass of :class:`GeneralPotentiostat` """ # If it is a technique that has multistep arguments, get the number of # steps step_number = 1 for arg in self.args: if arg.label == 'Step_number': step_number = arg.value constructed_args = [] for arg in self.args: # Bounds check the argument self._check_arg(arg) # When type is dict, it means that type is a int_code -> value_str # dict, that should be used to translate the str to an int by # reversing it be able to look up codes from strs and replace # value if isinstance(arg.type, dict): value = reverse_dict(arg.type)[arg.value] param = TECCParam() instrument.define_integer_parameter(arg.label, value, 0, param) constructed_args.append(param) continue # Get the appropriate conversion function, to populate the EccParam stripped_type = arg.type.strip('[]') try: # Get the conversion method from the instrument instance, this # is named something like defined_bool_parameter conversion_function = getattr( instrument, 'define_{}_parameter'.format(stripped_type) ) except AttributeError: message = ( 'Unable to find parameter definitions function for ' 'type: {}'.format(stripped_type) ) raise ECLibCustomException(message, -10010) # If the parameter is not a multistep paramter, put the value in a # list so we can iterate over it if arg.type.startswith('[') and arg.type.endswith(']'): values = arg.value else: values = [arg.value] # Iterate over all the steps for the parameter (for most will just # be 1) for index in range(min(step_number, len(values))): param = TECCParam() try: conversion_function(arg.label, values[index], index, param) except ECLibError: message = ( '{} is not a valid value for conversion to ' 'type {} for argument \'{}\''.format( values[index], stripped_type, arg.label ) ) raise ECLibCustomException(message, -10011) constructed_args.append(param) self._c_args = (TECCParam * len(constructed_args))() for index, param in enumerate(constructed_args): self._c_args[index] = param
[docs] @staticmethod def _check_arg(arg): """Perform bounds check on a single argument""" if arg.check is None: return # If the type is not a dict (used for constants) and indicates an array elif ( not isinstance(arg.type, dict) and arg.type.startswith('[') and arg.type.endswith(']') ): values = arg.value else: values = [arg.value] # Check arguments with a list of accepted values if arg.check == 'in': for value in values: if value not in arg.check_argument: message = ( '{} is not among the valid values for \'{}\'. ' 'Valid values are: {}'.format( value, arg.label, arg.check_argument ) ) raise ECLibCustomException(message, -10000) return # Perform bounds check, if any if arg.check == '>=': for value in values: if not value >= arg.check_argument: message = ( 'Value {} for parameter \'{}\' failed ' 'check >={}'.format(value, arg.label, arg.check_argument) ) raise ECLibCustomException(message, -10001) return # Perform in two parameter range check: A < value < B if arg.check == 'in_float_range': for value in values: if not arg.check_argument[0] <= value <= arg.check_argument[1]: message = ( 'Value {} for parameter \'{}\' failed ' 'check between {} and {}'.format( value, arg.label, *arg.check_argument ) ) raise ECLibCustomException(message, -10002) return message = 'Unknown technique parameter check: {}'.format(arg.check) raise ECLibCustomException(message, -10002)
# Section 7.2 in the specification
[docs]class OCV(Technique): """Open Circuit Voltage (OCV) technique class. The OCV technique returns data on fields (in order): * time (float) * Ewe (float) * Ece (float) (only wmp3 series hardware) """ #: Data fields definition data_fields = { 'vmp3': [DataField('Ewe', c_float), DataField('Ece', c_float)], 'sp300': [DataField('Ewe', c_float)], }
[docs] def __init__( self, rest_time_T=10.0, record_every_dE=10.0, record_every_dT=0.1, E_range='KBIO_ERANGE_AUTO', ): """Initialize the OCV technique Args: rest_time_t (float): The amount of time to rest (s) record_every_dE (float): Record every dE (V) record_every_dT (float): Record evergy dT (s) E_range (str): A string describing the E range to use, see the :data:`E_RANGES` module variable for possible values """ args = ( TechniqueArgument('Rest_time_T', 'single', rest_time_T, '>=', 0), TechniqueArgument('Record_every_dE', 'single', record_every_dE, '>=', 0), TechniqueArgument('Record_every_dT', 'single', record_every_dT, '>=', 0), TechniqueArgument('E_Range', E_RANGES, E_range, 'in', E_RANGES.values()), ) super(OCV, self).__init__(args, 'ocv.ecc')
# Section 7.3 in the specification
[docs]class CV(Technique): """Cyclic Voltammetry (CV) technique class. The CV technique returns data on fields (in order): * time (float) * Ec (float) * I (float) * Ewe (float) * cycle (int) """ #:Data fields definition data_fields = { 'common': [ DataField('Ec', c_float), DataField('I', c_float), DataField('Ewe', c_float), DataField('cycle', c_uint32), ] }
[docs] def __init__( self, vs_initial, voltage_step, scan_rate, record_every_dE=0.1, average_over_dE=True, N_cycles=0, begin_measuring_I=0.5, end_measuring_I=1.0, I_range='KBIO_IRANGE_AUTO', E_range='KBIO_ERANGE_2_5', bandwidth='KBIO_BW_5', ): r"""Initialize the CV technique:: E_we ^ | E_1 | /\ | / \ | / \ E_f | E_i/ \ / | \ / | \/ | E_2 +----------------------> t Args: vs_initial (list): List (or tuple) of 5 booleans indicating whether the current step is vs. the initial one voltage_step (list): List (or tuple) of 5 floats (Ei, E1, E2, Ei, Ef) indicating the voltage steps (V) scan_rate (list): List (or tuple) of 5 floats indicating the scan rates (mV/s) record_every_dE (float): Record every dE (V) average_over_dE (bool): Whether averaging should be performed over dE N_cycles (int): The number of cycles begin_measuring_I (float): Begin step accumulation, 1 is 100% end_measuring_I (float): Begin step accumulation, 1 is 100% I_Range (str): A string describing the I range, see the :data:`I_RANGES` module variable for possible values E_range (str): A string describing the E range to use, see the :data:`E_RANGES` module variable for possible values Bandwidth (str): A string describing the bandwidth setting, see the :data:`BANDWIDTHS` module variable for possible values Raises: ValueError: If vs_initial, voltage_step and scan_rate are not all of length 5 """ for input_name in ('vs_initial', 'voltage_step', 'scan_rate'): if len(locals()[input_name]) != 5: message = 'Input \'{}\' must be of length 5, not {}'.format( input_name, len(locals()[input_name]) ) raise ValueError(message) args = ( TechniqueArgument('vs_initial', '[bool]', vs_initial, 'in', [True, False]), TechniqueArgument('Voltage_step', '[single]', voltage_step, None, None), TechniqueArgument('Scan_Rate', '[single]', scan_rate, '>=', 0.0), TechniqueArgument('Scan_number', 'integer', 2, None, None), TechniqueArgument('Record_every_dE', 'single', record_every_dE, '>=', 0.0), TechniqueArgument( 'Average_over_dE', 'bool', average_over_dE, 'in', [True, False] ), TechniqueArgument('N_Cycles', 'integer', N_cycles, '>=', 0), TechniqueArgument( 'Begin_measuring_I', 'single', begin_measuring_I, 'in_float_range', (0.0, 1.0), ), TechniqueArgument( 'End_measuring_I', 'single', end_measuring_I, 'in_float_range', (0.0, 1.0), ), TechniqueArgument('I_Range', I_RANGES, I_range, 'in', I_RANGES.values()), TechniqueArgument('E_Range', E_RANGES, E_range, 'in', E_RANGES.values()), TechniqueArgument( 'Bandwidth', BANDWIDTHS, bandwidth, 'in', BANDWIDTHS.values() ), ) super(CV, self).__init__(args, 'cv.ecc')
# Section 7.4 in the specification
[docs]class CVA(Technique): """Cyclic Voltammetry Advanced (CVA) technique class. The CVA technique returns data on fields (in order): * time (float) * Ec (float) * I (float) * Ewe (float) * cycle (int) """ #:Data fields definition data_fields = { 'common': [ DataField('Ec', c_float), DataField('I', c_float), DataField('Ewe', c_float), DataField('cycle', c_uint32), ] }
[docs] def __init__( self, # pylint: disable=too-many-locals vs_initial_scan, voltage_scan, scan_rate, vs_initial_step, voltage_step, duration_step, record_every_dE=0.1, average_over_dE=True, N_cycles=0, begin_measuring_I=0.5, end_measuring_I=1.0, record_every_dT=0.1, record_every_dI=1, trig_on_off=False, I_range='KBIO_IRANGE_AUTO', E_range='KBIO_ERANGE_2_5', bandwidth='KBIO_BW_5', ): r"""Initialize the CVA technique:: E_we ^ | E_1 | /\ | / \ | / \ E_f_____________ | E_i/ \ /<----------->| | \ / t_f |_______E_i | \/ |<-----> | E_2 | t_i +------------------------------+-------------> t | trigger Args: vs_initial_scan (list): List (or tuple) of 4 booleans indicating whether the current scan is vs. the initial one voltage_scan (list): List (or tuple) of 4 floats (Ei, E1, E2, Ef) indicating the voltage steps (V) (see diagram above) scan_rate (list): List (or tuple) of 4 floats indicating the scan rates (mV/s) record_every_dE (float): Record every dE (V) average_over_dE (bool): Whether averaging should be performed over dE N_cycles (int): The number of cycles begin_measuring_I (float): Begin step accumulation, 1 is 100% end_measuring_I (float): Begin step accumulation, 1 is 100% vs_initial_step (list): A list (or tuple) of 2 booleans indicating whether this step is vs. the initial one voltage_step (list): A list (or tuple) of 2 floats indicating the voltage steps (V) duration_step (list): A list (or tuple) of 2 floats indicating the duration of each step (s) record_every_dT (float): A float indicating the change in time that leads to a point being recorded (s) record_every_dI (float): A float indicating the change in current that leads to a point being recorded (A) trig_on_off (bool): A boolean indicating whether to use the trigger I_Range (str): A string describing the I range, see the :data:`I_RANGES` module variable for possible values E_range (str): A string describing the E range to use, see the :data:`E_RANGES` module variable for possible values Bandwidth (str): A string describing the bandwidth setting, see the :data:`BANDWIDTHS` module variable for possible values Raises: ValueError: If vs_initial, voltage_step and scan_rate are not all of length 5 """ for input_name in ('vs_initial_scan', 'voltage_scan', 'scan_rate'): if len(locals()[input_name]) != 4: message = 'Input \'{}\' must be of length 4, not {}'.format( input_name, len(locals()[input_name]) ) raise ValueError(message) for input_name in ('vs_initial_step', 'voltage_step', 'duration_step'): if len(locals()[input_name]) != 2: message = 'Input \'{}\' must be of length 2, not {}'.format( input_name, len(locals()[input_name]) ) raise ValueError(message) args = ( TechniqueArgument( 'vs_initial_scan', '[bool]', vs_initial_scan, 'in', [True, False] ), TechniqueArgument('Voltage_scan', '[single]', voltage_scan, None, None), TechniqueArgument('Scan_Rate', '[single]', scan_rate, '>=', 0.0), TechniqueArgument('Scan_number', 'integer', 2, None, None), TechniqueArgument('Record_every_dE', 'single', record_every_dE, '>=', 0.0), TechniqueArgument( 'Average_over_dE', 'bool', average_over_dE, 'in', [True, False] ), TechniqueArgument('N_Cycles', 'integer', N_cycles, '>=', 0), TechniqueArgument( 'Begin_measuring_I', 'single', begin_measuring_I, 'in_float_range', (0.0, 1.0), ), TechniqueArgument( 'End_measuring_I', 'single', end_measuring_I, 'in_float_range', (0.0, 1.0), ), TechniqueArgument( 'vs_initial_step', '[bool]', vs_initial_step, 'in', [True, False] ), TechniqueArgument('Voltage_step', '[single]', voltage_step, None, None), TechniqueArgument('Duration_step', '[single]', duration_step, None, None), TechniqueArgument('Step_number', 'integer', 1, None, None), TechniqueArgument('Record_every_dT', 'single', record_every_dT, '>=', 0.0), TechniqueArgument('Record_every_dI', 'single', record_every_dI, '>=', 0.0), TechniqueArgument('Trig_on_off', 'bool', trig_on_off, 'in', [True, False]), TechniqueArgument('I_Range', I_RANGES, I_range, 'in', I_RANGES.values()), TechniqueArgument('E_Range', E_RANGES, E_range, 'in', E_RANGES.values()), TechniqueArgument( 'Bandwidth', BANDWIDTHS, bandwidth, 'in', BANDWIDTHS.values() ), ) super(CVA, self).__init__(args, 'biovscan.ecc')
# Section 7.5 in the specification
[docs]class CP(Technique): """Chrono-Potentiometry (CP) technique class. The CP technique returns data on fields (in order): * time (float) * Ewe (float) * I (float) * cycle (int) """ #: Data fields definition data_fields = { 'common': [ DataField('Ewe', c_float), DataField('I', c_float), DataField('cycle', c_uint32), ] }
[docs] def __init__( self, current_step=(50e-6,), vs_initial=(False,), duration_step=(10.0,), record_every_dT=0.1, record_every_dE=0.001, N_cycles=0, I_range='KBIO_IRANGE_100uA', E_range='KBIO_ERANGE_2_5', bandwidth='KBIO_BW_5', ): """Initialize the CP technique NOTE: The current_step, vs_initial and duration_step must be a list or tuple with the same length. Args: current_step (list): List (or tuple) of floats indicating the current steps (A). See NOTE above. vs_initial (list): List (or tuple) of booleans indicating whether the current steps is vs. the initial one. See NOTE above. duration_step (list): List (or tuple) of floats indicating the duration of each step (s). See NOTE above. record_every_dT (float): Record every dT (s) record_every_dE (float): Record every dE (V) N_cycles (int): The number of times the technique is REPEATED. NOTE: This means that the default value is 0 which means that the technique will be run once. I_Range (str): A string describing the I range, see the :data:`I_RANGES` module variable for possible values E_range (str): A string describing the E range to use, see the :data:`E_RANGES` module variable for possible values Bandwidth (str): A string describing the bandwidth setting, see the :data:`BANDWIDTHS` module variable for possible values Raises: ValueError: On bad lengths for the list arguments """ if not len(current_step) == len(vs_initial) == len(duration_step): message = ( 'The length of current_step, vs_initial and ' 'duration_step must be the same' ) raise ValueError(message) args = ( TechniqueArgument('Current_step', '[single]', current_step, None, None), TechniqueArgument('vs_initial', '[bool]', vs_initial, 'in', [True, False]), TechniqueArgument('Duration_step', '[single]', duration_step, '>=', 0), TechniqueArgument( 'Step_number', 'integer', len(current_step), 'in', range(99) ), TechniqueArgument('Record_every_dT', 'single', record_every_dT, '>=', 0), TechniqueArgument('Record_every_dE', 'single', record_every_dE, '>=', 0), TechniqueArgument('N_Cycles', 'integer', N_cycles, '>=', 0), TechniqueArgument('I_Range', I_RANGES, I_range, 'in', I_RANGES.values()), TechniqueArgument('E_Range', E_RANGES, E_range, 'in', E_RANGES.values()), TechniqueArgument( 'Bandwidth', BANDWIDTHS, bandwidth, 'in', BANDWIDTHS.values() ), ) super(CP, self).__init__(args, 'cp.ecc')
# Section 7.6 in the specification
[docs]class CA(Technique): """Chrono-Amperometry (CA) technique class. The CA technique returns data on fields (in order): * time (float) * Ewe (float) * I (float) * cycle (int) """ #:Data fields definition data_fields = { 'common': [ DataField('Ewe', c_float), DataField('I', c_float), DataField('cycle', c_uint32), ] }
[docs] def __init__( self, voltage_step=(0.35,), vs_initial=(False,), duration_step=(10.0,), record_every_dT=0.1, record_every_dI=5e-6, N_cycles=0, I_range='KBIO_IRANGE_AUTO', E_range='KBIO_ERANGE_2_5', bandwidth='KBIO_BW_5', ): """Initialize the CA technique NOTE: The voltage_step, vs_initial and duration_step must be a list or tuple with the same length. Args: voltage_step (list): List (or tuple) of floats indicating the voltage steps (A). See NOTE above. vs_initial (list): List (or tuple) of booleans indicating whether the current steps is vs. the initial one. See NOTE above. duration_step (list): List (or tuple) of floats indicating the duration of each step (s). See NOTE above. record_every_dT (float): Record every dT (s) record_every_dI (float): Record every dI (A) N_cycles (int): The number of times the technique is REPEATED. NOTE: This means that the default value is 0 which means that the technique will be run once. I_Range (str): A string describing the I range, see the :data:`I_RANGES` module variable for possible values E_range (str): A string describing the E range to use, see the :data:`E_RANGES` module variable for possible values Bandwidth (str): A string describing the bandwidth setting, see the :data:`BANDWIDTHS` module variable for possible values Raises: ValueError: On bad lengths for the list arguments """ if not len(voltage_step) == len(vs_initial) == len(duration_step): message = ( 'The length of voltage_step, vs_initial and ' 'duration_step must be the same' ) raise ValueError(message) args = ( TechniqueArgument('Voltage_step', '[single]', voltage_step, None, None), TechniqueArgument('vs_initial', '[bool]', vs_initial, 'in', [True, False]), TechniqueArgument('Duration_step', '[single]', duration_step, '>=', 0.0), TechniqueArgument( 'Step_number', 'integer', len(voltage_step), 'in', range(99) ), TechniqueArgument('Record_every_dT', 'single', record_every_dT, '>=', 0.0), TechniqueArgument('Record_every_dI', 'single', record_every_dI, '>=', 0.0), TechniqueArgument('N_Cycles', 'integer', N_cycles, '>=', 0), TechniqueArgument('I_Range', I_RANGES, I_range, 'in', I_RANGES.values()), TechniqueArgument('E_Range', E_RANGES, E_range, 'in', E_RANGES.values()), TechniqueArgument( 'Bandwidth', BANDWIDTHS, bandwidth, 'in', BANDWIDTHS.values() ), ) super(CA, self).__init__(args, 'ca.ecc')
# Section 7.12 in the specification
[docs]class SPEIS(Technique): """Staircase Potentio Electrochemical Impedance Spectroscopy (SPEIS) technique class The SPEIS technique returns data with a different set of fields depending on which process steps it is in. If it is in process step 0 it returns data on the following fields (in order): * time (float) * Ewe (float) * I (float) * step (int) If it is in process 1 it returns data on the following fields: * freq (float) * abs_Ewe (float) * abs_I (float) * Phase_Zwe (float) * Ewe (float) * I (float) * abs_Ece (float) * abs_Ice (float) * Phase_Zce (float) * Ece (float) * t (float) * Irange (float) * step (float) Which process it is in, can be checked with the ``process`` property on the :class:`.KBIOData` object. """ #:Data fields definition data_fields = { 'common': [ DataField('Ewe', c_float), DataField('I', c_float), DataField('step', c_uint32), ], 'no_time': [ DataField('freq', c_float), DataField('abs_Ewe', c_float), DataField('abs_I', c_float), DataField('Phase_Zwe', c_float), DataField('Ewe', c_float), DataField('I', c_float), DataField('Blank0', c_float), DataField('abs_Ece', c_float), DataField('abs_Ice', c_float), DataField('Phase_Zce', c_float), DataField('Ece', c_float), DataField('Blank1', c_float), DataField('Blank2', c_float), DataField('t', c_float), # The manual says this is a float, but playing around with # strongly suggests that it is an uint corresponding to a I_RANGE DataField('Irange', c_uint32), # The manual does not mention data conversion for step, but says # that cycle should be an uint, however, this technique does not # have a cycle field, so I assume that it should have been the # step field. Also, the data maskes sense it you interpret it as # an uint. DataField('step', c_uint32), ], }
[docs] def __init__( self, # pylint: disable=too-many-locals vs_initial, vs_final, initial_voltage_step, final_voltage_step, duration_step, step_number, record_every_dT=0.1, record_every_dI=5e-6, final_frequency=100.0e3, initial_frequency=100.0, sweep=True, amplitude_voltage=0.1, frequency_number=1, average_n_times=1, correction=False, wait_for_steady=1.0, I_range='KBIO_IRANGE_AUTO', E_range='KBIO_ERANGE_2_5', bandwidth='KBIO_BW_5', ): """Initialize the SPEIS technique Args: vs_initial (bool): Whether the voltage step is vs. the initial one vs_final (bool): Whether the voltage step is vs. the final one initial_step_voltage (float): The initial step voltage (V) final_step_voltage (float): The final step voltage (V) duration_step (float): Duration of step (s) step_number (int): The number of voltage steps record_every_dT (float): Record every dT (s) record_every_dI (float): Record every dI (A) final_frequency (float): The final frequency (Hz) initial_frequency (float): The initial frequency (Hz) sweep (bool): Sweep linear/logarithmic (True for linear points spacing) amplitude_voltage (float): Amplitude of sinus (V) frequency_number (int): The number of frequencies average_n_times (int): The number of repeat times used for frequency averaging correction (bool): Non-stationary correction wait_for_steady (float): The number of periods to wait before each frequency I_Range (str): A string describing the I range, see the :data:`I_RANGES` module variable for possible values E_range (str): A string describing the E range to use, see the :data:`E_RANGES` module variable for possible values Bandwidth (str): A string describing the bandwidth setting, see the :data:`BANDWIDTHS` module variable for possible values Raises: ValueError: On bad lengths for the list arguments """ args = ( TechniqueArgument('vs_initial', 'bool', vs_initial, 'in', [True, False]), TechniqueArgument('vs_final', 'bool', vs_final, 'in', [True, False]), TechniqueArgument( 'Initial_Voltage_step', 'single', initial_voltage_step, None, None ), TechniqueArgument( 'Final_Voltage_step', 'single', final_voltage_step, None, None ), TechniqueArgument('Duration_step', 'single', duration_step, None, None), TechniqueArgument('Step_number', 'integer', step_number, 'in', range(99)), TechniqueArgument('Record_every_dT', 'single', record_every_dT, '>=', 0.0), TechniqueArgument('Record_every_dI', 'single', record_every_dI, '>=', 0.0), TechniqueArgument('Final_frequency', 'single', final_frequency, '>=', 0.0), TechniqueArgument( 'Initial_frequency', 'single', initial_frequency, '>=', 0.0 ), TechniqueArgument('sweep', 'bool', sweep, 'in', [True, False]), TechniqueArgument( 'Amplitude_Voltage', 'single', amplitude_voltage, None, None ), TechniqueArgument('Frequency_number', 'integer', frequency_number, '>=', 1), TechniqueArgument('Average_N_times', 'integer', average_n_times, '>=', 1), TechniqueArgument('Correction', 'bool', correction, 'in', [True, False]), TechniqueArgument('Wait_for_steady', 'single', wait_for_steady, '>=', 0.0), TechniqueArgument('I_Range', I_RANGES, I_range, 'in', I_RANGES.values()), TechniqueArgument('E_Range', E_RANGES, E_range, 'in', E_RANGES.values()), TechniqueArgument( 'Bandwidth', BANDWIDTHS, bandwidth, 'in', BANDWIDTHS.values() ), ) super(SPEIS, self).__init__(args, 'seisp.ecc')
# Section 7.28 in the specification
[docs]class MIR(Technique): """Manual IR (MIR) technique class The MIR technique returns no data. """ #:Data fields definition data_fields = {}
[docs] def __init__(self, rcmp_value): """Initialize the MIR technique Args: rcmp_value (float): The R value to compensate """ args = (TechniqueArgument('Rcmp_Value', 'single', rcmp_value, '>=', 0.0),) super(MIR, self).__init__(args, 'IRcmp.ecc')
########## Structs
[docs]class DeviceInfos(Structure): """Device information struct""" _fields_ = [ # Translated to string with DEVICE_CODES ('DeviceCode', c_int32), ('RAMsize', c_int32), ('CPU', c_int32), ('NumberOfChannels', c_int32), ('NumberOfSlots', c_int32), ('FirmwareVersion', c_int32), ('FirmwareDate_yyyy', c_int32), ('FirmwareDate_mm', c_int32), ('FirmwareDate_dd', c_int32), ('HTdisplayOn', c_int32), ('NbOfConnectedPC', c_int32), ] # Hack to include the fields names in doc string (and Sphinx documentation) __doc__ += '\n\n Fields:\n\n' + '\n'.join( [' * {} {}'.format(*field) for field in _fields_] )
[docs]class ChannelInfos(Structure): """Channel information structure""" _fields_ = [ ('Channel', c_int32), ('BoardVersion', c_int32), ('BoardSerialNumber', c_int32), # Translated to string with FIRMWARE_CODES ('FirmwareCode', c_int32), ('FirmwareVersion', c_int32), ('XilinxVersion', c_int32), # Translated to string with AMP_CODES ('AmpCode', c_int32), # NbAmp is not mentioned in the documentation, but is in # in the examples and the info does not make sense # without it ('NbAmp', c_int32), ('LCboard', c_int32), ('Zboard', c_int32), ('MUXboard', c_int32), ('GPRAboard', c_int32), ('MemSize', c_int32), ('MemFilled', c_int32), # Translated to string with STATES ('State', c_int32), # Translated to string with MAX_I_RANGES ('MaxIRange', c_int32), # Translated to string with MIN_I_RANGES ('MinIRange', c_int32), # Translated to string with MAX_BANDWIDTHS ('MaxBandwidth', c_int32), ('NbOfTechniques', c_int32), ] # Hack to include the fields names in doc string (and Sphinx documentation) __doc__ += '\n\n Fields:\n\n' + '\n'.join( [' * {} {}'.format(*field) for field in _fields_] )
[docs]class CurrentValues(Structure): """Current values structure""" _fields_ = [ # Translate to string with STATES ('State', c_int32), # Channel state ('MemFilled', c_int32), # Memory filled (in Bytes) ('TimeBase', c_float), # Time base (s) ('Ewe', c_float), # Working electrode potential (V) ('EweRangeMin', c_float), # Ewe min range (V) ('EweRangeMax', c_float), # Ewe max range (V) ('Ece', c_float), # Counter electrode potential (V) ('EceRangeMin', c_float), # Ece min range (V) ('EceRangeMax', c_float), # Ece max range (V) ('Eoverflow', c_int32), # Potential overflow ('I', c_float), # Current value (A) # Translate to string with IRANGE ('IRange', c_int32), # Current range ('Ioverflow', c_int32), # Current overflow ('ElapsedTime', c_float), # Elapsed time ('Freq', c_float), # Frequency (Hz) ('Rcomp', c_float), # R-compenzation (Ohm) ('Saturation', c_int32), # E or/and I saturation ] # Hack to include the fields names in doc string (and Sphinx documentation) __doc__ += '\n\n Fields:\n\n' + '\n'.join( [' * {} {}'.format(*field) for field in _fields_] )
[docs]class DataInfos(Structure): """DataInfos structure""" _fields_ = [ ('IRQskipped', c_int32), # Number of IRQ skipped ('NbRaws', c_int32), # Number of raws into the data buffer, # i.e. number of points saced in the # data buffer ('NbCols', c_int32), # Number of columns into the data # buffer, i.e. number of variables # defining a point in the data buffer ('TechniqueIndex', c_int32), # Index (0-based) of the # technique that has generated # the data ('TechniqueID', c_int32), # Identifier of the technique that # has generated the data ('ProcessIndex', c_int32), # Index (0-based) of the process # of the technique that ahs # generated the data ('loop', c_int32), # Loop number ('StartTime', c_double), # Start time (s) ] # Hack to include the fields names in doc string (and Sphinx documentation) __doc__ += '\n\n Fields:\n\n' + '\n'.join( [' * {} {}'.format(*field) for field in _fields_] )
[docs]class TECCParam(Structure): """Technique parameter""" _fields_ = [ ('ParamStr', c_char * 64), ('ParamType', c_int32), ('ParamVal', c_int32), ('ParamIndex', c_int32), ] # Hack to include the fields names in doc string (and Sphinx documentation) __doc__ += '\n\n Fields:\n\n' + '\n'.join( [' * {} {}'.format(*field) for field in _fields_] )
[docs]class TECCParams(Structure): """Technique parameters""" _fields_ = [ ('len', c_int32), ('pParams', POINTER(TECCParam)), ] # Hack to include the fields names in doc string (and Sphinx documentation) __doc__ += '\n\n Fields:\n\n' + '\n'.join( [' * {} {}'.format(*field) for field in _fields_] )
########## Exceptions
[docs]class ECLibException(Exception): """Base exception for all ECLib exceptions"""
[docs] def __init__(self, message, error_code): super(ECLibException, self).__init__(message) self.error_code = error_code
def __str__(self): """__str__ representation of the ECLibException""" string = '{} code: {}. Message \'{}\''.format( self.__class__.__name__, self.error_code, self.message ) return string def __repr__(self): """__repr__ representation of the ECLibException""" return self.__str__()
[docs]class ECLibError(ECLibException): """Exception for ECLib errors"""
[docs] def __init__(self, message, error_code): super(ECLibError, self).__init__(message, error_code)
[docs]class ECLibCustomException(ECLibException): """Exceptions that does not originate from the lib"""
[docs] def __init__(self, message, error_code): super(ECLibCustomException, self).__init__(message, error_code)
########## Functions
[docs]def structure_to_dict(structure): """Convert a ctypes.Structure to a dict""" out = {} for key, _ in structure._fields_: # pylint: disable=protected-access out[key] = getattr(structure, key) return out
[docs]def reverse_dict(dict_): """Reverse the key/value status of a dict""" return dict([[v, k] for k, v in dict_.items()])
########## Constants #:Device number to device name translation dict DEVICE_CODES = { 0: 'KBIO_DEV_VMP', 1: 'KBIO_DEV_VMP2', 2: 'KBIO_DEV_MPG', 3: 'KBIO_DEV_BISTA', 4: 'KBIO_DEV_MCS200', 5: 'KBIO_DEV_VMP3', 6: 'KBIO_DEV_VSP', 7: 'KBIO_DEV_HCP803', 8: 'KBIO_DEV_EPP400', 9: 'KBIO_DEV_EPP4000', 10: 'KBIO_DEV_BISTAT2', 11: 'KBIO_DEV_FCT150S', 12: 'KBIO_DEV_VMP300', 13: 'KBIO_DEV_SP50', 14: 'KBIO_DEV_SP150', 15: 'KBIO_DEV_FCT50S', 16: 'KBIO_DEV_SP300', 17: 'KBIO_DEV_CLB500', 18: 'KBIO_DEV_HCP1005', 19: 'KBIO_DEV_CLB2000', 20: 'KBIO_DEV_VSP300', 21: 'KBIO_DEV_SP200', 22: 'KBIO_DEV_MPG2', 23: 'KBIO_DEV_SP100', 24: 'KBIO_DEV_MOSLED', 27: 'KBIO_DEV_SP240', 255: 'KBIO_DEV_UNKNOWN', } #:Firmware number to firmware name translation dict FIRMWARE_CODES = { 0: 'KBIO_FIRM_NONE', 1: 'KBIO_FIRM_INTERPR', 4: 'KBIO_FIRM_UNKNOWN', 5: 'KBIO_FIRM_KERNEL', 8: 'KBIO_FIRM_INVALID', 10: 'KBIO_FIRM_ECAL', } #:Amplifier number to aplifier name translation dict AMP_CODES = { 0: 'KBIO_AMPL_NONE', 1: 'KBIO_AMPL_2A', 2: 'KBIO_AMPL_1A', 3: 'KBIO_AMPL_5A', 4: 'KBIO_AMPL_10A', 5: 'KBIO_AMPL_20A', 6: 'KBIO_AMPL_HEUS', 7: 'KBIO_AMPL_LC', 8: 'KBIO_AMPL_80A', 9: 'KBIO_AMPL_4AI', 10: 'KBIO_AMPL_PAC', 11: 'KBIO_AMPL_4AI_VSP', 12: 'KBIO_AMPL_LC_VSP', 13: 'KBIO_AMPL_UNDEF', 14: 'KBIO_AMPL_MUIC', 15: 'KBIO_AMPL_NONE_GIL', 16: 'KBIO_AMPL_8AI', 17: 'KBIO_AMPL_LB500', 18: 'KBIO_AMPL_100A5V', 19: 'KBIO_AMPL_LB2000', 20: 'KBIO_AMPL_1A48V', 21: 'KBIO_AMPL_4A10V', } #:I range number to I range name translation dict I_RANGES = { 0: 'KBIO_IRANGE_100pA', 1: 'KBIO_IRANGE_1nA', 2: 'KBIO_IRANGE_10nA', 3: 'KBIO_IRANGE_100nA', 4: 'KBIO_IRANGE_1uA', 5: 'KBIO_IRANGE_10uA', 6: 'KBIO_IRANGE_100uA', 7: 'KBIO_IRANGE_1mA', 8: 'KBIO_IRANGE_10mA', 9: 'KBIO_IRANGE_100mA', 10: 'KBIO_IRANGE_1A', 11: 'KBIO_IRANGE_BOOSTER', 12: 'KBIO_IRANGE_AUTO', 13: 'KBIO_IRANGE_10pA', # IRANGE_100pA + Igain x10 14: 'KBIO_IRANGE_1pA', # IRANGE_100pA + Igain x100 } #:Bandwidth number to bandwidth name translation dict BANDWIDTHS = { 1: 'KBIO_BW_1', 2: 'KBIO_BW_2', 3: 'KBIO_BW_3', 4: 'KBIO_BW_4', 5: 'KBIO_BW_5', 6: 'KBIO_BW_6', 7: 'KBIO_BW_7', 8: 'KBIO_BW_8', 9: 'KBIO_BW_9', } #:E range number to E range name translation dict E_RANGES = { 0: 'KBIO_ERANGE_2_5', 1: 'KBIO_ERANGE_5', 2: 'KBIO_ERANGE_10', 3: 'KBIO_ERANGE_AUTO', } #:State number to state name translation dict STATES = {0: 'KBIO_STATE_STOP', 1: 'KBIO_STATE_RUN', 2: 'KBIO_STATE_PAUSE'} #:Technique number to technique name translation dict TECHNIQUE_IDENTIFIERS = { 0: 'KBIO_TECHID_NONE', 100: 'KBIO_TECHID_OCV', 101: 'KBIO_TECHID_CA', 102: 'KBIO_TECHID_CP', 103: 'KBIO_TECHID_CV', 104: 'KBIO_TECHID_PEIS', 105: 'KBIO_TECHID_POTPULSE', 106: 'KBIO_TECHID_GALPULSE', 107: 'KBIO_TECHID_GEIS', 108: 'KBIO_TECHID_STACKPEIS_SLAVE', 109: 'KBIO_TECHID_STACKPEIS', 110: 'KBIO_TECHID_CPOWER', 111: 'KBIO_TECHID_CLOAD', 112: 'KBIO_TECHID_FCT', 113: 'KBIO_TECHID_SPEIS', 114: 'KBIO_TECHID_SGEIS', 115: 'KBIO_TECHID_STACKPDYN', 116: 'KBIO_TECHID_STACKPDYN_SLAVE', 117: 'KBIO_TECHID_STACKGDYN', 118: 'KBIO_TECHID_STACKGEIS_SLAVE', 119: 'KBIO_TECHID_STACKGEIS', 120: 'KBIO_TECHID_STACKGDYN_SLAVE', 121: 'KBIO_TECHID_CPO', 122: 'KBIO_TECHID_CGA', 123: 'KBIO_TECHID_COKINE', 124: 'KBIO_TECHID_PDYN', 125: 'KBIO_TECHID_GDYN', 126: 'KBIO_TECHID_CVA', 127: 'KBIO_TECHID_DPV', 128: 'KBIO_TECHID_SWV', 129: 'KBIO_TECHID_NPV', 130: 'KBIO_TECHID_RNPV', 131: 'KBIO_TECHID_DNPV', 132: 'KBIO_TECHID_DPA', 133: 'KBIO_TECHID_EVT', 134: 'KBIO_TECHID_LP', 135: 'KBIO_TECHID_GC', 136: 'KBIO_TECHID_CPP', 137: 'KBIO_TECHID_PDP', 138: 'KBIO_TECHID_PSP', 139: 'KBIO_TECHID_ZRA', 140: 'KBIO_TECHID_MIR', 141: 'KBIO_TECHID_PZIR', 142: 'KBIO_TECHID_GZIR', 150: 'KBIO_TECHID_LOOP', 151: 'KBIO_TECHID_TO', 152: 'KBIO_TECHID_TI', 153: 'KBIO_TECHID_TOS', 155: 'KBIO_TECHID_CPLIMIT', 156: 'KBIO_TECHID_GDYNLIMIT', 157: 'KBIO_TECHID_CALIMIT', 158: 'KBIO_TECHID_PDYNLIMIT', 159: 'KBIO_TECHID_LASV', 167: 'KBIO_TECHID_MP', 169: 'KBIO_TECHID_CASG', 170: 'KBIO_TECHID_CASP', } #:Technique name to technique class translation dict. IMPORTANT. Add newly #:implemented techniques to this dictionary TECHNIQUE_IDENTIFIERS_TO_CLASS = { 'KBIO_TECHID_OCV': OCV, 'KBIO_TECHID_CP': CP, 'KBIO_TECHID_CA': CA, 'KBIO_TECHID_CV': CV, 'KBIO_TECHID_CVA': CVA, 'KBIO_TECHID_SPEIS': SPEIS, } #:List of devices in the WMP4/SP300 series SP300SERIES = [ 'KBIO_DEV_SP100', 'KBIO_DEV_SP200', 'KBIO_DEV_SP300', 'KBIO_DEV_VSP300', 'KBIO_DEV_VMP300', 'KBIO_DEV_SP240', ] # Hack to make links for classes in the documentation __doc__ += '\n\nInstrument classes:\n' # pylint: disable=W0622 for name, klass in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(klass, GeneralPotentiostat) or klass is GeneralPotentiostat: __doc__ += ' * :class:`.{.__name__}`\n'.format(klass) __doc__ += '\n\nTechniques:\n' for name, klass in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(klass, Technique): __doc__ += ' * :class:`.{.__name__}`\n'.format(klass)