# -*- coding: utf-8 -*-
#
# Copyright © keithley2600 Project Contributors
# Licensed under the terms of the MIT License
# (see keithley2600/__init__.py for details)

"""
Core driver with the low level functions.
"""

# system imports
import visa
import logging
import threading
import numpy as np
import time
from threading import RLock
from xdrlib import Error as XDRError

# local import
from keithley2600.keithley_doc import (CONSTANTS, FUNCTIONS, PROPERTIES,
                                       CLASSES, PROPERTY_LISTS, ALL_CMDS)
from keithley2600.result_table import FETResultTable

__version__ = 'v1.3.5-dev1'

logger = logging.getLogger(__name__)


def log_to_screen(level=logging.DEBUG):
    log_to_stream(None, level)  # sys.stderr by default


def log_to_stream(stream_output, level=logging.DEBUG):
    logger.setLevel(level)
    ch = logging.StreamHandler(stream_output)
    ch.setLevel(level)

    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    ch.setFormatter(formatter)

    logger.addHandler(ch)


class MagicPropertyList(object):
    """Mimics a Keithley TSP property list

    Class which mimics a Keithley TSP property list and can be dynamically
    created. It forwards all calls to the :func:`_read` method of the parent
    class and assignments to the :func:`_write` method. Arbitrary values can be
    assigned, as long as :func:`_write` can handle them.

    This class is designed to look like a Keithley TSP "attribute" list,
    forward function calls to the Keithley, and return the results.

    """

    def __init__(self, name, parent):
        if not isinstance(name, str):
            raise ValueError('First argument must be of type str.')
        self._name = name
        self._parent = parent

    def __getitem__(self, i):
        """Gets i-th item: query item from parent class

        :param int i: An integer item number

        :returns: Result from _query call of parent class.
        """
        new_name = '%s[%s]' % (self._name, i)
        return self._parent._query(new_name)

    def __setitem__(self, i, value):
        """Sets i-th item: set item at parent class

        :param int i: An integer item number
        :param value: An input object that can be accepted by parent class.

        """
        value = self._parent._convert_input(value)
        new_name = '%s[%s] = %s' % (self._name, i, value)
        self._parent._write(new_name)

    def __iter__(self):
        return self

    def getdoc(self):
        """Prevent pydoc from trying to document this class. This could conflict with
        on-demand creation of attributes."""
        pass


class MagicFunction(object):
    """Mimics a Keithley TSP function

    Class which mimics a function and can be dynamically created. It forwards
    all calls to the :func:`_query` method of the parent class and returns the
    result from :func:`_query`. Calls accept arbitrary arguments, as long as
    :func:`_query` can handle them.

    This class is designed to look like a Keithley TSP function, forward
    function calls to the Keithley, and return the results.
    """

    def __init__(self, name, parent):
        if not isinstance(name, str):
            raise ValueError('First argument must be of type str.')
        self._name = name
        self._parent = parent

    def __call__(self, *args, **kwargs):
        """Pass on calls to :func:`parent._write`, store result in variable.
        Querying results from function calls directly may result in
        a VisaIOError if the function does not return anything."""

        # convert incompatible arguments, return all arguments as tuple
        args = tuple(self._parent._convert_input(a) for a in args)
        # remove outside brackets and all quotation marks
        args_string = str(args).strip("(),").replace("'", "")

        # pass on a string representation of the function call to self._parent._query
        return self._parent._query('%s(%s)' % (self._name, args_string))


class MagicClass(object):
    """Mimics a TSP command group

    Class which dynamically creates new attributes on access. These can be
    functions, properties, or other classes.

    Attribute setters and getters are forwarded to :func:`_write` and
    :func:`_query` functions from the parent class. New functions are created
    as instances of :class:`MagicFunction`, new classes are created as
    instances of :class:`MagicClass`.

    MagicClass is designed to mimic a Keithley TSP command group with
    functions, attributes, and subordinate command groups.

    :Examples:

        >>> inst = MagicClass('keithley')
        >>> inst.reset()  # Dynamically creates a new attribute 'reset' as an
        ...               # instance of MagicFunction, then calls it.
        >>> inst.beeper  # Dynamically creates new attribute 'beeper' and sets
        ...              # it to a new MagicClass instance.
        >>> inst.beeper.enable  # Fakes the property 'enable' of 'beeper'
        ...                     # with _write as setter and _query as getter.

    """

    _name = ''
    _parent = None

    def __init__(self, name, parent=None):
        assert isinstance(name, str)
        self._name = name
        if parent is not None:
            self._parent = parent

    def __getattr__(self, attr_name):
        """Custom getter

        Get attributes as usual if they exist. Otherwise, fall back to
        :func:`__get_global_handler`.
        """
        try:
            try:
                # check if attribute already exists. return attr if yes.
                return object.__getattr__(self, attr_name)
            except AttributeError:
                # check if key already exists. return value if yes.
                return self.__dict__[attr_name]
        except KeyError:
            # handle if not
            return self.__get_global_handler(attr_name)

    def __get_global_handler(self, attr_name):
        """Custom getter

        Creates an attribute as :class:`MagicClass`, :class:`MagicFunction` or
        :class:`MagicPropertyList` instance if it is an expected Keithley TSP
        command group, function or property list. Queries and returns the value
        if the attribute corresponds to a Keithley TSP constant. Otherwise
        raises a :class:`AttributeError`.

        :param str attr_name: Attribute name.

        :returns: Instance of :class:`MagicClass`, :class:`MagicFunction` or
            :class:`MagicPropertyList`.

        :raises: :class:`AttributeError` if attribute is not expected.
        """

        # create callable sub-class for new attr
        new_name = '%s.%s' % (self._name, attr_name)
        new_name = new_name.strip('.')

        if attr_name in FUNCTIONS:
            handler = MagicFunction(new_name, parent=self)
            self.__dict__[new_name] = handler

        elif attr_name in PROPERTY_LISTS:
            handler = MagicPropertyList(new_name, parent=self)

        elif attr_name in PROPERTIES or attr_name in CONSTANTS:
            if new_name in PROPERTY_LISTS:
                handler = MagicPropertyList(new_name, parent=self)
            else:
                handler = self._query(new_name)

        elif attr_name in CLASSES:
            handler = MagicClass(new_name, parent=self)
            self.__dict__[new_name] = handler

        else:
            raise AttributeError(
                "'%s' object has no attribute '%s'" % (type(self), attr_name)
                )

        return handler

    def __setattr__(self, attr_name, value):
        """Custom setter

        Forward setting commands to `self._write` for expected Keithley TSP
        attributes. Otherwise use default setter.

        :param str attr_name: Attribute name.
        :param value: Value to set.

        :raises: :class:`ValueError` if trying to write a value to read-only
            Keithley attributes.
        """
        if attr_name in PROPERTIES:
            value = self._convert_input(value)
            self._write('%s.%s = %s' % (self._name, attr_name, value))
        elif attr_name in CONSTANTS:
            raise ValueError('%s.%s is read-only.' % (self._name, attr_name))
        else:
            object.__setattr__(self, attr_name, value)
            self.__dict__[attr_name] = value

    def _write(self, value):
        """Forward _write calls to parent class."""
        self._parent._write(value)

    def _query(self, value):
        """Forward _query calls to parent class."""
        return self._parent._query(value)

    def _convert_input(self, value):
        """Forward _convert_input calls to parent class."""
        try:
            return self._parent._convert_input(value)
        except AttributeError:
            return value

    def __getitem__(self, i):
        """Return new MagicClass instance for every item."""
        new_name = '%s[%s]' % (self._name, i)
        new_class = MagicClass(new_name, parent=self)
        return new_class

    def __iter__(self):
        return self

    def __dir__(self):
        prefix = self._name + '.' if self._name else ''

        sub_cmds = (c.replace(prefix, '') for c in ALL_CMDS if c.startswith(prefix))
        sub_cmds = list(set(c.split('.')[0] for c in sub_cmds))

        return sub_cmds


