#!/usr/bin/env python
"""
Provides support for the Keithley 6514 electrometer
"""
# IMPORTS #####################################################################
from enum import Enum
from instruments.abstract_instruments import Electrometer
from instruments.generic_scpi import SCPIInstrument
from instruments.units import ureg as u
from instruments.util_fns import bool_property, enum_property
# CLASSES #####################################################################
[docs]
class Keithley6514(SCPIInstrument, Electrometer):
"""
The `Keithley 6514`_ is an electrometer capable of doing sensitive current,
charge, voltage and resistance measurements.
Example usage:
>>> import instruments as ik
>>> import instruments.units as u
>>> dmm = ik.keithley.Keithley6514.open_gpibusb('/dev/ttyUSB0', 12)
"""
# ENUMS #
[docs]
class Mode(Enum):
"""
Enum containing valid measurement modes for the Keithley 6514
"""
voltage = "VOLT:DC"
current = "CURR:DC"
resistance = "RES"
charge = "CHAR"
[docs]
class TriggerMode(Enum):
"""
Enum containing valid trigger modes for the Keithley 6514
"""
immediate = "IMM"
tlink = "TLINK"
[docs]
class ArmSource(Enum):
"""
Enum containing valid trigger arming sources for the Keithley 6514
"""
immediate = "IMM"
timer = "TIM"
bus = "BUS"
tlink = "TLIN"
stest = "STES"
pstest = "PST"
nstest = "NST"
manual = "MAN"
[docs]
class ValidRange(Enum):
"""
Enum containing valid measurement ranges for the Keithley 6514
"""
voltage = (2, 20, 200)
current = (
20e-12,
200e-12,
2e-9,
20e-9,
200e-9,
2e-6,
20e-6,
200e-6,
2e-3,
20e-3,
)
resistance = (2e3, 20e3, 200e3, 2e6, 20e6, 200e6, 2e9, 20e9, 200e9)
charge = (20e-9, 200e-9, 2e-6, 20e-6)
# CONSTANTS #
_MODE_UNITS = {
Mode.voltage: u.volt,
Mode.current: u.amp,
Mode.resistance: u.ohm,
Mode.charge: u.coulomb,
}
# PRIVATE METHODS #
def _valid_range(self, mode):
if mode == self.Mode.voltage:
return self.ValidRange.voltage
elif mode == self.Mode.current:
return self.ValidRange.current
elif mode == self.Mode.resistance:
return self.ValidRange.resistance
elif mode == self.Mode.charge:
return self.ValidRange.charge
else:
raise ValueError("Invalid mode.")
def _parse_measurement(self, ascii):
# TODO: don't assume ASCII data format # pylint: disable=fixme
vals = list(map(float, ascii.split(",")))
reading = vals[0] * self.unit
timestamp = vals[1]
status = vals[2]
return reading, timestamp, status
# PROPERTIES #
# The mode values have quotes around them for some annoying reason.
mode = enum_property(
"FUNCTION",
Mode,
input_decoration=lambda val: val[1:-1],
# output_decoration=lambda val: '"{}"'.format(val),
set_fmt='{} "{}"',
doc="""
Gets/sets the measurement mode of the Keithley 6514.
""",
)
trigger_mode = enum_property(
"TRIGGER:SOURCE",
TriggerMode,
doc="""
Gets/sets the trigger mode of the Keithley 6514.
""",
)
arm_source = enum_property(
"ARM:SOURCE",
ArmSource,
doc="""
Gets/sets the arm source of the Keithley 6514.
""",
)
zero_check = bool_property(
"SYST:ZCH",
inst_true="ON",
inst_false="OFF",
doc="""
Gets/sets the zero checking status of the Keithley 6514.
""",
)
zero_correct = bool_property(
"SYST:ZCOR",
inst_true="ON",
inst_false="OFF",
doc="""
Gets/sets the zero correcting status of the Keithley 6514.
""",
)
@property
def unit(self):
return self._MODE_UNITS[self.mode]
@property
def auto_range(self):
"""
Gets/sets the auto range setting
:type: `bool`
"""
# pylint: disable=no-member
out = self.query(f"{self.mode.value}:RANGE:AUTO?")
return True if out == "1" else False
@auto_range.setter
def auto_range(self, newval):
# pylint: disable=no-member
self.sendcmd("{}:RANGE:AUTO {}".format(self.mode.value, "1" if newval else "0"))
@property
def input_range(self):
"""
Gets/sets the upper limit of the current range.
:type: `~pint.Quantity`
"""
# pylint: disable=no-member
mode = self.mode
out = self.query(f"{mode.value}:RANGE:UPPER?")
return float(out) * self._MODE_UNITS[mode]
@input_range.setter
def input_range(self, newval):
# pylint: disable=no-member
mode = self.mode
val = newval.to(self._MODE_UNITS[mode]).magnitude
if val not in self._valid_range(mode).value:
raise ValueError("Unexpected range limit for currently selected mode.")
self.sendcmd(f"{mode.value}:RANGE:UPPER {val:e}")
# METHODS ##
[docs]
def auto_config(self, mode):
"""
This command causes the device to do the following:
- Switch to the specified mode
- Reset all related controls to default values
- Set trigger and arm to the 'immediate' setting
- Set arm and trigger counts to 1
- Set trigger delays to 0
- Place unit in idle state
- Disable all math calculations
- Disable buffer operation
- Enable autozero
"""
self.sendcmd(f"CONF:{mode.value}")
[docs]
def fetch(self):
"""
Request the latest post-processed readings using the current mode.
(So does not issue a trigger)
Returns a tuple of the form (reading, timestamp)
"""
raw = self.query("FETC?")
reading, timestamp, _ = self._parse_measurement(raw)
return reading, timestamp
[docs]
def read_measurements(self):
"""
Trigger and acquire readings using the current mode.
Returns a tuple of the form (reading, timestamp)
"""
raw = self.query("READ?")
reading, timestamp, _ = self._parse_measurement(raw)
return reading, timestamp