diff --git a/src/instruments/glassman/glassmanfr.py b/src/instruments/glassman/glassmanfr.py index 5de38043..73da914a 100644 --- a/src/instruments/glassman/glassmanfr.py +++ b/src/instruments/glassman/glassmanfr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# hpe3631a.py: Driver for the Glassman FR Series Power Supplies +# glassmanfr.py: Driver for the Glassman FR Series Power Supplies # # © 2019 Francois Drielsma (francois.drielsma@gmail.com). # @@ -89,6 +89,7 @@ def __init__(self, filelike): self._device_timeout = False self._voltage = 0.0 * u.volt self._current = 0.0 * u.amp + self.device_timeout = False # ENUMS ## @@ -159,7 +160,9 @@ def voltage(self): @voltage.setter def voltage(self, newval): - self.set_status(voltage=assume_units(newval, u.volt)) + voltage = assume_units(newval, u.volt) + self.set_status(voltage=voltage) + self._voltage = voltage @property def current(self): @@ -173,7 +176,9 @@ def current(self): @current.setter def current(self, newval): - self.set_status(current=assume_units(newval, u.amp)) + current = assume_units(newval, u.amp) + self.set_status(current=current) + self._current = current @property def voltage_sense(self): diff --git a/src/instruments/keithley/__init__.py b/src/instruments/keithley/__init__.py index 56f8ef37..bc6cb2fb 100644 --- a/src/instruments/keithley/__init__.py +++ b/src/instruments/keithley/__init__.py @@ -9,4 +9,6 @@ from .keithley580 import Keithley580 from .keithley2182 import Keithley2182 from .keithley6220 import Keithley6220 +from .keithley6485 import Keithley6485 from .keithley6514 import Keithley6514 +from .keithley6517b import Keithley6517b diff --git a/src/instruments/keithley/keithley6485.py b/src/instruments/keithley/keithley6485.py new file mode 100644 index 00000000..ea43f4a5 --- /dev/null +++ b/src/instruments/keithley/keithley6485.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# +# keithley6485.py: Driver for the Keithley 6485 picoammeter +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Driver for the Keithley 6485 picoammeter. + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com). + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +from instruments.generic_scpi import SCPIInstrument +from instruments.units import ureg as u +from instruments.util_fns import bool_property + +# CLASSES ##################################################################### + + +class Keithley6485(SCPIInstrument): + """ + The `Keithley 6485` is an electrometer capable of doing sensitive current, + charge, voltage and resistance measurements. + + WARNING: Must set the terminator to `LF` on the define for this to work. + + Example usage: + + >>> import instruments as ik + >>> import instruments.units as u + >>> dmm = ik.keithley.Keithley6485.open_serial('/dev/ttyUSB0', baud=9600) + >>> dmm.measure() + + """ + + def __init__(self, filelike): + """ + Resets device to be read, disables zero check. + """ + super().__init__(filelike) + self.reset() + self.zero_check = False + + # PROPERTIES ## + + zero_check = bool_property( + "SYST:ZCH", + inst_true="ON", + inst_false="OFF", + doc=""" + Gets/sets the zero checking status of the Keithley 6485. + """, + ) + + zero_correct = bool_property( + "SYST:ZCOR", + inst_true="ON", + inst_false="OFF", + doc=""" + Gets/sets the zero correcting status of the Keithley 6485. + """, + ) + + @property + def auto_range(self): + """ + Gets/sets the auto range setting + + :type: `bool` + """ + # pylint: disable=no-member + out = self.query("RANG:AUTO?") + return out == "1" + + @auto_range.setter + def auto_range(self, newval): + # pylint: disable=no-member + self.sendcmd("RANG:AUTO {}".format("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 + out = self.query("RANG?") + return float(out) * u.amp + + @input_range.setter + def input_range(self, newval): + # pylint: disable=no-member + val = newval.to(u.amp).magnitude + if val not in self._valid_range(): + raise ValueError("Unexpected range limit for currently selected mode.") + self.sendcmd(f"RANG {val:e}") + + # METHODS ## + + 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, trigger_count) + """ + return self._parse_measurement(self.query("FETC?")) + + def read_measurements(self): + """ + Trigger and acquire readings using the current mode. + Returns a tuple of the form (reading, timestamp, trigger_count) + """ + return self._parse_measurement(self.query("READ?")) + + def measure(self): + """ + Trigger and acquire readings. + Returns the measurement reading only. + """ + return self.read_measurements()[0] + + # PRIVATE METHODS ## + + @staticmethod + def _valid_range(): + return (2e-9, 20e-9, 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + + @staticmethod + def _parse_measurement(ascii): + # Split the string in three comma-separated parts (value, time, number of triggers) + vals = ascii.split(",") + reading = float(vals[0][:-1]) * u.amp + timestamp = float(vals[1]) * u.second + trigger_count = int(float(vals[2])) + return reading, timestamp, trigger_count diff --git a/src/instruments/keithley/keithley6517b.py b/src/instruments/keithley/keithley6517b.py new file mode 100644 index 00000000..c8d2d21c --- /dev/null +++ b/src/instruments/keithley/keithley6517b.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# +# keithley6517b.py: Driver for the Keithley 6517b Electrometer +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Provides support for the Keithley 6517b electrometer. + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com). + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# 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 ##################################################################### + + +class Keithley6517b(SCPIInstrument, Electrometer): + """ + The `Keithley 6517b` 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.Keithley6517b.open_serial('/dev/ttyUSB0', baud=115200) + >>> dmm.measure(dmm.Mode.current) + + """ + + def __init__(self, filelike): + """ + Auto configs the instrument in to the current readout mode + (sets the trigger and communication types rights) + """ + super().__init__(filelike) + self.auto_config(self.mode) + + # ENUMS ## + + class Mode(Enum): + """ + Enum containing valid measurement modes for the Keithley 6517b + """ + + voltage_dc = "VOLT:DC" + current_dc = "CURR:DC" + resistance = "RES" + charge = "CHAR" + + class TriggerMode(Enum): + """ + Enum containing valid trigger modes for the Keithley 6517b + """ + + immediate = "IMM" + tlink = "TLINK" + + class ArmSource(Enum): + """ + Enum containing valid trigger arming sources for the Keithley 6517b + """ + + immediate = "IMM" + timer = "TIM" + bus = "BUS" + tlink = "TLIN" + stest = "STES" + pstest = "PST" + nstest = "NST" + manual = "MAN" + + class ValidRange(Enum): + """ + Enum containing valid measurement ranges for the Keithley 6517b + """ + + voltage_dc = (2, 20, 200) + current_dc = ( + 20e-12, + 200e-12, + 2e-9, + 20e-9, + 200e-9, + 2e-6, + 20e-6, + 200e-6, + 2e-3, + 20e-3, + ) + resistance = (2e6, 20e6, 200e6, 2e9, 20e9, 200e9, 2e12, 20e12, 200e12) + charge = (2e-9, 20e-9, 200e-9, 2e-6) + + # PROPERTIES ## + + 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 6517b. + """, + ) + + trigger_mode = enum_property( + "TRIGGER:SOURCE", + TriggerMode, + doc=""" + Gets/sets the trigger mode of the Keithley 6517b. + """, + ) + + arm_source = enum_property( + "ARM:SOURCE", + ArmSource, + doc=""" + Gets/sets the arm source of the Keithley 6517b. + """, + ) + + zero_check = bool_property( + "SYST:ZCH", + inst_true="ON", + inst_false="OFF", + doc=""" + Gets/sets the zero checking status of the Keithley 6517b. + """, + ) + + zero_correct = bool_property( + "SYST:ZCOR", + inst_true="ON", + inst_false="OFF", + doc=""" + Gets/sets the zero correcting status of the Keithley 6517b. + """, + ) + + @property + def unit(self): + return 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 out == "1" + + @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) * UNITS[mode] + + @input_range.setter + def input_range(self, newval): + # pylint: disable=no-member + mode = self.mode + val = newval.to(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 ## + + 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}") + + 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, trigger_count) + """ + return self._parse_measurement(self.query("FETC?")) + + def read_measurements(self): + """ + Trigger and acquire readings using the current mode. + Returns a tuple of the form (reading, timestamp, trigger_count) + """ + return self._parse_measurement(self.query("READ?")) + + def measure(self, mode=None): + """ + Trigger and acquire readings using the requested mode. + Returns the measurement reading only. + """ + # Check the current mode, change if necessary + if mode is not None: + if mode != self.mode: + self.auto_config(mode) + + return self.read_measurements()[0] + + # PRIVATE METHODS ## + + def _valid_range(self, mode): + if mode == self.Mode.voltage_dc: + return self.ValidRange.voltage_dc + if mode == self.Mode.current_dc: + return self.ValidRange.current_dc + if mode == self.Mode.resistance: + return self.ValidRange.resistance + if mode == self.Mode.charge: + return self.ValidRange.charge + + raise ValueError("Invalid mode.") + + def _parse_measurement(self, ascii): + # Split the string in three comma-separated parts (value, time, number of triggers) + vals = ascii.split(",") + reading = float(vals[0].split("N")[0]) * self.unit + timestamp = float(vals[1].split("s")[0]) * u.second + trigger_count = int(vals[2][:-5].split("R")[0]) + return reading, timestamp, trigger_count + + +# UNITS ####################################################################### + +UNITS = { + Keithley6517b.Mode.voltage_dc: u.volt, + Keithley6517b.Mode.current_dc: u.amp, + Keithley6517b.Mode.resistance: u.ohm, + Keithley6517b.Mode.charge: u.coulomb, +} diff --git a/tests/test_keithley/test_keithley6485.py b/tests/test_keithley/test_keithley6485.py new file mode 100644 index 00000000..03121247 --- /dev/null +++ b/tests/test_keithley/test_keithley6485.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +""" +Unit tests for the Keithley 6485 picoammeter +""" + +# IMPORTS ##################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol +from instruments.units import ureg as u + +# TESTS ####################################################################### + +# pylint: disable=protected-access + + +init_sequence = ["*RST", "SYST:ZCH OFF"] + + +def test_parse_measurement(): + with expected_protocol(ik.keithley.Keithley6485, init_sequence, []) as inst: + reading, timestamp, trigger_count = inst._parse_measurement("1.234E-3A,567,89") + assert reading == 1.234 * u.milliamp + assert timestamp == 567 * u.second + assert trigger_count == 89 + + +def test_zero_check(): + with expected_protocol( + ik.keithley.Keithley6485, init_sequence + ["SYST:ZCH?", "SYST:ZCH ON"], ["OFF"] + ) as inst: + assert inst.zero_check is False + inst.zero_check = True + + +def test_zero_correct(): + with expected_protocol( + ik.keithley.Keithley6485, + init_sequence + ["SYST:ZCOR?", "SYST:ZCOR ON"], + ["OFF"], + ) as inst: + assert inst.zero_correct is False + inst.zero_correct = True + + +def test_auto_range(): + with expected_protocol( + ik.keithley.Keithley6485, init_sequence + ["RANG:AUTO?", "RANG:AUTO 1"], ["0"] + ) as inst: + assert inst.auto_range is False + inst.auto_range = True + + +def test_input_range(): + with expected_protocol( + ik.keithley.Keithley6485, + init_sequence + ["RANG?", f"RANG {2e-3:e}"], + ["0.002"], + ) as inst: + assert inst.input_range == 2 * u.milliamp + inst.input_range = 2 * u.milliamp + + +def test_input_range_invalid(): + with pytest.raises(ValueError): + with expected_protocol( + ik.keithley.Keithley6485, init_sequence + [f"RANG {10:e}"], [] + ) as inst: + inst.input_range = 10 * u.amp + + +def test_fetch(): + with expected_protocol( + ik.keithley.Keithley6485, init_sequence + ["FETC?"], ["1.234E-3A,567,89"] + ) as inst: + reading, timestamp, trigger_count = inst.fetch() + assert reading == 1.234 * u.milliamp + assert timestamp == 567 * u.second + assert trigger_count == 89 + + +def test_read(): + with expected_protocol( + ik.keithley.Keithley6485, init_sequence + ["READ?"], ["1.234E-3A,567,89"] + ) as inst: + reading, timestamp, trigger_count = inst.read_measurements() + assert reading == 1.234 * u.milliamp + assert timestamp == 567 * u.second + assert trigger_count == 89 diff --git a/tests/test_keithley/test_keithley6517b.py b/tests/test_keithley/test_keithley6517b.py new file mode 100644 index 00000000..e75aad96 --- /dev/null +++ b/tests/test_keithley/test_keithley6517b.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +""" +Unit tests for the Keithley 6517b electrometer +""" + +# IMPORTS ##################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol +from instruments.units import ureg as u + +# TESTS ####################################################################### + +# pylint: disable=protected-access + + +init_sequence = ["FUNCTION?", "CONF:VOLT:DC"] +init_response = ['"VOLT:DC"'] + + +def test_parse_measurement(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["FUNCTION?"], + init_response + ['"VOLT:DC"'], + ) as inst: + reading, timestamp, trigger_count = inst._parse_measurement( + "1.0N,1234s,5678R00000" + ) + assert reading == 1.0 * u.volt + assert timestamp == 1234 * u.second + assert trigger_count == 5678 + + +def test_mode(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["FUNCTION?", 'FUNCTION "VOLT:DC"'], + init_response + ['"VOLT:DC"'], + ) as inst: + assert inst.mode == inst.Mode.voltage_dc + inst.mode = inst.Mode.voltage_dc + + +def test_trigger_source(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["TRIGGER:SOURCE?", "TRIGGER:SOURCE IMM"], + init_response + ["TLINK"], + ) as inst: + assert inst.trigger_mode == inst.TriggerMode.tlink + inst.trigger_mode = inst.TriggerMode.immediate + + +def test_arm_source(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["ARM:SOURCE?", "ARM:SOURCE IMM"], + init_response + ["TIM"], + ) as inst: + assert inst.arm_source == inst.ArmSource.timer + inst.arm_source = inst.ArmSource.immediate + + +def test_zero_check(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["SYST:ZCH?", "SYST:ZCH ON"], + init_response + ["OFF"], + ) as inst: + assert inst.zero_check is False + inst.zero_check = True + + +def test_zero_correct(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["SYST:ZCOR?", "SYST:ZCOR ON"], + init_response + ["OFF"], + ) as inst: + assert inst.zero_correct is False + inst.zero_correct = True + + +def test_unit(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + + [ + "FUNCTION?", + ], + init_response + ['"VOLT:DC"'], + ) as inst: + assert inst.unit == u.volt + + +def test_auto_range(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + + ["FUNCTION?", "VOLT:DC:RANGE:AUTO?", "FUNCTION?", "VOLT:DC:RANGE:AUTO 1"], + init_response + ['"VOLT:DC"', "0", '"VOLT:DC"'], + ) as inst: + assert inst.auto_range is False + inst.auto_range = True + + +def test_input_range(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + + [ + "FUNCTION?", + "VOLT:DC:RANGE:UPPER?", + "FUNCTION?", + f"VOLT:DC:RANGE:UPPER {20:e}", + ], + init_response + ['"VOLT:DC"', "10", '"VOLT:DC"'], + ) as inst: + assert inst.input_range == 10 * u.volt + inst.input_range = 20 * u.volt + + +def test_input_range_invalid(): + with pytest.raises(ValueError): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence, + init_response, + ) as inst: + inst.input_range = 10 * u.volt + + +def test_auto_config(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["CONF:VOLT:DC"], + init_response + ['"VOLT:DC"'], + ) as inst: + inst.auto_config(inst.Mode.voltage_dc) + + +def test_fetch(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + + [ + "FETC?", + "FUNCTION?", + ], + init_response + ["1.0N,1234s,5678R00000", '"VOLT:DC"'], + ) as inst: + reading, timestamp, trigger_count = inst.fetch() + assert reading == 1.0 * u.volt + assert timestamp == 1234 * u.second + assert trigger_count == 5678 + + +def test_read(): + with expected_protocol( + ik.keithley.Keithley6517b, + init_sequence + ["READ?", "FUNCTION?"], + init_response + ["1.0N,1234s,5678R00000", '"VOLT:DC"'], + ) as inst: + reading, timestamp, trigger_count = inst.read_measurements() + assert reading == 1.0 * u.volt + assert timestamp == 1234 * u.second + assert trigger_count == 5678