class KeithleyIOError(Exception):
    """Raised when no Keithley instrument is connected."""
    pass


class KeithleyError(Exception):
    """Raised for error messages from the Keithley itself."""
    pass


class Keithley2600Base(MagicClass):
    """Keithley2600 driver

    Keithley driver for base functionality. It replicates the functionality and
    syntax from the Keithley TSP commands, which have a syntax similar to
    python. Attributes are created on-access if they correspond to Keithley TSP
    type commands.

    :param str visa_address: Visa address of the instrument.
    :param str visa_library: PyVisa backend specification. Defaults to "@py"
        for pyvisa-py but NI-VISA may be appropriate, depending on the
        interface type.
    :param bool raise_keithley_errors: If ``True``, all Keithley errors will be
        raised as Python errors instead of being ignored. This causes
        significant communication overhead because the Keithley's error queue
        is read after each command. Defaults to ``False``.
    :param kwargs: Keyword arguments passed on to the visa connection, for
        instance baude-rate or timeout. If not given, reasonable defaults will
        be used.

    :cvar connection: Attribute holding a reference to the actual connection.
    :cvar bool connected: ``True`` if connected to an instrument, ``False``
        otherwise.
    :cvar bool busy: ``True`` if a measurement is running, ``False`` otherwise.
    :cvar list TO_TSP_LIST: List of python types which will be converted to
        Keithley TSP lists by this driver and can be used as inputs. Currently,
        those are :class:`list`, :class:`numpy.ndarray`, :class:`tuple`,
        :class:`set` and :class:`range` (:class:`xrange` in Python 2).
    :cvar int CHUNK_SIZE: Maximum length of lists which can be sent to the
        Keithley. Longer lists will be transferred in chunks.

    .. note::

        See the Keithley 2600 reference manual for all available commands and
        arguments. Almost all Keithley TSP commands can be used with this
        driver. Not supported are:

         - ``lan.trigger[N].connected``: conflicts with the connected attribute
         - ``io.output()``: conflicts with ``smuX.source.output``
         - All Keithley IV sweep commands. We implement our own in
           :class:`Keithley2600`.

    :Examples:

        >>> keithley = Keithley2600Base('TCPIP0::192.168.2.121::INSTR')
        >>> keithley.smua.measure.v()  # measures voltage at smuA
        >>> keithley.smua.source.levelv = -40  # applies -40V to smuA

    """

    connection = None
    connected = False
    busy = False

    TO_TSP_LIST = (list, np.ndarray, tuple, set, range)
    CHUNK_SIZE = 50

    _lock = RLock()

    def __init__(self, visa_address, visa_library='@py', raise_keithley_errors=False, **kwargs):

        MagicClass.__init__(self, name='', parent=self)
        self._name = ''

        self.abort_event = threading.Event()

        self.visa_address = visa_address
        self.visa_library = visa_library
        self._connection_kwargs = kwargs

        self.raise_keithley_errors = raise_keithley_errors

        # open visa resource manager with selected library / backend
        self.rm = visa.ResourceManager(self.visa_library)
        # connect to keithley
        self.connect(**kwargs)

    def __repr__(self):
        return '<%s(%s)>' % (type(self).__name__, self.visa_address)

# =============================================================================
# Connect to keithley
# =============================================================================

    def connect(self, **kwargs):
        """
        Connects to Keithley.

        :param kwargs: Keyword arguments for Visa connection.

        """
        kwargs = kwargs or self._connection_kwargs  # use specified or remembered kwargs
        try:
            self.connection = self.rm.open_resource(self.visa_address, **kwargs)
            self.connection.read_termination = '\n'
            self.connected = True
            logger.debug('Connected to Keithley at %s.' % self.visa_address)
        except ValueError:
            self.connection = None
            self.connected = False
            raise
        except ConnectionError:
            logger.info('Connection error. Please check that ' +
                        'no other program is connected.')
            self.connection = None
            self.connected = False
        except AttributeError:
            logger.info('Invalid VISA address %s.' % self.visa_address)
            self.connection = None
            self.connected = False
        except Exception:
            logger.info('Could not connect to Keithley at %s.' % self.visa_address)
            self.connection = None
            self.connected = False

    def disconnect(self):
        """
        Disconnects from Keithley.
        """
        if self.connection:
            try:
                self.connection.close()
                self.connection = None
                self.connected = False
                del self.connection
                logger.debug('Disconnected from Keithley at %s.' % self.visa_address)
            except AttributeError:
                self.connected = False
                pass

