Source code for labdrivers.keithley.keithley2400

from statistics import mean, stdev

import visa


[docs]class Keithley2400: def __init__(self, gpib_addr=23): """ Constructor for Keithley 2400 Sourcemeter :param gpib_addr: GPIB address (configured on Keithley 2400) """ self._gpib_addr = str(gpib_addr) self._resource_manager = visa.ResourceManager() self._instrument = self._resource_manager.open_resource("GPIB::{}".format(self.gpib_addr)) @property def gpib_addr(self): """Returns the GPIB address of the Keithley 2400 Sourcemeter.""" return self._gpib_addr # source functions @property def source_type(self): """Gets or sets the source type of the Keithley 2400 SourceMeter. Expected strings for setting: 'voltage', 'current'""" response = self._instrument.query("source:function:mode?").strip() source_type = {'VOLT': 'voltage', 'CURR': 'current'} return source_type[response] @source_type.setter def source_type(self, value): if value.lower() == 'voltage' or value.lower() == 'v': source = 'voltage' self._instrument.write("source:function:mode {}".format(source.lower())) elif value.lower() == 'current' or value.lower() == 'i': source = 'current' self._instrument.write('source:function:mode {}'.format(source.lower())) else: raise RuntimeError('Not a valid source type.') @property def source_mode(self): """Gets or sets the mode of the source. Expected strings for setting: 'fixed', 'sweep', 'list'""" # TODO: test return self._instrument.query('source:' + self.source_type.lower() + ':mode?') @source_mode.setter def source_mode(self, mode): if mode.lower() in ('fixed', 'sweep', 'list'): self._instrument.write('source:' + self.source_type.lower() + ':mode {}'.format(mode)) else: raise RuntimeError('Mode is not one of [fixed | sweep | list]') @property def source_value(self): """Get or set the numeric value of the source chosen from Keithley2400.source_type.""" # TODO: test return self._instrument.query('source:' + self.source_type.lower() + ':level?') @source_value.setter def source_value(self, value): self._instrument.write("source:function:mode " + self.source_type.lower()) self._instrument.write("source:" + self.source_type.lower() + ":range " + str(value)) self._instrument.write("source:" + self.source_type.lower() + ":level " + str(value)) @property def measure_type(self): """The type of measurement the Keithley 2400 SourceMeter will make. Expected strings for setting: 'voltage', 'current', 'resistance' """ measure_type = {'VOLT:DC': 'voltage', 'CURR:DC': 'current', 'RES': 'resistance'} measure_type_response = self._instrument.query("sense:function?").strip().replace('\"', '').split(',')[-1] return measure_type[measure_type_response] @measure_type.setter def measure_type(self, value): measure_type = {'voltage': '\'VOLTAGE:DC\'', 'current': '\'CURRENT:DC\'', 'resistance': 'RESISTANCE'} if value.lower() in measure_type: self._instrument.write("sense:function:ON {}".format(measure_type[value.lower()])) else: raise RuntimeError('Expected a value from [\'voltage\'|\'current\'|\'resistance\'') # Resistance sensing @property def resistance_ohms_mode(self): """Gets or sets the resistance mode. Expected strings for setting: 'manual', 'auto'""" modes = {'MAN': 'manual', 'AUTO': 'auto'} response = self._instrument.query('sense:resistance:mode?').strip() return modes[response] @resistance_ohms_mode.setter def resistance_ohms_mode(self, value): modes = {'manual': 'MAN', 'auto': 'AUTO'} if value.lower() in modes.keys(): self._instrument.write('sense:resistance:mode {}'.format(modes[value.lower()])) else: raise RuntimeError('Expected a value from [\'manual\'|\'auto\']') @property def expected_ohms_reading(self): """Gets or sets the expected range of a resistance reading from the device under test.""" response = self._instrument.query('sense:resistance:range?').strip() return float(response) @expected_ohms_reading.setter def expected_ohms_reading(self, value): if isinstance(value, int) or isinstance(value, float): self._instrument.write('sense:resistance:range {}'.format(value)) else: raise RuntimeError('Expected an int or float.') @property def four_wire_sensing(self): """Gets the status of or sets four-wire sensing. Expected booleans for setting: True, False.""" response = self._instrument.query('system:rsense?').strip() return bool(int(response)) @four_wire_sensing.setter def four_wire_sensing(self, value): if isinstance(value, bool): self._instrument.write('system:rsense {}'.format(int(value))) else: raise RuntimeError('Expected boolean value.') # Voltage sensing and compliance @property def expected_voltage_reading(self): """Gets or sets the expected voltage reading from the device under test.""" response = self._instrument.query('sense:voltage:RANGE?').strip() return float(response) @expected_voltage_reading.setter def expected_voltage_reading(self, value): if isinstance(value, int) or isinstance(value, float): self._instrument.write('sense:voltage:range {}'.format(value)) else: raise RuntimeError('Expected an int or float.') @property def voltage_compliance(self): """Gets or sets the voltage compliance. Expected range of floats: 200e-6 <= x <= 210""" response = self._instrument.query("SENS:VOLT:PROT:LEV?").strip() return float(response) @voltage_compliance.setter def voltage_compliance(self, value): if 200e-6 <= value <= 210: self._instrument.write("SENS:VOLT:PROT {}".format(str(value))) else: raise RuntimeError('Voltage compliance cannot be set. Value must be between 200 \u03BC' + 'V and 210 V.')
[docs] def within_voltage_compliance(self): """Queries if the measured voltage is within the set compliance. :returns: boolean""" response = self._instrument.query('SENS:VOLT:PROT:TRIP?').strip() return not bool(int(response))
# Current sensing and compilance @property def expected_current_reading(self): """Gets or sets the expected current reading from the device under test.""" response = self._instrument.query('sense:current:range?').strip() return float(response) @expected_current_reading.setter def expected_current_reading(self, value): if isinstance(value, int) or isinstance(value, float): self._instrument.write('sense:current:range {}'.format(value)) else: RuntimeError('Expected an int or float.') @property def current_compliance(self): """Sets or gets the current compliance level in Amperes.""" response = self._instrument.query("SENS:CURR:PROT:LEV?").strip() return float(response) @current_compliance.setter def current_compliance(self, value): if 1e-9 <= value <= 1.05: self._instrument.write("SENS:CURR:PROT {}".format(str(value))) else: raise RuntimeError('Current compliance cannot be set. Value must be between 1 nA and 1.05 A.')
[docs] def within_current_compliance(self): """Queries if the measured current is within the set compliance. :returns: boolean""" response = self._instrument.query('SENS:CURR:PROT:TRIP?').strip() return not bool(int(response))
# Output configuration @property def output(self): """Gets or sets the source output of the Keithley 2400. Expected input: boolean :returns: boolean""" output = {'0': False, '1': True} response = self._instrument.query("OUTP?").strip() return output[response] @output.setter def output(self, value): if value: self._instrument.write("OUTP ON") else: self._instrument.write("OUTP OFF") @property def output_off_mode(self): """Gets or sets the output mode when the output is off. Expected input strings: 'himp', 'normal', 'zero', 'guard' :returns: description of the output's off mode""" modes = {'HIMP': 'high impedance', 'NORM': 'normal', 'ZERO': 'zero', 'GUAR': 'guard'} response = self._instrument.query('OUTP:SMOD?').strip() return modes[response] @output_off_mode.setter def output_off_mode(self, value): modes = {'high impedance': 'HIMP', 'himp': 'HIMP', 'normal': 'NORM', 'norm': 'NORM', 'zero': 'ZERO', '0': 'ZERO', 'guard': 'GUARD'} self._instrument.write('OUTP:SMOD {}'.format(modes[value.lower()])) # Data acquisition
[docs] def read(self, *measurements): """ Reads data from the Keithley 2400. Equivalent to the command :INIT; :FETCH? Multiple string arguments may be used. For example:: keithley.read('voltage', 'current') keithley.read('time') The first line returns a list in the form [voltage, current] and the second line returns a list in the form [time]. Note: The returned lists contains the values in the order that you requested. :param str *measurements: Any number of arguments that are from: 'voltage', 'current', 'resistance', 'time' :return list measure_list: A list of the arithmetic means in the order of the given arguments :return list measure_stdev_list: A list of the standard deviations (if more than 1 measurement) in the order of the given arguments """ response = self._instrument.query('read?').strip().split(',') response = [float(x) for x in response] read_types = {'voltage': 0, 'current': 1, 'resistance': 2, 'time': 3} measure_list = [] measure_stdev_list = [] for measurement in measurements: samples = response[read_types[measurement]::5] measure_list.append(mean(samples)) if len(samples) > 1: measure_stdev_list.append(stdev(samples)) return measure_list, measure_stdev_list
# Trigger functions @property def trace_delay(self): """The amount of time the SourceMeter waits after the trigger to perform Device Action.""" return float(self._instrument.query('trigger:delay?').strip()) @trace_delay.setter def trace_delay(self, delay): if isinstance(delay, float) or isinstance(delay, int): if 0.0 <= delay <= 999.9999: self._instrument.write('trigger:delay {}'.format(delay)) else: raise RuntimeError('Expected delay to be between 0.0 and 999.9999 seconds.') else: raise RuntimeError('Expected delay to be an int or float.') @property def trigger(self): """Gets or sets the type of trigger to be used. Expected strings for setting: 'immediate', 'tlink', 'timer', 'manual', 'bus', 'nst', 'pst', 'bst' (see source code for other possibilities)""" triggers = {'IMM': 'immediate', 'TLIN': 'trigger link', 'TIM': 'timer', 'MAN': 'manual', 'BUS': 'bus trigger', 'NST': 'low SOT pulse', 'PST': 'high SOT pulse', 'BST': 'high or low SOT pulse'} return triggers[self._instrument.query('trigger:source?')] @trigger.setter def trigger(self, trigger): triggers = { 'imm': 'IMM', 'immediate': 'IMM', 'tlin': 'TLIN', 'tlink': 'TLIN', 'trigger link': 'TLIN', 'tim': 'TIM', 'timer': 'TIM', 'man': 'MAN', 'manual': 'MAN', 'bus': 'BUS', 'bus trigger': 'BUS', 'nst': 'NST', 'low SOT pulse': 'NST', 'pst': 'PST', 'high SOT pulse': 'PST', 'bst': 'BST', 'high or low SOT pulse': 'BST' } if trigger.lower() in triggers.keys(): self._instrument.query('trigger:source {}'.format(trigger)) else: raise RuntimeError('Unexpected trigger input. See documentation for details.') @property def trigger_count(self): """Gets or sets the number of triggers Expected integer value range: 1 <= n <= 2500""" return float(self._instrument.query('trigger:count?').strip()) @trigger_count.setter def trigger_count(self, num_triggers): if isinstance(num_triggers, int): if 1 <= num_triggers <= 2500: self._instrument.write('trigger:count {}'.format(num_triggers)) else: raise RuntimeError('Trigger count expected to be between 1 and 2500.') else: raise RuntimeError('Trigger count expected to be type int.')
[docs] def initiate_cycle(self): """Initiates source or measure cycle, taking the SourceMeter out of an idle state.""" self._instrument.write('initiate')
[docs] def abort_cycle(self): """Aborts the source or measure cycle, bringing the SourceMeter back into an idle state.""" self._instrument.write('abort')
# Data storage / Buffer functions # Note: :trace:data? and :read? are two separate buffers of # maximum size 2500 readings. @property def num_readings_in_buffer(self): """Gets the number of readings that are stored in the buffer.""" return int(self._instrument.query('trace:points:actual?').strip()) @property def trace_points(self): """Gets or sets the size of the buffer Expected integer value range: 1 <= n <= 2500""" return int(self._instrument.query('trace:points?').strip()) @trace_points.setter def trace_points(self, num_points): if isinstance(num_points, int): if 1 <= num_points <= 2500: self._instrument.write('trace:points {}'.format(num_points)) else: raise RuntimeError('Keithley 2400 SourceMeter may only have 1 to 2500 buffer points.') else: raise RuntimeError('Expected type of num_points: int.')
[docs] def trace_feed_source(self, value): """Sets the source of the trace feed. Expected strings: 'sense', 'calculate1', 'calculate2'""" if value in ('sense', 'calculate1', 'calculate2'): self._instrument.write('trace:feed {}'.format(value)) else: raise RuntimeError('Unexpected trace source type. See documentation for details.')
[docs] def read_trace(self): """Read contents of buffer.""" trace = self._instrument.query('trace:data?').strip().split(',') trace_list = [float(x) for x in trace] return trace_list
[docs] def clear_trace(self): """Clear the buffer.""" self._instrument.query('trace:clear')
[docs] def buffer_memory_status(self): """Check buffer memory status.""" response = self._instrument.query('trace:free?') return response
[docs] def fill_buffer(self): """Fill buffer and stop.""" self._instrument.write('trace:feed:control next')
[docs] def disable_buffer(self): """Disables the buffer.""" self._instrument.write('trace:feed:control never')
# Sweeping # TODO: implement these!!! @property def sweep_start(self): """To be implemented.""" pass @sweep_start.setter def sweep_start(self, start): pass @property def sweep_end(self): """To be implemented.""" pass @sweep_end.setter def sweep_end(self, end): pass @property def sweep_center(self): """To be implemented.""" pass @sweep_center.setter def sweep_center(self, center): pass @property def sweep_span(self): """To be implemented.""" pass @sweep_span.setter def sweep_span(self, span): pass @property def sweep_ranging(self): """To be implemented.""" pass @sweep_ranging.setter def sweep_ranging(self, _range): pass @property def sweep_scale(self): """To be implemented.""" pass @sweep_scale.setter def sweep_scale(self, scale): pass @property def sweep_points(self): """To be implemented.""" pass @sweep_points.setter def sweep_points(self, num_points): pass @property def sweep_direction(self): """To be implemented.""" pass @sweep_direction.setter def sweep_direction(self, direction): pass # Ramping commands
[docs] def ramp_to_zero(self): pass
[docs] def ramp_to_setpoint(self, setpoint: float, step: float, wait: float): pass
# Common commands
[docs] def clear_status(self): """Clears all event registers and Error Queue.""" self._instrument.write('*cls')
[docs] def reset_to_defaults(self): """Resets to defaults of Sourcemeter.""" self._instrument.write('*rst')
[docs] def identify(self): """Returns manufacturer, model number, serial number, and firmware revision levels.""" response = self._instrument.write('*idn?') return {'manufacturer': response[0], 'model': response[1], 'serial number': response[2], 'firmware revision level': response[3] }
[docs] def send_bus_trigger(self): """Sends a bus trigger to SourceMeter.""" self._instrument.write('*trg')