# -*- 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.4.0'
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))
sub_cmds += super().__dir__()
return sub_cmds
[docs]class KeithleyIOError(Exception):
"""Raised when no Keithley instrument is connected."""
pass
[docs]class KeithleyError(Exception):
"""Raised for error messages from the Keithley itself."""
pass
[docs]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: Path to visa library. Defaults to "@py" for pyvisa-py
but another IVI library may be appropriate (NI-VISA, Keysight VISA, R&S VISA,
tekVISA etc.). If an empty string is given, an IVI library will be used if
installed and pyvisa-py otherwise.
: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
# =============================================================================
[docs] 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
[docs] 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
[docs]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: Path to visa library. Defaults to "@py" for pyvisa-py
but another IVI library may be appropriate (NI-VISA, Keysight VISA, R&S VISA,
tekVISA etc.). If an empty string is given, an IVI library will be used if
installed and pyvisa-py otherwise.
: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
# =============================================================================
[docs] 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
[docs] @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
[docs] 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
[docs] 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
[docs] 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
[docs] 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()
[docs] 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()
[docs] 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)
[docs] 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
[docs] 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
# =============================================================================
[docs] 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
[docs] 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
[docs] 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