# =============================================================================
# Define I/O
# =============================================================================

    def _write(self, value):
        """
        Writes text to Keithley. Input must be a string.
        """

        with self._lock:
            logger.debug('write: %s' % value)

            if self.connection:

                if self.raise_keithley_errors and 'errorqueue' not in value:
                    self.errorqueue.clear()

                self.connection.write(value)

                if self.raise_keithley_errors and 'errorqueue' not in value:
                    err = self.errorqueue.next()
                    if err[0] != 0:
                        raise KeithleyError("Error during command '{0}': {1}".format(
                            value, err[1]))
            else:
                raise KeithleyIOError(
                    "No connection to keithley present. Try to call 'connect'.")

    def _query(self, value):
        """
        Queries and expects response from Keithley. Input must be a string.
        """

        with self._lock:
            logger.debug('write: print(%s)' % value)
            if self.connection:

                if self.raise_keithley_errors and 'errorqueue' not in value:
                    self.errorqueue.clear()

                try:
                    r = self.connection.query('print(%s)' % value)
                    logger.debug('read: %s' % r)
                except XDRError:
                    r = 'nil'
                    logger.debug('read failed: unpack-error')

                if self.raise_keithley_errors and 'errorqueue' not in value:
                    err = self.errorqueue.next()
                    if err[0] != 0:
                        err_msg = err[1].replace('TSP Runtime error at line 1: ', '')
                        raise KeithleyError("Error during command '{0}': {1}".format(
                            value, err_msg))

                return self._parse_response(r)
            else:
                raise KeithleyIOError(
                    "No connection to keithley present. Try to call 'connect'.")

    @staticmethod
    def _parse_single_response(string):

        # Dictionary to convert from Keithley TSP to Python types.
        # Note that emtpy strings are converted to `None`. This is necessary
        # since `self.connection.query('print(func())')` returns an empty
        # string if the TSP function `func()` returns 'nil'.
        conversion_dict = {'true': True, 'false': False, 'nil': None, '': None}

        try:
            r = float(string)
            if r.is_integer():
                r = int(r)
        except ValueError:
            if string in conversion_dict.keys():
                r = conversion_dict[string]
            else:
                r = string

        return r

    def _parse_response(self, string):

        string_list = string.split('\t')

        converted_tuple = tuple(self._parse_single_response(s) for s in string_list)

        if len(converted_tuple) == 1:
            return converted_tuple[0]
        else:
            return converted_tuple

    def _convert_input(self, value):
        """ Convert bool to lower case string and list / tuples to comma
        delimited string enclosed by curly brackets."""
        if isinstance(value, bool):
            # convert bool True to string 'true'
            value = str(value).lower()
        elif isinstance(value, self.TO_TSP_LIST):
            # convert some iterables to a TSP type list '{1,2,3,4}'
            value = '{%s}' % ', '.join(map(str, value))
        elif isinstance(value, MagicClass):
            # convert keithley object to string with its name
            value = value._name

        return value


class Keithley2600(Keithley2600Base):
    """Keithley2600 driver with high level functionality

    Keithley driver with access to base functions and higher level functions
    such as IV measurements, transfer and output curves, etc. Inherits from
    :class:`Keithley2600Base`. Base commands replicate the functionality and
    syntax of Keithley TSP functions.

    :param str visa_address: Visa address of the instrument.
    :param str visa_library: PyVisa backend specification. Defaults to "@py"
        for pyvisa-py but NI-VISA may be appropriate, depending on the
        interface type.
    :param bool raise_keithley_errors: If ``True``, all Keithley errors will be
        raised as Python errors instead of being ignored. This causes
        significant communication overhead because the Keithley's error queue
        is read after each command. Defaults to ``False``.
    :param kwargs: Keyword arguments passed on to the visa connection, for
        instance baude-rate or timeout. If not given, reasonable defaults will
        be used.

    :cvar connection: Attribute holding a reference to the actual connection.
    :cvar bool connected: ``True`` if connected to an instrument, ``False``
        otherwise.
    :cvar bool busy: ``True`` if a measurement is running, ``False`` otherwise.

    :Examples:

        *Base commands from Keithley TSP*:

        >>> k = Keithley2600('TCPIP0::192.168.2.121::INSTR')
        >>> volts = k.smua.measure.v()  # measures and returns the smuA voltage
        >>> k.smua.source.levelv = -40  # sets source level of smuA
        >>> k.smua.nvbuffer1.clear()  # clears nvbuffer1 of smuA

        *New mid-level commands*:

        >>> data = k.readBuffer(k.smua.nvbuffer1)
        >>> errs = k.readErrorQueue()
        >>> k.setIntegrationTime(k.smua, 0.001) # in sec

        >>> k.applyVoltage(k.smua, -60) # applies -60V to smuA
        >>> k.applyCurrent(k.smub, 0.1) # sources 0.1A from smuB
        >>> k.rampToVoltage(k.smua, 10, delay=0.1, step_size=1)

        >>> # voltage sweeps, single and dual SMU
        >>> k.voltageSweepSingleSMU(smu=k.smua, smu_sweeplist=range(0, 61),
        ...                         t_int=0.1, delay=-1, pulsed=False)
        >>> k.voltageSweepDualSMU(smu1=k.smua, smu2=k.smub,
        ...                       smu1_sweeplist=range(0, 61),
        ...                       smu2_sweeplist=range(0, 61),
        ...                       t_int=0.1, delay=-1, pulsed=False)

        *New high-level commands*:

        >>> data1 = k.outputMeasurement(...) # records output curve
        >>> data2 = k.transferMeasurement(...) # records transfer curve

    """

    SMU_LIST = ['smua', 'smub']

    def __init__(self, visa_address, visa_library='@py', raise_keithley_errors=False,
                 **kwargs):
        Keithley2600Base.__init__(self, visa_address, visa_library,
                                  raise_keithley_errors=raise_keithley_errors, **kwargs)

    def __repr__(self):
        return '<%s(%s)>' % (type(self).__name__, self.visa_address)

    def _check_smu(self, smu):
        """
        Check if selected smu is indeed present.

        :param smu: A keithley smu instance.
        """
        if self._get_smu_string(smu) not in self.SMU_LIST:
            raise RuntimeError('The specified SMU does not exist.')

    @staticmethod
    def _get_smu_string(smu):
        return smu._name.split('.')[-1]

# =============================================================================
# Define lower level control functions
# =============================================================================

    def readErrorQueue(self):
        """
        Returns all entries from the Keithley error queue and clears the queue.

        :returns: List of errors from the Keithley error queue. Each entry is a
            tuple ``(error_code, message, severity, error_node)``. If the queue
            is empty, an empty list is returned.
        :rtype: list
        """

        error_list = []

        err = self.errorqueue.next()
        while err[0] != 0:
            error_list += err
            err = self.errorqueue.next()

        return error_list

    @staticmethod
    def readBuffer(buffer):
        """
        Reads buffer values and returns them as a list. This can be done more
        quickly by calling :attr:`buffer.readings` but such a call may fail due
        to I/O limitations of the keithley if the returned list is too long.

        :param buffer: A keithley buffer instance.

        :returns: A list with buffer readings.
        :rtype: list
        """
        list_out = []
        for i in range(0, int(buffer.n)):
            list_out.append(buffer.readings[i+1])

        return list_out

    def setIntegrationTime(self, smu, t_int):
        """
        Sets the integration time of SMU for measurements in sec.

        :param smu: A keithley smu instance.
        :param float t_int: Integration time in sec. Value must be between
            1/1000 and 25 power line cycles (50Hz or 60 Hz).
        :raises: :class:`ValueError` for too short or too long integration
            times.
        """

        self._check_smu(smu)

        # determine number of power-line-cycles used for integration
        freq = self.localnode.linefreq
        nplc = t_int * freq

        if nplc < 0.001 or nplc > 25:
            raise ValueError('Integration time must be between 0.001 and 25 ' +
                             'power line cycles of 1/(%s Hz).' % freq)
        smu.measure.nplc = nplc

    def applyVoltage(self, smu, voltage):
        """
        Turns on the specified SMU and applies a voltage.

        :param smu: A keithley smu instance.
        :param float voltage: Voltage to apply in Volts.
        """

        self._check_smu(smu)

        smu.source.levelv = voltage
        smu.source.func = smu.OUTPUT_DCVOLTS
        smu.source.output = smu.OUTPUT_ON

    def applyCurrent(self, smu, curr):
        """
        Turns on the specified SMU and sources a current.

        :param smu: A keithley smu instance.
        :param float curr: Current to apply in Ampere.
        """
        self._check_smu(smu)

        smu.source.leveli = curr
        smu.source.func = smu.OUTPUT_DCAMPS
        smu.source.output = smu.OUTPUT_ON

    def measureVoltage(self, smu):
        """
        Measures a voltage at the specified SMU.

        :param smu: A keithley smu instance.

        :returns: Measured voltage in Volts.
        :rtype: float
        """

        self._check_smu(smu)

        return smu.measure.v()

    def measureCurrent(self, smu):
        """
        Measures a current at the specified SMU.

        :param smu: A keithley smu instance.

        :returns: Measured current in Ampere.
        :rtype: float
        """
        self._check_smu(smu)

        return smu.measure.i()

    def rampToVoltage(self, smu, target_volt, delay=0.1, step_size=1):
        """
        Ramps up the voltage of the specified SMU. Beeps when done.

        :param smu: A keithley smu instance.
        :param float target_volt: Target voltage in Volts.
        :param float step_size: Size of the voltage steps in Volts.
        :param float delay: Delay between steps in sec.
        """

        self._check_smu(smu)

        smu.source.output = smu.OUTPUT_ON

        # get current voltage
        vcurr = smu.source.levelv
        if vcurr == target_volt:
            return

        self.display.smua.measure.func = self.display.MEASURE_DCVOLTS
        self.display.smub.measure.func = self.display.MEASURE_DCVOLTS

        step = np.sign(target_volt - vcurr) * abs(step_size)

        for v in np.arange(vcurr, target_volt, step):
            smu.source.levelv = v
            smu.measure.v()
            time.sleep(delay)

        smu.source.levelv = target_volt
        logger.info('Gate voltage set to Vg = %s V.' % round(target_volt))

        self.beeper.beep(0.3, 2400)

    def voltageSweepSingleSMU(self, smu, smu_sweeplist, t_int, delay, pulsed):
        """
        Sweeps the voltage through the specified list of steps at the given
        SMU. Measures and returns the current and voltage during the sweep.

        :param smu: A keithley smu instance.
        :param smu_sweeplist: Voltages to sweep through (can be a numpy array,
            list, tuple or range / xrange).
        :param float t_int: Integration time per data point. Must be between
            0.001 to 25 times the power line frequency (50Hz or 60Hz).
        :param float delay: Settling delay before each measurement. A value of
            -1 automatically starts a measurement once the current is stable.
        :param bool pulsed: Select pulsed or continuous sweep. In a pulsed
            sweep, the voltage is always reset to zero between data points.

        :returns: Lists of voltages and currents measured during the sweep (in
            Volt and Ampere, respectively): ``(v_smu, i_smu)``.
        :rtype: (list, list)
        """

        # input checks
        self._check_smu(smu)
        assert isinstance(smu_sweeplist, self.TO_TSP_LIST)

        # set state to busy
        self.busy = True
        # Define lists containing results.
        # If we abort early, we have something to return.
        v_smu, i_smu = [], []

        if self.abort_event.is_set():
            self.busy = False
            return v_smu, i_smu

        # setup smu to sweep through list on trigger
        # send sweep_list over in chunks if too long
        if len(smu_sweeplist) > self.CHUNK_SIZE:
            self._write('mylist = {}')
            for num in smu_sweeplist:
                self._write('table.insert(mylist, %s)' % num)
            smu.trigger.source.listv('mylist')
        else:
            smu.trigger.source.listv(smu_sweeplist)

        smu.trigger.source.action = smu.ENABLE

        # CONFIGURE INTEGRATION TIME FOR EACH MEASUREMENT
        self.setIntegrationTime(smu, t_int)

        # CONFIGURE SETTLING TIME FOR GATE VOLTAGE, I-LIMIT, ETC...
        smu.measure.delay = delay
        smu.measure.autorangei = smu.AUTORANGE_ON

        # smu.trigger.source.limiti = 0.1

        smu.source.func = smu.OUTPUT_DCVOLTS

        # 2-wire measurement (use SENSE_REMOTE for 4-wire)
        # smu.sense = smu.SENSE_LOCAL

        # clears SMU buffers
        smu.nvbuffer1.clear()
        smu.nvbuffer2.clear()

        smu.nvbuffer1.clearcache()
        smu.nvbuffer2.clearcache()

        # display current values during measurement
        self.display.smua.measure.func = self.display.MEASURE_DCAMPS
        self.display.smub.measure.func = self.display.MEASURE_DCAMPS

        # SETUP TRIGGER ARM AND COUNTS
        # trigger count = number of data points in measurement
        # arm count = number of times the measurement is repeated (set to 1)

        npts = len(smu_sweeplist)
        smu.trigger.count = npts

        # SET THE MEASUREMENT TRIGGER ON BOTH SMU'S
        # Set measurement to trigger once a change in the gate value on
        # sweep smu is complete, i.e., a measurement will occur
        # after the voltage is stepped.
        # Both channels should be set to trigger on the sweep smu event
        # so the measurements occur at the same time.

        # enable smu
        smu.trigger.measure.action = smu.ENABLE

        # measure current and voltage on trigger, store in buffer of smu
        smu.trigger.measure.iv(smu.nvbuffer1, smu.nvbuffer2)

        # initiate measure trigger when source is complete
        smu.trigger.measure.stimulus = smu.trigger.SOURCE_COMPLETE_EVENT_ID

        # SET THE ENDPULSE ACTION TO HOLD
        # Options are SOURCE_HOLD AND SOURCE_IDLE, hold maintains same voltage
        # throughout step in sweep (typical IV sweep behavior). idle will allow
        # pulsed IV sweeps.

        if pulsed:
            end_pulse_action = 0  # SOURCE_IDLE
        elif not pulsed:
            end_pulse_action = 1  # SOURCE_HOLD
        else:
            raise TypeError("'pulsed' must be of type 'bool'.")

        smu.trigger.endpulse.action = end_pulse_action

        # SET THE ENDSWEEP ACTION TO HOLD IF NOT PULSED
        # Output voltage will be held after sweep is done!

        smu.trigger.endsweep.action = end_pulse_action

        # SET THE EVENT TO TRIGGER THE SMU'S TO THE ARM LAYER
        # A typical measurement goes from idle -> arm -> trigger.
        # The 'trigger.event_id' option sets the transition arm -> trigger
        # to occur after sending *trg to the instrument.

        smu.trigger.arm.stimulus = self.trigger.EVENT_ID

        # Prepare an event blender (blender #1) that triggers when
        # the smua enters the trigger layer or reaches the end of a
        # single trigger layer cycle.

        # triggers when either of the stimuli are true ('or enable')
        self.trigger.blender[1].orenable = True
        self.trigger.blender[1].stimulus[1] = smu.trigger.ARMED_EVENT_ID
        self.trigger.blender[1].stimulus[2] = smu.trigger.PULSE_COMPLETE_EVENT_ID

        # SET THE smu SOURCE STIMULUS TO BE EVENT BLENDER #1
        # A source measure cycle within the trigger layer will occur when
        # either the trigger layer is entered (termed 'armed event') for the
        # first time or a single cycle of the trigger layer is complete (termed
        # 'pulse complete event').

        smu.trigger.source.stimulus = self.trigger.blender[1].EVENT_ID

        # PREPARE AN EVENT BLENDER (blender #2) THAT TRIGGERS WHEN BOTH SMU'S
        # HAVE COMPLETED A MEASUREMENT.
        # This is needed to prevent the next source measure cycle from occurring
        # before the measurement on both channels is complete.

        self.trigger.blender[2].orenable = True  # triggers when both stimuli are true
        self.trigger.blender[2].stimulus[1] = smu.trigger.MEASURE_COMPLETE_EVENT_ID

        # SET THE SMU ENDPULSE STIMULUS TO BE EVENT BLENDER #2
        smu.trigger.endpulse.stimulus = self.trigger.blender[2].EVENT_ID

        # TURN ON smu
        smu.source.output = smu.OUTPUT_ON

        # INITIATE MEASUREMENT
        # prepare SMUs to wait for trigger
        smu.trigger.initiate()

        # send trigger
        self._write('*trg')

        # CHECK STATUS BUFFER FOR MEASUREMENT TO FINISH
        # Possible return values:
        # 6 = smua and smub sweeping
        # 4 = only smub sweeping
        # 2 = only smua sweeping
        # 0 = neither smu sweeping

        # while loop that runs until the sweep begins
        while self.status.operation.sweeping.condition == 0:
            time.sleep(0.1)

        # while loop that runs until the sweep ends
        while self.status.operation.sweeping.condition > 0:
            time.sleep(0.1)

        # EXTRACT DATA FROM SMU BUFFERS
        i_smu = self.readBuffer(smu.nvbuffer1)
        v_smu = self.readBuffer(smu.nvbuffer2)

        smu.nvbuffer1.clear()
        smu.nvbuffer2.clear()

        smu.nvbuffer1.clearcache()
        smu.nvbuffer2.clearcache()

        self.busy = False

        return v_smu, i_smu

    def voltageSweepDualSMU(self, smu1, smu2, smu1_sweeplist, smu2_sweeplist, t_int,
                            delay, pulsed):
        """
        Sweeps voltages at two SMUs. Measures and returns current and voltage
        during sweep.

        :param smu1: 1st keithley smu instance to be swept.
        :param smu2: 2nd keithley smu instance to be swept.
        :param smu1_sweeplist: Voltages to sweep at ``smu1`` (can be a numpy
             array, list, tuple or range / xrange).
        :param smu2_sweeplist: Voltages to sweep at ``smu2`` (can be a numpy
             array, list, tuple, range / xrange).
        :param float t_int: Integration time per data point. Must be
            between 0.001 to 25 times the power line frequency (50Hz or 60Hz).
        :param float delay: Settling delay before each measurement. A value of
            -1 automatically starts a measurement once the current is stable.
        :param bool pulsed: Select pulsed or continuous sweep. In a pulsed
            sweep, the voltage is always reset to zero between data points.

        :returns: Lists of voltages and currents measured during the sweep (in
            Volt and Ampere, respectively): ``(v_smu1, i_smu1, v_smu2,
            i_smu2)``.
        :rtype: (list, list, list, list)
        """

        # input checks
        self._check_smu(smu1)
        self._check_smu(smu2)
        assert isinstance(smu1_sweeplist, self.TO_TSP_LIST)
        assert isinstance(smu2_sweeplist, self.TO_TSP_LIST)

        assert len(smu1_sweeplist) == len(smu2_sweeplist)

        # set state to busy
        self.busy = True
        # Define lists containing results.
        # If we abort early, we have something to return.
        v_smu1, i_smu1, v_smu2, i_smu2 = [], [], [], []

        if self.abort_event.is_set():
            self.busy = False
            return v_smu1, i_smu1, v_smu2, i_smu2

        # Setup smua/smub for sweep measurement.

        # setup smu1 and smu2 to sweep through lists on trigger
        # send sweep_list over in chunks if too long
        if len(smu1_sweeplist) > self.CHUNK_SIZE:
            self._write('mylist = {}')
            for num in smu1_sweeplist:
                self._write('table.insert(mylist, %s)' % num)
            smu1.trigger.source.listv('mylist')
        else:
            smu1.trigger.source.listv(smu1_sweeplist)

        if len(smu2_sweeplist) > self.CHUNK_SIZE:
            self._write('mylist = {}')
            for num in smu2_sweeplist:
                self._write('table.insert(mylist, %s)' % num)
            smu2.trigger.source.listv('mylist')
        else:
            smu2.trigger.source.listv(smu2_sweeplist)

        smu1.trigger.source.action = smu1.ENABLE
        smu2.trigger.source.action = smu2.ENABLE

        # CONFIGURE INTEGRATION TIME FOR EACH MEASUREMENT
        self.setIntegrationTime(smu1, t_int)
        self.setIntegrationTime(smu2, t_int)

        # CONFIGURE SETTLING TIME FOR GATE VOLTAGE, I-LIMIT, ETC...
        smu1.measure.delay = delay
        smu2.measure.delay = delay

        smu1.measure.autorangei = smu1.AUTORANGE_ON
        smu2.measure.autorangei = smu2.AUTORANGE_ON

        # smu1.trigger.source.limiti = 0.1
        # smu2.trigger.source.limiti = 0.1

        smu1.source.func = smu1.OUTPUT_DCVOLTS
        smu2.source.func = smu2.OUTPUT_DCVOLTS

        # 2-wire measurement (use SENSE_REMOTE for 4-wire)
        # smu1.sense = smu1.SENSE_LOCAL
        # smu2.sense = smu2.SENSE_LOCAL

        # CLEAR BUFFERS
        for smu in [smu1, smu2]:
            smu.nvbuffer1.clear()
            smu.nvbuffer2.clear()
            smu.nvbuffer1.clearcache()
            smu.nvbuffer2.clearcache()

        # display current values during measurement
        self.display.smua.measure.func = self.display.MEASURE_DCAMPS
        self.display.smub.measure.func = self.display.MEASURE_DCAMPS

        # SETUP TRIGGER ARM AND COUNTS
        # trigger count = number of data points in measurement
        # arm count = number of times the measurement is repeated (set to 1)

        npts = len(smu1_sweeplist)

        smu1.trigger.count = npts
        smu2.trigger.count = npts

        # SET THE MEASUREMENT TRIGGER ON BOTH SMU'S
        # Set measurement to trigger once a change in the gate value on
        # sweep smu is complete, i.e., a measurement will occur
        # after the voltage is stepped.
        # Both channels should be set to trigger on the sweep smu event
        # so the measurements occur at the same time.

        # enable smu
        smu1.trigger.measure.action = smu1.ENABLE
        smu2.trigger.measure.action = smu2.ENABLE

        # measure current and voltage on trigger, store in buffer of smu
        smu1.trigger.measure.iv(smu1.nvbuffer1, smu1.nvbuffer2)
        smu2.trigger.measure.iv(smu2.nvbuffer1, smu2.nvbuffer2)

        # initiate measure trigger when source is complete
        smu1.trigger.measure.stimulus = smu1.trigger.SOURCE_COMPLETE_EVENT_ID
        smu2.trigger.measure.stimulus = smu1.trigger.SOURCE_COMPLETE_EVENT_ID

        # SET THE ENDPULSE ACTION TO HOLD
        # Options are SOURCE_HOLD AND SOURCE_IDLE, hold maintains same voltage
        # throughout step in sweep (typical IV sweep behavior). idle will allow
        # pulsed IV sweeps.

        if pulsed:
            end_pulse_action = 0  # SOURCE_IDLE
        elif not pulsed:
            end_pulse_action = 1  # SOURCE_HOLD
        else:
            raise TypeError("'pulsed' must be of type 'bool'.")

        smu1.trigger.endpulse.action = end_pulse_action
        smu2.trigger.endpulse.action = end_pulse_action

        # SET THE ENDSWEEP ACTION TO HOLD IF NOT PULSED
        # Output voltage will be held after sweep is done!

        smu1.trigger.endsweep.action = end_pulse_action
        smu2.trigger.endsweep.action = end_pulse_action

        # SET THE EVENT TO TRIGGER THE SMU'S TO THE ARM LAYER
        # A typical measurement goes from idle -> arm -> trigger.
        # The 'trigger.event_id' option sets the transition arm -> trigger
        # to occur after sending *trg to the instrument.

        smu1.trigger.arm.stimulus = self.trigger.EVENT_ID

        # Prepare an event blender (blender #1) that triggers when
        # the smua enters the trigger layer or reaches the end of a
        # single trigger layer cycle.

        # triggers when either of the stimuli are true ('or enable')
        self.trigger.blender[1].orenable = True
        self.trigger.blender[1].stimulus[1] = smu1.trigger.ARMED_EVENT_ID
        self.trigger.blender[1].stimulus[2] = smu1.trigger.PULSE_COMPLETE_EVENT_ID

        # SET THE smu1 SOURCE STIMULUS TO BE EVENT BLENDER #1
        # A source measure cycle within the trigger layer will occur when
        # either the trigger layer is entered (termed 'armed event') for the
        # first time or a single cycle of the trigger layer is complete (termed
        # 'pulse complete event').

        smu1.trigger.source.stimulus = self.trigger.blender[1].EVENT_ID

        # PREPARE AN EVENT BLENDER (blender #2) THAT TRIGGERS WHEN BOTH SMU'S
        # HAVE COMPLETED A MEASUREMENT.
        # This is needed to prevent the next source measure cycle from occurring
        # before the measurement on both channels is complete.

        self.trigger.blender[2].orenable = False  # triggers when both stimuli are true
        self.trigger.blender[2].stimulus[1] = smu1.trigger.MEASURE_COMPLETE_EVENT_ID
        self.trigger.blender[2].stimulus[2] = smu2.trigger.MEASURE_COMPLETE_EVENT_ID

        # SET THE smu1 ENDPULSE STIMULUS TO BE EVENT BLENDER #2
        smu1.trigger.endpulse.stimulus = self.trigger.blender[2].EVENT_ID

        # TURN ON smu1 AND smu2
        smu1.source.output = smu1.OUTPUT_ON
        smu2.source.output = smu2.OUTPUT_ON

        # INITIATE MEASUREMENT
        # prepare SMUs to wait for trigger
        smu1.trigger.initiate()
        smu2.trigger.initiate()
        # send trigger
        self._write('*trg')

        # CHECK STATUS BUFFER FOR MEASUREMENT TO FINISH
        # Possible return values:
        # 6 = smua and smub sweeping
        # 4 = only smub sweeping
        # 2 = only smua sweeping
        # 0 = neither smu sweeping

        # while loop that runs until the sweep begins
        while self.status.operation.sweeping.condition == 0:
            time.sleep(0.1)

        # while loop that runs until the sweep ends
        while self.status.operation.sweeping.condition > 0:
            time.sleep(0.1)

        # EXTRACT DATA FROM SMU BUFFERS
        i_smu1 = self.readBuffer(smu1.nvbuffer1)
        v_smu1 = self.readBuffer(smu1.nvbuffer2)
        i_smu2 = self.readBuffer(smu2.nvbuffer1)
        v_smu2 = self.readBuffer(smu2.nvbuffer2)

        # CLEAR BUFFERS
        for smu in [smu1, smu2]:
            smu.nvbuffer1.clear()
            smu.nvbuffer2.clear()
            smu.nvbuffer1.clearcache()
            smu.nvbuffer2.clearcache()

        self.busy = False

        return v_smu1, i_smu1, v_smu2, i_smu2

# =============================================================================
# Define higher level control functions
# =============================================================================

    def transferMeasurement(self, smu_gate, smu_drain, vg_start, vg_stop,
                            vg_step, vd_list, t_int, delay, pulsed):
        """
        Records a transfer curve with forward and reverse sweeps and returns
        the results in a :class:`sweep_data.TransistorSweepData` instance.


        :param smu_gate: Keithley smu attached to gate electrode.
        :param smu_drain: Keithley smu attached to drain electrode.
        :param float vg_start: Start voltage of transfer sweep in Volt.
        :param float vg_stop: End voltage of transfer sweep in Volt.
        :param float vg_step: Voltage step size for transfer sweep in Volt.
        :param vd_list: List of drain voltage steps in Volt. Can be a numpy
             array, list, tuple, range / xrange.
        :param float t_int: Integration time per data point. Must be
            between 0.001 to 25 times the power line frequency (50Hz or 60Hz).
        :param float delay: Settling delay before each measurement. A value of
            -1 automatically starts a measurement once the current is stable.
        :param bool pulsed: Select pulsed or continuous sweep. In a pulsed
            sweep, the voltage is always reset to zero between data points.

        :returns: Transfer curve data.
        :rtype: :class:`sweep_data.TransistorSweepData`

        """
        self.busy = True
        self.abort_event.clear()

        msg = ('Recording transfer curve with Vg from %sV to %sV, Vd = %s V. '
               % (vg_start, vg_stop, vd_list))
        logger.info(msg)

        # create array with gate voltage steps, always include a step >= VgStop
        step = np.sign(vg_stop - vg_start) * abs(vg_step)
        sweeplist_gate_fwd = np.arange(vg_start, vg_stop + step, step)
        sweeplist_gate_rvs = np.flip(sweeplist_gate_fwd, 0)
        sweeplist_gate = np.append(sweeplist_gate_fwd, sweeplist_gate_rvs)

        # create ResultTable instance
        params = {
            'sweep_type': 'transfer',
            'time': time.time(),
            'time_str': time.strftime('%d/%m/%Y %H:%M'),
            't_int': t_int,
            'delay': delay,
            'pulsed': pulsed,
        }
        rt = FETResultTable(params=params)
        rt.append_column(sweeplist_gate, name='Gate voltage', unit='V')

        # record sweeps for every drain voltage step
        for vdrain in vd_list:

            # check for abort event
            if self.abort_event.is_set():
                self.reset()
                self.beeper.beep(0.3, 2400)
                return rt

            # create array with drain voltages
            if vdrain == 'trailing':
                sweeplist_drain = sweeplist_gate
            else:
                sweeplist_drain = np.full_like(sweeplist_gate, vdrain)

            # conduct sweep
            v_g, i_g, v_d, i_d = self.voltageSweepDualSMU(
                    smu_gate, smu_drain, sweeplist_gate, sweeplist_drain, t_int,
                    delay, pulsed
                    )

            if not self.abort_event.is_set():
                i_s = np.array(i_d) + np.array(i_g)
                rt.append_column(i_s, name='Source current (Vd = %s)' % vdrain, unit='A')
                rt.append_column(i_d, name='Drain current (Vd = %s)' % vdrain, unit='A')
                rt.append_column(i_g, name='Gate current (Vd = %s)' % vdrain, unit='A')

        self.reset()
        self.beeper.beep(0.3, 2400)

        self.busy = False
        return rt

    def outputMeasurement(self, smu_gate, smu_drain, vd_start, vd_stop, vd_step,
                          vg_list, t_int, delay, pulsed):
        """
        Records an output curve with forward and reverse sweeps and returns the
        results in a :class:`sweep_data.TransistorSweepData` instance.

        :param smu_gate: Keithley smu attached to gate electrode.
        :param smu_drain: Keithley smu attached to drain electrode.
        :param float vd_start: Start voltage of output sweep in Volt.
        :param float vd_stop: End voltage of output sweep in Volt.
        :param float vd_step: Voltage step size for output sweep in Volt.
        :param vg_list: List of gate voltage steps in Volt. Can be a numpy
             array, list, tuple, range / xrange.
        :param float t_int: Integration time per data point. Must be
            between 0.001 to 25 times the power line frequency (50Hz or 60Hz).
        :param float delay: Settling delay before each measurement. A value of
            -1 automatically starts a measurement once the current is stable.
        :param bool pulsed: Select pulsed or continuous sweep. In a pulsed
            sweep, the voltage is always reset to zero between data points.

        :returns: Output curve data.
        :rtype: :class:`sweep_data.TransistorSweepData`

        """

        self.busy = True
        self.abort_event.clear()
        msg = ('Recording output curve with Vd from %sV to %sV, Vg = %s V. '
               % (vd_start, vd_stop, vg_list))
        logger.info(msg)

        # create array with drain voltage steps, always include a step >= VgStop
        step = np.sign(vd_stop - vd_start) * abs(vd_step)
        sweeplist_drain_fwd = np.arange(vd_start, vd_stop + step, step)
        sweeplist_drain_rvs = np.flip(sweeplist_drain_fwd, 0)
        sweeplist_drain = np.append(sweeplist_drain_fwd, sweeplist_drain_rvs)

        # create ResultTable instance
        params = {
            'sweep_type': 'output',
            'time': time.time(),
            'time_str': time.strftime('%d/%m/%Y %H:%M'),
            't_int': t_int,
            'delay': delay,
            'pulsed': pulsed,
        }
        rt = FETResultTable(params=params)
        rt.append_column(sweeplist_drain, name='Drain voltage', unit='V')

        for vgate in vg_list:
            if self.abort_event.is_set():
                self.reset()
                self.beeper.beep(0.3, 2400)
                return rt

            # create array with gate voltages
            sweeplist_gate = np.full_like(sweeplist_drain, vgate)

            # conduct forward sweep
            v_d, i_d, v_g, i_g = self.voltageSweepDualSMU(
                    smu_drain, smu_gate, sweeplist_drain, sweeplist_gate, t_int,
                    delay, pulsed
                    )

            if not self.abort_event.is_set():
                i_s = np.array(i_d) + np.array(i_g)
                rt.append_column(i_s, name='Source current (Vg = %s)' % vgate, unit='A')
                rt.append_column(i_d, name='Drain current (Vg = %s)' % vgate, unit='A')
                rt.append_column(i_g, name='Gate current (Vg = %s)' % vgate, unit='A')

        self.reset()
        self.beeper.beep(0.3, 2400)

        self.busy = False
        return rt

    def playChord(self, notes=('C6', 'E6', 'G6'), durations=0.3):
        """Plays a chord on the Keithley.

        :param list notes: List of notes in scientific pitch notation, for
            instance ``['F4', 'Ab4', 'C4']`` for a f-minor chord in the 4th
            octave. Defaults to c-major in the 6th octave.
        :param durations: List of durations for each note in sec. If a single
            float is given, all notes will have the same duration. Defaults to
            0.3 sec.
        :type durations: float or list
        """

        freqs = [self._pitch_to_freq(p) for p in notes]
        if not isinstance(durations, list):
            durations = [durations]*len(freqs)

        for f, d in zip(freqs, durations):
            self.beeper.beep(d, f)

    @staticmethod
    def _pitch_to_freq(pitch):

        A4 = 440
        C4 = A4*2.0**(-9/12)

        names_sharp = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        names_flat = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]

        octave = list(filter(lambda x: x in '0123456789', pitch))
        octave = ''.join(octave)

        pitch = pitch.strip(octave)

        if octave == '':
            octave = 4
        else:
            octave = int(octave)

        try:
            steps = names_sharp.index(pitch)
        except ValueError:
            steps = names_flat.index(pitch)

        steps += 12*(octave-4)

        freq = C4*2.0**(steps/12)

        return freq


class Keithley2600Factory(object):

    _instances = {}
    SMU_LIST = Keithley2600.SMU_LIST

    def __new__(cls, *args, **kwargs):
        """
        Create new instance for a new visa_address, otherwise return existing
        instance.
        """
        if args[0] in cls._instances:
            logger.debug("Returning existing instance with address '%s'." % args[0])

            return cls._instances[args[0]]
        else:
            logger.debug("Creating new instance with address '%s'." % args[0])
            instance = Keithley2600(*args, **kwargs)
            cls._instances[args[0]] = instance

            return instance
