From 08190430d1a878a3833b6259495f6a1b0c86a77e Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Fri, 20 Jan 2023 13:29:50 +0100 Subject: [PATCH 001/181] Kuhne Electronic KU SG 2.45 250 A instrument implementation added. --- docs/api/instruments/index.rst | 1 + .../api/instruments/kuhneelectronic/index.rst | 12 + .../kuhneelectronic/kusg245_250a.rst | 7 + pymeasure/instruments/__init__.py | 1 + .../instruments/kuhneelectronic/__init__.py | 25 ++ .../kuhneelectronic/kusg245_250a.py | 305 ++++++++++++++++++ .../kuhneelectronic/test_kusg245_250a.py | 273 ++++++++++++++++ 7 files changed, 624 insertions(+) create mode 100644 docs/api/instruments/kuhneelectronic/index.rst create mode 100644 docs/api/instruments/kuhneelectronic/kusg245_250a.rst create mode 100644 pymeasure/instruments/kuhneelectronic/__init__.py create mode 100644 pymeasure/instruments/kuhneelectronic/kusg245_250a.py create mode 100644 tests/instruments/kuhneelectronic/test_kusg245_250a.py diff --git a/docs/api/instruments/index.rst b/docs/api/instruments/index.rst index f1504a69ff..171f96d9c5 100644 --- a/docs/api/instruments/index.rst +++ b/docs/api/instruments/index.rst @@ -41,6 +41,7 @@ Instruments by manufacturer: hp/index keithley/index keysight/index + kuhneelectronic/index lakeshore/index lecroy/index mksinst/index diff --git a/docs/api/instruments/kuhneelectronic/index.rst b/docs/api/instruments/kuhneelectronic/index.rst new file mode 100644 index 0000000000..e193664727 --- /dev/null +++ b/docs/api/instruments/kuhneelectronic/index.rst @@ -0,0 +1,12 @@ +.. module:: pymeasure.instruments.kuhneelectronic + +################ +Kuhne Electronic +################ + +This section contains specific documentation on the Kuhne Electronic instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. + +.. toctree:: + :maxdepth: 2 + + kusg245_250a diff --git a/docs/api/instruments/kuhneelectronic/kusg245_250a.rst b/docs/api/instruments/kuhneelectronic/kusg245_250a.rst new file mode 100644 index 0000000000..61abd5cf2a --- /dev/null +++ b/docs/api/instruments/kuhneelectronic/kusg245_250a.rst @@ -0,0 +1,7 @@ +######################################################## +KU SG 2.45 250 A - 2.45 GHz ISM-Band Microwave Generator +######################################################## + +.. autoclass:: pymeasure.instruments.kuhneelectronic.Kusg245_250A + :members: + :show-inheritance: \ No newline at end of file diff --git a/pymeasure/instruments/__init__.py b/pymeasure/instruments/__init__.py index abad78f104..d56757f06f 100644 --- a/pymeasure/instruments/__init__.py +++ b/pymeasure/instruments/__init__.py @@ -50,6 +50,7 @@ from . import hp from . import keithley from . import keysight +from . import kuhneelectronic from . import lakeshore from . import lecroy from . import mksinst diff --git a/pymeasure/instruments/kuhneelectronic/__init__.py b/pymeasure/instruments/kuhneelectronic/__init__.py new file mode 100644 index 0000000000..9e66811ca7 --- /dev/null +++ b/pymeasure/instruments/kuhneelectronic/__init__.py @@ -0,0 +1,25 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from .kusg245_250a import Kusg245_250A diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py new file mode 100644 index 0000000000..5749e51ed0 --- /dev/null +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -0,0 +1,305 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import time + +from pymeasure.instruments import Instrument +from pymeasure.instruments.validators import strict_discrete_set +from pymeasure.instruments.validators import truncated_range, truncated_discrete_set + + +class Kusg245_250A(Instrument): + """Represents KU SG 2.45 250 A the 2.45 GHz ISM-Band Microwave Generator + and provides a high-level interface for interacting with the instrument. + + :param power_limit: power set-point limit in Watts. + See :attr:`~.Kusg245_250A.power_setpoint` + and :meth:`~.Kusg245_250A.tune()`. + + Usage example: + + .. code-block:: python + + from pymeasure.instruments.kuhneelectronic import Kusg245_250A + + generator = Kusg245_250A("ASRL3::INSTR", power_limit=100) # limits the output + # power set-point to 100 W + + generator.external_enabled = False # biasing and RF output controlled by serial comm + generator.power = 20 # Sets the output power to 20 Watts + generator.bias_enabled = True # Enables amplifier biasing + generator.rf_enabled = True # Enables the RF output + + p_fwd = generator.power_forward # Reads forward power in Watts + p_rev = generator.power_reverse # Reads reflected power in Watts + """ + + def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): + kwargs.setdefault("baud_rate", 115200) + kwargs.setdefault("read_termination", "\r") + kwargs.setdefault("write_termination", "\r") + + super().__init__(adapter, name, **kwargs) + + assert 0 < power_limit <= 250 + self._power_limit = power_limit + self.power_setpoint_values = [0, power_limit] + + version = Instrument.measurement("v", """Readout of the firmware version.""") + + voltage_5v = Instrument.measurement( + "5", + """Readout of internal 5V supply voltage in Volts.""", + get_process=lambda v: 103.0 / 4700.0 * v, + ) + + voltage_32v = Instrument.measurement( + "8", + """Readout of 32V supply voltage in Volts.""", + get_process=lambda v: 1282.0 / 8200.0 * v, + ) + + power_forward = Instrument.measurement("6", """Readout of forward power in Watts.""") + + power_reverse = Instrument.measurement("7", """Readout of reverse power in Watts.""") + + temperature = Instrument.measurement( + "T", """Readout of temperature sensor near the final transistor in °C.""" + ) + + external_enabled = Instrument.control( + "r?", + "%s", + """Control the whether the amplifier enabling is done + via external inputs on 8-pin connector + or via the serial interface (boolean). + """, + validator=strict_discrete_set, + values=[False, True], + set_process=lambda v: {True: "R", False: "r"}[v], + get_process=lambda v: bool(v) + ) + + bias_enabled = Instrument.control( + "x?", + "%s", + """Transistor biasing (boolean). + + Biasing must be enabled before switching RF on + (see :attr:`~.Kusg245_250A.rf_enabled`). + """, + validator=strict_discrete_set, + values=[False, True], + set_process=lambda v: {True: "X", False: "x"}[v], + get_process=lambda v: bool(v) + ) + + rf_enabled = Instrument.control( + "o?", + "%s", + """Enable RF output (boolean). + + .. note:: + + Bias must be enabled before RF is enabled + (see :attr:`~.Kusg245_250A.bias_enabled`) + """, + validator=strict_discrete_set, + values=[False, True], + set_process=lambda v: {True: "O", False: "o"}[v], + get_process=lambda v: bool(v) + ) + + pulse_mode_enabled = Instrument.control( + "p?", + "%s", + """Enable pulse mode (boolean).""", + validator=strict_discrete_set, + values=[False, True], + set_process=lambda v: {True: "P", False: "p"}[v], + get_process=lambda v: bool(v) + ) + + freq_steps_fine_enabled = Instrument.control( + "fm?", + "fm%d", + """Enables fine frequency steps (boolean).""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True, + ) + + frequency_coarse = Instrument.control( + "f?", + "f%04d", + """Coarse frequency in MHz (integer from 2400 to 2500). + + Fine frequency mode must be disabled + (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`). + Resolution: 1 MHz. Invalid values are truncated. + """, + validator=truncated_range, + values=[2400, 2500], + ) + + frequency_fine = Instrument.control( + "f?", + "f%07d", + """Fine frequency in kHz (integer from 2400000 to 2500000). + + Fine frequency mode must be enabled + (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`). + Resolution: 10 kHz. Invalid values are truncated. + Values are rounded to tens. + """, + validator=truncated_range, + values=[2400000, 2500000], + set_process=lambda v: round(v, -1), + ) + + power_setpoint = Instrument.control( + "A?", + "A%03d", + """Output power set-point in Watts (integer from 0 to :attr:`power_limit` + parameter - see constructor). + + Resolution: 1 W. Invalid values are truncated. + """, + validator=truncated_range, + values=[0, 250], + dynamic=True, + ) + + pulse_width = Instrument.control( + "C?", + "C%04d", + """Pulse width in ms (integer from 10 to 1000). + + Resolution: 5 ms. Invalid values are truncated. + Values are rounded to multipliers of 5. + """, + validator=truncated_range, + values=[10, 1000], + set_process=lambda v: round(2 * v, -1) / 2, + ) + + off_time = Instrument.control( + "c?", + "c%04d", + """Off time for the pulse mode in ms (integer from 10 to 1000). + + Resolution: 5 ms. Invalid values are truncated. + Values are rounded to multipliers of 5. + """, + validator=truncated_range, + values=[10, 1000], + set_process=lambda v: round(2 * v, -1) / 2, + ) + + phase_shift = Instrument.control( + "H?", + "H%03d", + """Phase shift in degrees (float from 0 to 358.6). + + Resolution: 8-bits. Values out of range are truncated. + """, + validator=truncated_range, + values=[0, 358.6], + set_process=lambda v: round(v / 360 * 256), + get_process=lambda v: v / 256 * 360, + ) + + reflection_limit = Instrument.control( + "B?", + "B%d", + """Limit of reflection in Watts (integer in 0 - no limit, 100, 150, 180, 200, 230). + + .. note:: + + If the limit for the reflected power is reached, the forward power + is reduced to the specified value and the power control mechanism + is locked until the alarm has been cleared by the user via + :meth:`~.Kusg245_250A.clear_VSWR_error()`. + """, + validator=truncated_discrete_set, + values={0: 0, 100: 1, 150: 2, 180: 3, 200: 4, 230: 5}, + map_values=True, + ) + + def tune(self, power): + """ + Find and set the frequency with lowest reflection + at a given power. + + :param power: A power set-point for tuning (in Watts). + (integer from 0 to :attr:`power_limit` + parameter - see constructor). + """ + power = truncated_range(power, [0, self._power_limit]) + self.write(f"b{power:03d}") + + def clear_VSWR_error(self): + """ + Clears the VSWR error. + + See: :attr:`~.Kusg245_250A.reflection_limit`. + """ + self.write("z") + + def store_settings(self): + """ + Save actual settings to EEPROM. + + The following parameters are saved: + frequency mode (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`), + frequency (see :attr:`~.Kusg245_250A.frequency_coarse` + or :attr:`~.Kusg245_250A.frequency_fine`), + output power set-point (see :attr:`~.Kusg245_250A.power_setpoint`), + ON/OFF control setting (see :attr:`~.Kusg245_250A.external_enabled`), + reflection limit (see :attr:`~.Kusg245_250A.reflection_limit`), + on time for pulse mode (see :attr:`~.Kusg245_250A.pulse_width`) and + off time for pulse mode (see :attr:`~.Kusg245_250A.off_time`). + """ + self.write("SE") + + def shutdown(self): + """ + Safe shut-down the generator. + + 1. Disable RF output. + 2. Deactivate biasing. + """ + self.rf_enabled = False + self.bias_enabled = False + + def turn_on(self): + """ + Safe turn-on the generator. + + 1. Activate biasing. + 2. Enable RF output. + """ + self.bias_enabled = True + time.sleep(0.500) # not sure if needed + self.rf_enabled = True diff --git a/tests/instruments/kuhneelectronic/test_kusg245_250a.py b/tests/instruments/kuhneelectronic/test_kusg245_250a.py new file mode 100644 index 0000000000..c314db27dd --- /dev/null +++ b/tests/instruments/kuhneelectronic/test_kusg245_250a.py @@ -0,0 +1,273 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import pytest + +from pymeasure.test import expected_protocol +from pymeasure.instruments.kuhneelectronic import Kusg245_250A + + +def test_external_enabled(): + with expected_protocol( + Kusg245_250A, + [("R", None), ("r?", "1"), ("r", None), ("r?", "0")], + ) as inst: + inst.external_enabled = True + assert inst.external_enabled is True + inst.external_enabled = False + assert inst.external_enabled is False + + +def test_bias_enabled(): + with expected_protocol( + Kusg245_250A, + [("X", None), ("x?", "1"), ("x", None), ("x?", "0")], + ) as inst: + inst.bias_enabled = True + assert inst.bias_enabled is True + inst.bias_enabled = False + assert inst.bias_enabled is False + + +def test_rf_enabled(): + with expected_protocol( + Kusg245_250A, + [("O", None), ("o?", "1"), ("o", None), ("o?", "0")], + ) as inst: + inst.rf_enabled = True + assert inst.rf_enabled is True + inst.rf_enabled = False + assert inst.rf_enabled is False + + +def test_pulse_mode_enabled(): + with expected_protocol( + Kusg245_250A, + [("P", None), ("p?", "1"), ("p", None), ("p?", "0")], + ) as inst: + inst.pulse_mode_enabled = True + assert inst.pulse_mode_enabled is True + inst.pulse_mode_enabled = False + assert inst.pulse_mode_enabled is False + + +def test_freq_steps_fine_enabled(): + with expected_protocol( + Kusg245_250A, + [("fm1", None), ("fm?", "1"), ("fm0", None), ("fm?", "0")], + ) as inst: + inst.freq_steps_fine_enabled = True + assert inst.freq_steps_fine_enabled is True + inst.freq_steps_fine_enabled = False + assert inst.freq_steps_fine_enabled is False + + +def test_frequency_coarse(): + with expected_protocol( + Kusg245_250A, + [ + ("f2456", None), + ("f?", "2456"), + ("f2400", None), + ("f?", "2400"), + ("f2500", None), + ("f?", "2500"), + ], + ) as inst: + inst.frequency_coarse = 2456 + assert inst.frequency_coarse == 2456 + inst.frequency_coarse = 2000 # must be truncated to 2400 + assert inst.frequency_coarse == 2400 + inst.frequency_coarse = 3000 # must be truncated to 2500 + assert inst.frequency_coarse == 2500 + + +def test_frequency_fine(): + with expected_protocol( + Kusg245_250A, + [ + ("f2456780", None), + ("f?", "2456780"), + ("f2400000", None), + ("f?", "2400000"), + ("f2500000", None), + ("f?", "2500000"), + ], + ) as inst: + inst.frequency_fine = 2456778 # must be rounded to 2456780 + assert inst.frequency_fine == 2456780 + inst.frequency_fine = 2000000 # must be truncated to 2400000 + assert inst.frequency_fine == 2400000 + inst.frequency_fine = 3000000 # must be truncated to 2500000 + assert inst.frequency_fine == 2500000 + + +def test_power_setpoint(): + with expected_protocol( + Kusg245_250A, + [ + ("A123", None), + ("A?", "123"), + ("A000", None), + ("A?", "000"), + ("A250", None), + ("A?", "250"), + ], + ) as inst: + inst.power_setpoint = 123 + assert inst.power_setpoint == 123 + inst.power_setpoint = -1 # must be truncated to 0 + assert inst.power_setpoint == 0 + inst.power_setpoint = 300 # must be truncated to 250 + assert inst.power_setpoint == 250 + + +def test_power_setpoint_limited(): + with expected_protocol( + Kusg245_250A, [("A000", None), ("A?", "000"), ("A020", None), ("A?", "020")], power_limit=20 + ) as inst: + inst.power_setpoint = -1 # must be truncated to 0 + assert inst.power_setpoint == 0 + inst.power_setpoint = 300 # must be truncated to power_limit + assert inst.power_setpoint == 20 + + +def test_pulse_width(): + with expected_protocol( + Kusg245_250A, + [ + ("C0125", None), + ("C?", "0125"), + ("C0010", None), + ("C?", "0010"), + ("C1000", None), + ("C?", "1000"), + ], + ) as inst: + inst.pulse_width = 123 # must be rounded to 125 + assert inst.pulse_width == 125 + inst.pulse_width = 0 # must be truncated to 10 + assert inst.pulse_width == 10 + inst.pulse_width = 10000 # must be truncated to 250 + assert inst.pulse_width == 1000 + + +def test_off_time(): + with expected_protocol( + Kusg245_250A, + [ + ("c0125", None), + ("c?", "0125"), + ("c0010", None), + ("c?", "0010"), + ("c1000", None), + ("c?", "1000"), + ], + ) as inst: + inst.off_time = 123 # must be rounded to 125 + assert inst.off_time == 125 + inst.off_time = 0 # must be truncated to 10 + assert inst.off_time == 10 + inst.off_time = 10000 # must be truncated to 250 + assert inst.off_time == 1000 + + +def test_phase_shift(): + with expected_protocol( + Kusg245_250A, + [ + ("H088", None), + ("H?", "088"), + ("H000", None), + ("H?", "000"), + ("H255", None), + ("H?", "255"), + ], + ) as inst: + inst.phase_shift = 124 + assert inst.phase_shift == pytest.approx(124, 0.01) + inst.phase_shift = -1 # must be truncated to 0 + assert inst.phase_shift == 0 + inst.phase_shift = 358.6 + assert inst.phase_shift == pytest.approx(358.6, 0.01) + + +def test_reflection_limit(): + with expected_protocol( + Kusg245_250A, + [("B0", None), ("B?", "0"), ("B4", None), ("B?", "4"), ("B5", None), ("B?", "5")], + ) as inst: + inst.reflection_limit = 0 + assert inst.reflection_limit == 0 + inst.reflection_limit = 182 # must be rounded to the next + # higher discrete value (200) + assert inst.reflection_limit == 200 + inst.reflection_limit = 300 # must be truncated to 230 + assert inst.reflection_limit == 230 + + +def test_tune(): + with expected_protocol( + Kusg245_250A, + [("b010", None)], + ) as inst: + inst.tune(10) + + +def test_tune_power_limited(): + with expected_protocol(Kusg245_250A, [("b020", None)], power_limit=20) as inst: + inst.tune(100) + + +def test_clear_VSWR_error(): + with expected_protocol( + Kusg245_250A, + [("z", None)], + ) as inst: + inst.clear_VSWR_error() + + +def test_store_settings(): + with expected_protocol( + Kusg245_250A, + [("SE", None)], + ) as inst: + inst.store_settings() + + +def test_shutdown(): + with expected_protocol( + Kusg245_250A, + [("o", None), + ("x", None)], + ) as inst: + inst.shutdown() + + +def test_turn_on(): + with expected_protocol( + Kusg245_250A, + [("X", None), + ("O", None)], + ) as inst: + inst.turn_on() From e5746a0281b39ddc3fad2622927f094f81b4da08 Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Thu, 26 Jan 2023 10:14:06 +0100 Subject: [PATCH 002/181] Fix: requests returning binary values + acknowledge character processing --- .../kuhneelectronic/kusg245_250a.py | 259 +++++++++++++----- .../kuhneelectronic/test_kusg245_250a.py | 152 +++++++--- 2 files changed, 294 insertions(+), 117 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index 5749e51ed0..edd385a80a 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -29,6 +29,28 @@ from pymeasure.instruments.validators import truncated_range, truncated_discrete_set +byteorder = 'little' +encoding = 'utf-8' +termination_character = "\r" +reflection_limit_map = {0: 0, 1: 100, 2: 150, 3: 180, 4: 200, 5: 230} + + +def _has_correct_termination_character(b): + return b[-1] == termination_character.encode(encoding=encoding)[0] + + +def _err_msg_invalid_termination_character(b): + return f"Invalid termination character received: {hex(b[-1])}" + + +def _is_expecting_acknowledgement(command): + if command in ["v", "5", "8", "6", "7", "T"]: + return False + if command.endswith("?"): + return False + return True + + class Kusg245_250A(Instrument): """Represents KU SG 2.45 250 A the 2.45 GHz ISM-Band Microwave Generator and provides a high-level interface for interacting with the instrument. @@ -57,8 +79,8 @@ class Kusg245_250A(Instrument): def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): kwargs.setdefault("baud_rate", 115200) - kwargs.setdefault("read_termination", "\r") - kwargs.setdefault("write_termination", "\r") + kwargs.setdefault("read_termination", termination_character) + kwargs.setdefault("write_termination", termination_character) super().__init__(adapter, name, **kwargs) @@ -68,86 +90,154 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): version = Instrument.measurement("v", """Readout of the firmware version.""") - voltage_5v = Instrument.measurement( - "5", - """Readout of internal 5V supply voltage in Volts.""", - get_process=lambda v: 103.0 / 4700.0 * v, - ) - - voltage_32v = Instrument.measurement( - "8", - """Readout of 32V supply voltage in Volts.""", - get_process=lambda v: 1282.0 / 8200.0 * v, - ) - - power_forward = Instrument.measurement("6", """Readout of forward power in Watts.""") - - power_reverse = Instrument.measurement("7", """Readout of reverse power in Watts.""") + @property + def voltage_5v(self): + """Readout of internal 5V supply voltage in Volts.""" + self.write("5") + b = self.read_bytes(3) + if _has_correct_termination_character(b): + return 103.0 / 4700.0 * int.from_bytes(b[:2]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @property + def voltage_32v(self): + """Readout of 32V supply voltage in Volts.""" + self.write("8") + b = self.read_bytes(3) + if _has_correct_termination_character(b): + return 1282.0 / 8200.0 * int.from_bytes(b[:2]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @property + def power_forward(self): + """Readout of forward power in Watts.""" + self.write("6") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return int.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @property + def power_reverse(self): + """Readout of reverse power in Watts.""" + self.write("7") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return int.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) temperature = Instrument.measurement( - "T", """Readout of temperature sensor near the final transistor in °C.""" + "T", + """Readout of temperature sensor near the final transistor in °C.""" ) - external_enabled = Instrument.control( - "r?", - "%s", + @property + def external_enabled(self): """Control the whether the amplifier enabling is done via external inputs on 8-pin connector or via the serial interface (boolean). - """, - validator=strict_discrete_set, - values=[False, True], - set_process=lambda v: {True: "R", False: "r"}[v], - get_process=lambda v: bool(v) - ) - - bias_enabled = Instrument.control( - "x?", - "%s", + """ + self.write("r?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return bool.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @external_enabled.setter + def external_enabled(self, value): + if value: + self.write("R") + else: + self.write("r") + + @property + def bias_enabled(self): """Transistor biasing (boolean). Biasing must be enabled before switching RF on (see :attr:`~.Kusg245_250A.rf_enabled`). - """, - validator=strict_discrete_set, - values=[False, True], - set_process=lambda v: {True: "X", False: "x"}[v], - get_process=lambda v: bool(v) - ) - - rf_enabled = Instrument.control( - "o?", - "%s", + """ + self.write("x?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return bool.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @bias_enabled.setter + def bias_enabled(self, value): + if value: + self.write("X") + else: + self.write("x") + + @property + def rf_enabled(self): """Enable RF output (boolean). .. note:: Bias must be enabled before RF is enabled (see :attr:`~.Kusg245_250A.bias_enabled`) - """, - validator=strict_discrete_set, - values=[False, True], - set_process=lambda v: {True: "O", False: "o"}[v], - get_process=lambda v: bool(v) - ) + """ + self.write("o?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return bool.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @rf_enabled.setter + def rf_enabled(self, value): + if value: + self.write("O") + else: + self.write("o") + + @property + def pulse_mode_enabled(self): + """Enable RF output (boolean). - pulse_mode_enabled = Instrument.control( - "p?", - "%s", - """Enable pulse mode (boolean).""", - validator=strict_discrete_set, - values=[False, True], - set_process=lambda v: {True: "P", False: "p"}[v], - get_process=lambda v: bool(v) - ) + .. note:: + + Bias must be enabled before RF is enabled + (see :attr:`~.Kusg245_250A.bias_enabled`) + """ + self.write("p?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return bool.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @pulse_mode_enabled.setter + def pulse_mode_enabled(self, value): + if value: + self.write("P") + else: + self.write("p") + + @property + def freq_steps_fine_enabled(self): + """Enables fine frequency steps (boolean).""" + self.write("fm?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return bool.from_bytes(b[:1]) + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @freq_steps_fine_enabled.setter + def freq_steps_fine_enabled(self, value): + if value: + self.write("fm1") + else: + self.write("fm0") freq_steps_fine_enabled = Instrument.control( "fm?", "fm%d", """Enables fine frequency steps (boolean).""", validator=strict_discrete_set, - values={False: 0, True: 1}, - map_values=True, + values=[False, True], + set_process=lambda v: {True: 1, False: 0}[v], + get_process=lambda v: bool.from_bytes(v.encode(encoding), byteorder=byteorder), ) frequency_coarse = Instrument.control( @@ -161,6 +251,7 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): """, validator=truncated_range, values=[2400, 2500], + get_process=lambda v: int(v[:-3]) if v.endswith("MHz") else None, ) frequency_fine = Instrument.control( @@ -176,6 +267,7 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): validator=truncated_range, values=[2400000, 2500000], set_process=lambda v: round(v, -1), + get_process=lambda v: int(v[:-3]) if v.endswith("kHz") else None, ) power_setpoint = Instrument.control( @@ -217,21 +309,25 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): set_process=lambda v: round(2 * v, -1) / 2, ) - phase_shift = Instrument.control( - "H?", - "H%03d", + @property + def phase_shift(self): """Phase shift in degrees (float from 0 to 358.6). Resolution: 8-bits. Values out of range are truncated. - """, - validator=truncated_range, - values=[0, 358.6], - set_process=lambda v: round(v / 360 * 256), - get_process=lambda v: v / 256 * 360, - ) - - reflection_limit = Instrument.control( - "B?", + """ + self.write("H?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return int.from_bytes(b[:1]) / 256.0 * 360.0 + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @phase_shift.setter + def phase_shift(self, value): + value = int(round(truncated_range(value, [0, 358.6])) / 360.0 * 256.0) + self.write(f"H{value:03d}") + + @property + def reflection_limit(self): "B%d", """Limit of reflection in Watts (integer in 0 - no limit, 100, 150, 180, 200, 230). @@ -241,11 +337,19 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): is reduced to the specified value and the power control mechanism is locked until the alarm has been cleared by the user via :meth:`~.Kusg245_250A.clear_VSWR_error()`. - """, - validator=truncated_discrete_set, - values={0: 0, 100: 1, 150: 2, 180: 3, 200: 4, 230: 5}, - map_values=True, - ) + """ + self.write("B?") + b = self.read_bytes(2) + if _has_correct_termination_character(b): + return reflection_limit_map[b[0]] + raise ConnectionError(_err_msg_invalid_termination_character(b)) + + @reflection_limit.setter + def reflection_limit(self, value): + value = truncated_discrete_set(value, reflection_limit_map.values()) + inv_reflection_limit_map = {v: k for k, v in reflection_limit_map.items()} + value = inv_reflection_limit_map[value] + self.write(f"B{value:d}") def tune(self, power): """ @@ -303,3 +407,8 @@ def turn_on(self): self.bias_enabled = True time.sleep(0.500) # not sure if needed self.rf_enabled = True + + def write(self, command, **kwargs): + self.adapter.write(command, **kwargs) + if _is_expecting_acknowledgement(command) and self.read() != "A": + raise ConnectionError("Expected acknowledgment.") diff --git a/tests/instruments/kuhneelectronic/test_kusg245_250a.py b/tests/instruments/kuhneelectronic/test_kusg245_250a.py index c314db27dd..d969a5ae51 100644 --- a/tests/instruments/kuhneelectronic/test_kusg245_250a.py +++ b/tests/instruments/kuhneelectronic/test_kusg245_250a.py @@ -25,12 +25,53 @@ from pymeasure.test import expected_protocol from pymeasure.instruments.kuhneelectronic import Kusg245_250A +from pymeasure.instruments.kuhneelectronic.kusg245_250a import termination_character, encoding + + +termination_character = termination_character.encode(encoding=encoding)[0] + + +def test_voltage_5v(): + with expected_protocol( + Kusg245_250A, + [("5", bytes([0, 1, termination_character]))], + ) as inst: + assert inst.voltage_5v == 103.0 / 4700.0 + + +def test_voltage_32v(): + with expected_protocol( + Kusg245_250A, + [("8", bytes([0, 1, termination_character]))], + ) as inst: + assert inst.voltage_32v == 1282.0 / 8200.0 + + +def test_power_forward(): + with expected_protocol( + Kusg245_250A, + [("6", bytes([255, termination_character]))], + ) as inst: + assert inst.power_forward == 255 + + +def test_power_reverse(): + with expected_protocol( + Kusg245_250A, + [("7", bytes([255, termination_character]))], + ) as inst: + assert inst.power_reverse == 255 def test_external_enabled(): with expected_protocol( Kusg245_250A, - [("R", None), ("r?", "1"), ("r", None), ("r?", "0")], + [ + ("R", "A"), + ("r?", bytes([1, termination_character])), + ("r", "A"), + ("r?", bytes([0, termination_character])) + ], ) as inst: inst.external_enabled = True assert inst.external_enabled is True @@ -41,7 +82,12 @@ def test_external_enabled(): def test_bias_enabled(): with expected_protocol( Kusg245_250A, - [("X", None), ("x?", "1"), ("x", None), ("x?", "0")], + [ + ("X", "A"), + ("x?", bytes([1, termination_character])), + ("x", "A"), + ("x?", bytes([0, termination_character])) + ], ) as inst: inst.bias_enabled = True assert inst.bias_enabled is True @@ -52,7 +98,12 @@ def test_bias_enabled(): def test_rf_enabled(): with expected_protocol( Kusg245_250A, - [("O", None), ("o?", "1"), ("o", None), ("o?", "0")], + [ + ("O", "A"), + ("o?", bytes([1, termination_character])), + ("o", "A"), + ("o?", bytes([0, termination_character])) + ], ) as inst: inst.rf_enabled = True assert inst.rf_enabled is True @@ -63,7 +114,12 @@ def test_rf_enabled(): def test_pulse_mode_enabled(): with expected_protocol( Kusg245_250A, - [("P", None), ("p?", "1"), ("p", None), ("p?", "0")], + [ + ("P", "A"), + ("p?", bytes([1, termination_character])), + ("p", "A"), + ("p?", bytes([0, termination_character])) + ], ) as inst: inst.pulse_mode_enabled = True assert inst.pulse_mode_enabled is True @@ -74,7 +130,12 @@ def test_pulse_mode_enabled(): def test_freq_steps_fine_enabled(): with expected_protocol( Kusg245_250A, - [("fm1", None), ("fm?", "1"), ("fm0", None), ("fm?", "0")], + [ + ("fm1", "A"), + ("fm?", bytes([1, termination_character])), + ("fm0", "A"), + ("fm?", bytes([0, termination_character])) + ], ) as inst: inst.freq_steps_fine_enabled = True assert inst.freq_steps_fine_enabled is True @@ -86,12 +147,12 @@ def test_frequency_coarse(): with expected_protocol( Kusg245_250A, [ - ("f2456", None), - ("f?", "2456"), - ("f2400", None), - ("f?", "2400"), - ("f2500", None), - ("f?", "2500"), + ("f2456", "A"), + ("f?", "2456MHz"), + ("f2400", "A"), + ("f?", "2400MHz"), + ("f2500", "A"), + ("f?", "2500MHz"), ], ) as inst: inst.frequency_coarse = 2456 @@ -106,12 +167,12 @@ def test_frequency_fine(): with expected_protocol( Kusg245_250A, [ - ("f2456780", None), - ("f?", "2456780"), - ("f2400000", None), - ("f?", "2400000"), - ("f2500000", None), - ("f?", "2500000"), + ("f2456780", "A"), + ("f?", "2456780kHz"), + ("f2400000", "A"), + ("f?", "2400000kHz"), + ("f2500000", "A"), + ("f?", "2500000kHz"), ], ) as inst: inst.frequency_fine = 2456778 # must be rounded to 2456780 @@ -126,11 +187,11 @@ def test_power_setpoint(): with expected_protocol( Kusg245_250A, [ - ("A123", None), + ("A123", "A"), ("A?", "123"), - ("A000", None), + ("A000", "A"), ("A?", "000"), - ("A250", None), + ("A250", "A"), ("A?", "250"), ], ) as inst: @@ -144,7 +205,7 @@ def test_power_setpoint(): def test_power_setpoint_limited(): with expected_protocol( - Kusg245_250A, [("A000", None), ("A?", "000"), ("A020", None), ("A?", "020")], power_limit=20 + Kusg245_250A, [("A000", "A"), ("A?", "000"), ("A020", "A"), ("A?", "020")], power_limit=20 ) as inst: inst.power_setpoint = -1 # must be truncated to 0 assert inst.power_setpoint == 0 @@ -156,11 +217,11 @@ def test_pulse_width(): with expected_protocol( Kusg245_250A, [ - ("C0125", None), + ("C0125", "A"), ("C?", "0125"), - ("C0010", None), + ("C0010", "A"), ("C?", "0010"), - ("C1000", None), + ("C1000", "A"), ("C?", "1000"), ], ) as inst: @@ -176,11 +237,11 @@ def test_off_time(): with expected_protocol( Kusg245_250A, [ - ("c0125", None), + ("c0125", "A"), ("c?", "0125"), - ("c0010", None), + ("c0010", "A"), ("c?", "0010"), - ("c1000", None), + ("c1000", "A"), ("c?", "1000"), ], ) as inst: @@ -196,12 +257,12 @@ def test_phase_shift(): with expected_protocol( Kusg245_250A, [ - ("H088", None), - ("H?", "088"), - ("H000", None), - ("H?", "000"), - ("H255", None), - ("H?", "255"), + ("H088", "A"), + ("H?", bytes([88, termination_character])), + ("H000", "A"), + ("H?", bytes([0, termination_character])), + ("H255", "A"), + ("H?", bytes([255, termination_character])), ], ) as inst: inst.phase_shift = 124 @@ -215,7 +276,14 @@ def test_phase_shift(): def test_reflection_limit(): with expected_protocol( Kusg245_250A, - [("B0", None), ("B?", "0"), ("B4", None), ("B?", "4"), ("B5", None), ("B?", "5")], + [ + ("B0", "A"), + ("B?", bytes([0, termination_character])), + ("B4", "A"), + ("B?", bytes([4, termination_character])), + ("B5", "A"), + ("B?", bytes([5, termination_character])) + ], ) as inst: inst.reflection_limit = 0 assert inst.reflection_limit == 0 @@ -229,20 +297,20 @@ def test_reflection_limit(): def test_tune(): with expected_protocol( Kusg245_250A, - [("b010", None)], + [("b010", "A")], ) as inst: inst.tune(10) def test_tune_power_limited(): - with expected_protocol(Kusg245_250A, [("b020", None)], power_limit=20) as inst: + with expected_protocol(Kusg245_250A, [("b020", "A")], power_limit=20) as inst: inst.tune(100) def test_clear_VSWR_error(): with expected_protocol( Kusg245_250A, - [("z", None)], + [("z", "A")], ) as inst: inst.clear_VSWR_error() @@ -250,7 +318,7 @@ def test_clear_VSWR_error(): def test_store_settings(): with expected_protocol( Kusg245_250A, - [("SE", None)], + [("SE", "A")], ) as inst: inst.store_settings() @@ -258,8 +326,8 @@ def test_store_settings(): def test_shutdown(): with expected_protocol( Kusg245_250A, - [("o", None), - ("x", None)], + [("o", "A"), + ("x", "A")], ) as inst: inst.shutdown() @@ -267,7 +335,7 @@ def test_shutdown(): def test_turn_on(): with expected_protocol( Kusg245_250A, - [("X", None), - ("O", None)], + [("X", "A"), + ("O", "A")], ) as inst: inst.turn_on() From a75ea30585440e34b50ea9f750888a1018ce5040 Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Thu, 26 Jan 2023 10:15:15 +0100 Subject: [PATCH 003/181] Update AUTHORS.txt --- AUTHORS.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 24c2de6bdf..39e9c6220d 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -57,4 +57,5 @@ David Sanchez Sanchez Andres Ruz-Nieto Carlos Martinez Scott Candey -Tom Verbeure \ No newline at end of file +Tom Verbeure +Juraj Jašík From 85b6940074167775834e81e23648893991e80e5b Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Mon, 6 Feb 2023 15:59:23 +0100 Subject: [PATCH 004/181] Import from validators once --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index edd385a80a..b9308cf205 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -25,8 +25,8 @@ import time from pymeasure.instruments import Instrument -from pymeasure.instruments.validators import strict_discrete_set -from pymeasure.instruments.validators import truncated_range, truncated_discrete_set +from pymeasure.instruments.validators import (strict_discrete_set, + truncated_range, truncated_discrete_set) byteorder = 'little' From 816b189bc33e9ef61746ea55d5daf3199e498469 Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Mon, 6 Feb 2023 16:26:55 +0100 Subject: [PATCH 005/181] Baud rate and termination characters added as ASRL dict to the super init. --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index b9308cf205..c7ea445dd3 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -78,11 +78,12 @@ class Kusg245_250A(Instrument): """ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): - kwargs.setdefault("baud_rate", 115200) - kwargs.setdefault("read_termination", termination_character) - kwargs.setdefault("write_termination", termination_character) - - super().__init__(adapter, name, **kwargs) + super().__init__(adapter, + name, + asrl={"baud_rate": 115200, + "read_termination": termination_character, + "write_termination": termination_character}, + **kwargs) assert 0 < power_limit <= 250 self._power_limit = power_limit From ae63b430be94d6f7243c89c48445fe818c5b0899 Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Mon, 6 Feb 2023 16:34:43 +0100 Subject: [PATCH 006/181] Parameter power_limit tested before creating VISA connection, error message added, docstring updated. --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index c7ea445dd3..2c389790f0 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -55,7 +55,7 @@ class Kusg245_250A(Instrument): """Represents KU SG 2.45 250 A the 2.45 GHz ISM-Band Microwave Generator and provides a high-level interface for interacting with the instrument. - :param power_limit: power set-point limit in Watts. + :param power_limit: power set-point limit in Watts (integer from 0 to 250). See :attr:`~.Kusg245_250A.power_setpoint` and :meth:`~.Kusg245_250A.tune()`. @@ -78,6 +78,7 @@ class Kusg245_250A(Instrument): """ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): + assert 0 < power_limit <= 250, "Param 'power_limit' is out of bounds (0, 250)." super().__init__(adapter, name, asrl={"baud_rate": 115200, @@ -85,7 +86,6 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): "write_termination": termination_character}, **kwargs) - assert 0 < power_limit <= 250 self._power_limit = power_limit self.power_setpoint_values = [0, power_limit] From ab2b46bbf2b8ea6c534afdb5d70cc3dcb77e1559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Ja=C5=A1=C3=ADk?= <89189631+jurajjasik@users.noreply.github.com> Date: Mon, 6 Feb 2023 16:41:04 +0100 Subject: [PATCH 007/181] Docstring of external_enabled() method updated. Co-authored-by: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index 2c389790f0..10514ff19c 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -134,7 +134,7 @@ def power_reverse(self): @property def external_enabled(self): - """Control the whether the amplifier enabling is done + """Control whether the amplifier enabling is done via external inputs on 8-pin connector or via the serial interface (boolean). """ From 9e875a671c8da44928ebbf6f2244d02ee750401a Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Mon, 6 Feb 2023 17:15:17 +0100 Subject: [PATCH 008/181] Redundant (wrong) definition of freq_steps_fine_enabled() deleted. Explicit value of byteorder param of from_bytes() methodes added. --- .../kuhneelectronic/kusg245_250a.py | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index 10514ff19c..fcf3401e03 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -25,11 +25,10 @@ import time from pymeasure.instruments import Instrument -from pymeasure.instruments.validators import (strict_discrete_set, - truncated_range, truncated_discrete_set) +from pymeasure.instruments.validators import truncated_range, truncated_discrete_set -byteorder = 'little' +byteorder = 'big' encoding = 'utf-8' termination_character = "\r" reflection_limit_map = {0: 0, 1: 100, 2: 150, 3: 180, 4: 200, 5: 230} @@ -97,7 +96,7 @@ def voltage_5v(self): self.write("5") b = self.read_bytes(3) if _has_correct_termination_character(b): - return 103.0 / 4700.0 * int.from_bytes(b[:2]) + return 103.0 / 4700.0 * int.from_bytes(b[:2], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @property @@ -106,7 +105,7 @@ def voltage_32v(self): self.write("8") b = self.read_bytes(3) if _has_correct_termination_character(b): - return 1282.0 / 8200.0 * int.from_bytes(b[:2]) + return 1282.0 / 8200.0 * int.from_bytes(b[:2], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @property @@ -115,7 +114,7 @@ def power_forward(self): self.write("6") b = self.read_bytes(2) if _has_correct_termination_character(b): - return int.from_bytes(b[:1]) + return int.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @property @@ -124,7 +123,7 @@ def power_reverse(self): self.write("7") b = self.read_bytes(2) if _has_correct_termination_character(b): - return int.from_bytes(b[:1]) + return int.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) temperature = Instrument.measurement( @@ -141,7 +140,7 @@ def external_enabled(self): self.write("r?") b = self.read_bytes(2) if _has_correct_termination_character(b): - return bool.from_bytes(b[:1]) + return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @external_enabled.setter @@ -161,7 +160,7 @@ def bias_enabled(self): self.write("x?") b = self.read_bytes(2) if _has_correct_termination_character(b): - return bool.from_bytes(b[:1]) + return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @bias_enabled.setter @@ -183,7 +182,7 @@ def rf_enabled(self): self.write("o?") b = self.read_bytes(2) if _has_correct_termination_character(b): - return bool.from_bytes(b[:1]) + return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @rf_enabled.setter @@ -205,7 +204,7 @@ def pulse_mode_enabled(self): self.write("p?") b = self.read_bytes(2) if _has_correct_termination_character(b): - return bool.from_bytes(b[:1]) + return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @pulse_mode_enabled.setter @@ -221,7 +220,7 @@ def freq_steps_fine_enabled(self): self.write("fm?") b = self.read_bytes(2) if _has_correct_termination_character(b): - return bool.from_bytes(b[:1]) + return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @freq_steps_fine_enabled.setter @@ -231,16 +230,6 @@ def freq_steps_fine_enabled(self, value): else: self.write("fm0") - freq_steps_fine_enabled = Instrument.control( - "fm?", - "fm%d", - """Enables fine frequency steps (boolean).""", - validator=strict_discrete_set, - values=[False, True], - set_process=lambda v: {True: 1, False: 0}[v], - get_process=lambda v: bool.from_bytes(v.encode(encoding), byteorder=byteorder), - ) - frequency_coarse = Instrument.control( "f?", "f%04d", @@ -319,7 +308,7 @@ def phase_shift(self): self.write("H?") b = self.read_bytes(2) if _has_correct_termination_character(b): - return int.from_bytes(b[:1]) / 256.0 * 360.0 + return int.from_bytes(b[:1], byteorder=byteorder) / 256.0 * 360.0 raise ConnectionError(_err_msg_invalid_termination_character(b)) @phase_shift.setter From 301d18c7477110cc8a568a446fa2625edb8d0caa Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Tue, 7 Feb 2023 08:39:41 +0100 Subject: [PATCH 009/181] Docstrings updated. --- .../kuhneelectronic/kusg245_250a.py | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index fcf3401e03..1a31980b16 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -88,11 +88,11 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): self._power_limit = power_limit self.power_setpoint_values = [0, power_limit] - version = Instrument.measurement("v", """Readout of the firmware version.""") + version = Instrument.measurement("v", """Get firmware version.""") @property def voltage_5v(self): - """Readout of internal 5V supply voltage in Volts.""" + """Measure internal 5V supply voltage in Volts.""" self.write("5") b = self.read_bytes(3) if _has_correct_termination_character(b): @@ -101,7 +101,7 @@ def voltage_5v(self): @property def voltage_32v(self): - """Readout of 32V supply voltage in Volts.""" + """Measure 32V supply voltage in Volts.""" self.write("8") b = self.read_bytes(3) if _has_correct_termination_character(b): @@ -110,7 +110,7 @@ def voltage_32v(self): @property def power_forward(self): - """Readout of forward power in Watts.""" + """Measure forward power in Watts.""" self.write("6") b = self.read_bytes(2) if _has_correct_termination_character(b): @@ -119,7 +119,7 @@ def power_forward(self): @property def power_reverse(self): - """Readout of reverse power in Watts.""" + """Measure reverse power in Watts.""" self.write("7") b = self.read_bytes(2) if _has_correct_termination_character(b): @@ -128,14 +128,14 @@ def power_reverse(self): temperature = Instrument.measurement( "T", - """Readout of temperature sensor near the final transistor in °C.""" + """Measure temperature near final transistor in °C.""" ) @property def external_enabled(self): - """Control whether the amplifier enabling is done + """Control whether amplifier enabling is done via external inputs on 8-pin connector - or via the serial interface (boolean). + or via serial interface (boolean). """ self.write("r?") b = self.read_bytes(2) @@ -152,7 +152,7 @@ def external_enabled(self, value): @property def bias_enabled(self): - """Transistor biasing (boolean). + """Control whether transistor biasing is enabled (boolean). Biasing must be enabled before switching RF on (see :attr:`~.Kusg245_250A.rf_enabled`). @@ -172,11 +172,11 @@ def bias_enabled(self, value): @property def rf_enabled(self): - """Enable RF output (boolean). + """Control whether RF output is enabled (boolean). .. note:: - Bias must be enabled before RF is enabled + Biasing must be enabled before RF is enabled (see :attr:`~.Kusg245_250A.bias_enabled`) """ self.write("o?") @@ -194,11 +194,11 @@ def rf_enabled(self, value): @property def pulse_mode_enabled(self): - """Enable RF output (boolean). + """Control whether pulse mode is enabled (boolean). .. note:: - Bias must be enabled before RF is enabled + Biasing must be enabled before the pulse mode is enabled (see :attr:`~.Kusg245_250A.bias_enabled`) """ self.write("p?") @@ -216,7 +216,7 @@ def pulse_mode_enabled(self, value): @property def freq_steps_fine_enabled(self): - """Enables fine frequency steps (boolean).""" + """Control whether fine frequency steps are enabled (boolean).""" self.write("fm?") b = self.read_bytes(2) if _has_correct_termination_character(b): @@ -233,7 +233,7 @@ def freq_steps_fine_enabled(self, value): frequency_coarse = Instrument.control( "f?", "f%04d", - """Coarse frequency in MHz (integer from 2400 to 2500). + """Control coarse frequency in MHz (integer from 2400 to 2500). Fine frequency mode must be disabled (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`). @@ -247,7 +247,7 @@ def freq_steps_fine_enabled(self, value): frequency_fine = Instrument.control( "f?", "f%07d", - """Fine frequency in kHz (integer from 2400000 to 2500000). + """Control fine frequency in kHz (integer from 2400000 to 2500000). Fine frequency mode must be enabled (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`). @@ -263,7 +263,7 @@ def freq_steps_fine_enabled(self, value): power_setpoint = Instrument.control( "A?", "A%03d", - """Output power set-point in Watts (integer from 0 to :attr:`power_limit` + """Control output power set-point in Watts (integer from 0 to :attr:`power_limit` parameter - see constructor). Resolution: 1 W. Invalid values are truncated. @@ -276,7 +276,7 @@ def freq_steps_fine_enabled(self, value): pulse_width = Instrument.control( "C?", "C%04d", - """Pulse width in ms (integer from 10 to 1000). + """Control pulse width in ms (integer from 10 to 1000). Resolution: 5 ms. Invalid values are truncated. Values are rounded to multipliers of 5. @@ -289,7 +289,7 @@ def freq_steps_fine_enabled(self, value): off_time = Instrument.control( "c?", "c%04d", - """Off time for the pulse mode in ms (integer from 10 to 1000). + """Control off time for the pulse mode in ms (integer from 10 to 1000). Resolution: 5 ms. Invalid values are truncated. Values are rounded to multipliers of 5. @@ -301,7 +301,7 @@ def freq_steps_fine_enabled(self, value): @property def phase_shift(self): - """Phase shift in degrees (float from 0 to 358.6). + """Control phase shift in degrees (float from 0 to 358.6). Resolution: 8-bits. Values out of range are truncated. """ @@ -319,7 +319,8 @@ def phase_shift(self, value): @property def reflection_limit(self): "B%d", - """Limit of reflection in Watts (integer in 0 - no limit, 100, 150, 180, 200, 230). + """Control limit of reflection in Watts + (integer in 0 - no limit, 100, 150, 180, 200, 230). .. note:: @@ -342,8 +343,7 @@ def reflection_limit(self, value): self.write(f"B{value:d}") def tune(self, power): - """ - Find and set the frequency with lowest reflection + """Find and set frequency with lowest reflection at a given power. :param power: A power set-point for tuning (in Watts). @@ -354,18 +354,16 @@ def tune(self, power): self.write(f"b{power:03d}") def clear_VSWR_error(self): - """ - Clears the VSWR error. + """Clear the VSWR error. See: :attr:`~.Kusg245_250A.reflection_limit`. """ self.write("z") def store_settings(self): - """ - Save actual settings to EEPROM. + """Save actual settings to EEPROM. - The following parameters are saved: + The following parameters are stored: frequency mode (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`), frequency (see :attr:`~.Kusg245_250A.frequency_coarse` or :attr:`~.Kusg245_250A.frequency_fine`), @@ -378,8 +376,7 @@ def store_settings(self): self.write("SE") def shutdown(self): - """ - Safe shut-down the generator. + """Safe shut-down the generator. 1. Disable RF output. 2. Deactivate biasing. @@ -388,8 +385,7 @@ def shutdown(self): self.bias_enabled = False def turn_on(self): - """ - Safe turn-on the generator. + """Safe turn-on the generator. 1. Activate biasing. 2. Enable RF output. From ffc3df0782e73a1f8ddd6b1eb579c0888f489c7a Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Thu, 9 Feb 2023 14:36:59 +0100 Subject: [PATCH 010/181] updated error message when acknowledgment is not received --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index 1a31980b16..f60fb39fa5 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -396,5 +396,7 @@ def turn_on(self): def write(self, command, **kwargs): self.adapter.write(command, **kwargs) - if _is_expecting_acknowledgement(command) and self.read() != "A": - raise ConnectionError("Expected acknowledgment.") + if _is_expecting_acknowledgement(command): + s = self.read() + if s != "A": + raise ConnectionError(f"Expected acknowledgment character 'A'. Received: '{s}'") From ad6dff3a8f58eddb8841a70fe39de9e85ab84f69 Mon Sep 17 00:00:00 2001 From: jurajjasik Date: Mon, 13 Feb 2023 13:09:26 +0100 Subject: [PATCH 011/181] Rename method shutdown() to turn_off() --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 4 ++-- tests/instruments/kuhneelectronic/test_kusg245_250a.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index f60fb39fa5..d3c2dcfb83 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -375,8 +375,8 @@ def store_settings(self): """ self.write("SE") - def shutdown(self): - """Safe shut-down the generator. + def turn_off(self): + """Safe turn-off the generator. 1. Disable RF output. 2. Deactivate biasing. diff --git a/tests/instruments/kuhneelectronic/test_kusg245_250a.py b/tests/instruments/kuhneelectronic/test_kusg245_250a.py index d969a5ae51..e65e13d84d 100644 --- a/tests/instruments/kuhneelectronic/test_kusg245_250a.py +++ b/tests/instruments/kuhneelectronic/test_kusg245_250a.py @@ -323,13 +323,13 @@ def test_store_settings(): inst.store_settings() -def test_shutdown(): +def test_turn_off(): with expected_protocol( Kusg245_250A, [("o", "A"), ("x", "A")], ) as inst: - inst.shutdown() + inst.turn_off() def test_turn_on(): From 06938a06a3e4041da12f97dd699d967f590c4bc4 Mon Sep 17 00:00:00 2001 From: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> Date: Mon, 8 May 2023 11:31:55 +0200 Subject: [PATCH 012/181] SCPI base commands moved to its own mixin class. --- CHANGES.rst | 1 + pymeasure/instruments/generic_types.py | 100 ++++++++++++++++++++++++ pymeasure/instruments/instrument.py | 8 ++ tests/instruments/test_generic_types.py | 70 +++++++++++++++++ tests/instruments/test_instrument.py | 10 +++ 5 files changed, 189 insertions(+) create mode 100644 pymeasure/instruments/generic_types.py create mode 100644 tests/instruments/test_generic_types.py diff --git a/CHANGES.rst b/CHANGES.rst index b2492292c9..214c1949f5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ Deprecated features - Attocube ANC300: :code:`host` argument, pass a resource string or adapter as :code:`Adapter` passed to :code:`Instrument`. Now communicates through the :code:`VISAAdapter` rather than deprecated :code:`TelnetAdapter`. The initializer now accepts :code:`name` as its second keyword argument so all previous initialization positional arguments (`axisnames`, `passwd`, `query_delay`) should be switched to keyword arguments. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept arbitrary keyword arguments anymore. Use the :code:`v_kwargs` parameter to give further arguments to :code:`values` method. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept `command_process` anymore. Use a dynamic property or a `Channel` instead, as appropriate. +- Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPImixin` class if the device supports SCPI commands. Version 0.11.1 (2022-12-31) =========================== diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py new file mode 100644 index 0000000000..81a3d3d2ad --- /dev/null +++ b/pymeasure/instruments/generic_types.py @@ -0,0 +1,100 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging +from warnings import warn + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +class SCPImixin: + """Base class for SCPI instruments with the default implementations of SCPI commands.""" + + def __init__(self, *args, **kwargs): + kwargs.setdefault("includeSCPI", False) # in order not to trigger the deprecation warning + super().__init__(*args, **kwargs) + + # SCPI default properties + @property + def complete(self): + """Get the synchronization bit. + + This property allows synchronization between a controller and a device. The Operation + Complete query places an ASCII character 1 into the device's Output Queue when all pending + selected device operations have been finished. + """ + return self.ask("*OPC?").strip() + + @property + def status(self): + """ Get the status byte and Master Summary Status bit. """ + return self.ask("*STB?").strip() + + @property + def options(self): + """ Get the device options installed. """ + return self.ask("*OPT?").strip() + + @property + def id(self): + """ Get the identification of the instrument. """ + return self.ask("*IDN?").strip() + + # SCPI default methods + def clear(self): + """ Clears the instrument status byte + """ + self.write("*CLS") + + def reset(self): + """ Resets the instrument. """ + self.write("*RST") + + def check_errors(self): + """ Read all errors from the instrument. + + :return: list of error entries + """ + errors = [] + while True: + err = self.values("SYST:ERR?") + if int(err[0]) != 0: + log.error(f"{self.name}: {err[0]}, {err[1]}") + errors.append(err) + else: + break + return errors + + +class SCPIunknownMixin(SCPImixin): + """Mixin which adds SCPI commands to an instrument from which it is not knwon, whether it + supports SCPI commands or not. + """ + + def __init__(self, *args, **kwargs): + kwargs.setdefault("includeSCPI", False) # in order not to trigger the deprecation warning + warn("It is not known, whether this device support SCPI commands or not. Please inform " + "the pymeasure maintainers, if you know the answer.", FutureWarning) + super().__init__(*args, **kwargs) diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py index 62f657803a..561f5c8a11 100644 --- a/pymeasure/instruments/instrument.py +++ b/pymeasure/instruments/instrument.py @@ -24,6 +24,7 @@ import logging import time +from warnings import warn from .common_base import CommonBase from ..adapters import VISAAdapter @@ -59,6 +60,10 @@ class Instrument(CommonBase): :param adapter: A string, integer, or :py:class:`~pymeasure.adapters.Adapter` subclass object :param string name: The name of the instrument. Often the model designation by default. :param includeSCPI: A boolean, which toggles the inclusion of standard SCPI commands + + .. deprecated:: 0.12 + Inherit the :class:`~pymeasure.instruments.generic_types.SCPImixin` class instead. + :param preprocess_reply: An optional callable used to preprocess strings received from the instrument. The callable returns the processed string. @@ -83,6 +88,9 @@ def __init__(self, adapter, name, includeSCPI=True, " PyVISA is not present") self.adapter = adapter self.SCPI = includeSCPI + if includeSCPI: + warn("Defining SCPI base functionality with `includeSCPI=True` is deprecated, inherit " + "the `SCPImixin` class instead.", FutureWarning) self.isShutdown = False self.name = name diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py new file mode 100644 index 0000000000..5ca3476ce9 --- /dev/null +++ b/tests/instruments/test_generic_types.py @@ -0,0 +1,70 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest + +from pymeasure.test import expected_protocol +from pymeasure.adapters import ProtocolAdapter +from pymeasure.instruments.generic_types import SCPImixin, SCPIunknownMixin +from pymeasure.instruments import Instrument + + +class Test_SCPIMixin: + class SCPIInstrument(SCPImixin, Instrument): + pass + + def test_init(self): + inst = self.SCPIInstrument(ProtocolAdapter(), "test") + assert inst.SCPI is False # should not be set by the new init + + @pytest.mark.parametrize("method, write, reply", (("id", "*IDN?", "xyz"), + ("complete", "*OPC?", "1"), + ("status", "*STB?", "189"), + ("options", "*OPT?", "a9"), + )) + def test_SCPI_properties(self, method, write, reply): + with expected_protocol( + self.SCPIInstrument, + [(write, reply)], + name="test") as instr: + assert getattr(instr, method) == reply + + @pytest.mark.parametrize("method, write", (("clear", "*CLS"), + ("reset", "*RST") + )) + def test_SCPI_write_commands(self, method, write): + with expected_protocol( + self.SCPIInstrument, + [(write, None)], + name="test") as instr: + getattr(instr, method)() + + +def test_SCPIunknownMixin(): + class SCPIunknownInstrument(SCPIunknownMixin, Instrument): + pass + + with pytest.warns(FutureWarning): + inst = SCPIunknownInstrument(ProtocolAdapter(), "test") + assert inst.SCPI is False diff --git a/tests/instruments/test_instrument.py b/tests/instruments/test_instrument.py index d7f6fa2c5b..95747fd8b5 100644 --- a/tests/instruments/test_instrument.py +++ b/tests/instruments/test_instrument.py @@ -116,6 +116,16 @@ def test_init_visa_fail(): Instrument("abc", "def", visa_library="@xyz") +def test_init_includeSCPI_implicit_warning(): + with pytest.warns(FutureWarning, match="includeSCPI"): + Instrument("COM1", "def", visa_library="@sim") + + +def test_init_includeSCPI_explicit_warning(): + with pytest.warns(FutureWarning, match="includeSCPI"): + Instrument("COM1", "def", visa_library="@sim", includeSCPI=True) + + def test_global_preprocess_reply(): with pytest.warns(FutureWarning, match="deprecated"): inst = Instrument(FakeAdapter(), "name", preprocess_reply=lambda v: v.strip("x")) From cf1cba4ef33fabc74857f617187e609f5ea25ead Mon Sep 17 00:00:00 2001 From: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> Date: Wed, 10 May 2023 09:35:38 +0200 Subject: [PATCH 013/181] Test for check_errors added. --- tests/instruments/test_generic_types.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py index 5ca3476ce9..b2e70715a3 100644 --- a/tests/instruments/test_generic_types.py +++ b/tests/instruments/test_generic_types.py @@ -51,7 +51,7 @@ def test_SCPI_properties(self, method, write, reply): assert getattr(instr, method) == reply @pytest.mark.parametrize("method, write", (("clear", "*CLS"), - ("reset", "*RST") + ("reset", "*RST"), )) def test_SCPI_write_commands(self, method, write): with expected_protocol( @@ -60,6 +60,17 @@ def test_SCPI_write_commands(self, method, write): name="test") as instr: getattr(instr, method)() + def test_check_errors(self): + with expected_protocol( + self.SCPIInstrument, + [("SYST:ERR?", '-100,"Command error"'), + ("SYST:ERR?", '-222,"Data out of range"'), + ("SYST:ERR?", '0,"No error"'), + ], + name="test") as instr: + assert instr.check_errors() == [[-100, '"Command error"'], + [-222, '"Data out of range"']] + def test_SCPIunknownMixin(): class SCPIunknownInstrument(SCPIunknownMixin, Instrument): From 89a36b69c8d40725e83fa0b0eeb5b9e43b3b2e05 Mon Sep 17 00:00:00 2001 From: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> Date: Wed, 10 May 2023 09:35:56 +0200 Subject: [PATCH 014/181] fetch_next_error command added. --- pymeasure/instruments/generic_types.py | 13 +++++++++++-- tests/instruments/test_generic_types.py | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index 81a3d3d2ad..4fced25e15 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -72,14 +72,23 @@ def reset(self): """ Resets the instrument. """ self.write("*RST") + def fetch_next_error(self): + """Fetch the next error in the error queue. + + If you want to read and log all errors, use :meth:`check_errors` instead. + + :return: Error entry. + """ + return self.values("SYST:ERR?") + def check_errors(self): """ Read all errors from the instrument. - :return: list of error entries + :return: List of error entries. """ errors = [] while True: - err = self.values("SYST:ERR?") + err = self.fetch_next_error() if int(err[0]) != 0: log.error(f"{self.name}: {err[0]}, {err[1]}") errors.append(err) diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py index b2e70715a3..bf1d2897da 100644 --- a/tests/instruments/test_generic_types.py +++ b/tests/instruments/test_generic_types.py @@ -60,6 +60,13 @@ def test_SCPI_write_commands(self, method, write): name="test") as instr: getattr(instr, method)() + def test_fetch_next_error(self): + with expected_protocol( + self.SCPIInstrument, + [("SYST:ERR?", '-100,"Command error"')], + name="test") as instr: + assert instr.fetch_next_error() == [-100, '"Command error"'] + def test_check_errors(self): with expected_protocol( self.SCPIInstrument, From bd2681cf41fb085a16f27fdc2ce0cfe099505474 Mon Sep 17 00:00:00 2001 From: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> Date: Thu, 11 May 2023 08:55:16 +0200 Subject: [PATCH 015/181] Changed method fetch_next_error to property next_error. --- pymeasure/instruments/generic_types.py | 20 ++++++------- tests/instruments/test_generic_types.py | 39 +++++++++++++------------ 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index 4fced25e15..4f3bb648e7 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -25,6 +25,8 @@ import logging from warnings import warn +from .instrument import Instrument + log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -62,6 +64,13 @@ def id(self): """ Get the identification of the instrument. """ return self.ask("*IDN?").strip() + next_error = Instrument.measurement( + "SYST:ERR?", + """Get the next error in the queue. + If you want to read and log all errors, use :meth:`check_errors` instead. + """, + ) + # SCPI default methods def clear(self): """ Clears the instrument status byte @@ -72,15 +81,6 @@ def reset(self): """ Resets the instrument. """ self.write("*RST") - def fetch_next_error(self): - """Fetch the next error in the error queue. - - If you want to read and log all errors, use :meth:`check_errors` instead. - - :return: Error entry. - """ - return self.values("SYST:ERR?") - def check_errors(self): """ Read all errors from the instrument. @@ -88,7 +88,7 @@ def check_errors(self): """ errors = [] while True: - err = self.fetch_next_error() + err = self.next_error if int(err[0]) != 0: log.error(f"{self.name}: {err[0]}, {err[1]}") errors.append(err) diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py index bf1d2897da..d59c67f674 100644 --- a/tests/instruments/test_generic_types.py +++ b/tests/instruments/test_generic_types.py @@ -38,17 +38,25 @@ def test_init(self): inst = self.SCPIInstrument(ProtocolAdapter(), "test") assert inst.SCPI is False # should not be set by the new init - @pytest.mark.parametrize("method, write, reply", (("id", "*IDN?", "xyz"), - ("complete", "*OPC?", "1"), - ("status", "*STB?", "189"), - ("options", "*OPT?", "a9"), - )) + @pytest.mark.parametrize("method, write, reply", ( + ("id", "*IDN?", "xyz"), + ("complete", "*OPC?", "1"), + ("status", "*STB?", "189"), + ("options", "*OPT?", "a9"), + )) def test_SCPI_properties(self, method, write, reply): with expected_protocol( self.SCPIInstrument, [(write, reply)], - name="test") as instr: - assert getattr(instr, method) == reply + name="test") as inst: + assert getattr(inst, method) == reply + + def test_next_error(self): + with expected_protocol( + self.SCPIInstrument, + [("SYST:ERR?", '-100,"Command error"')], + name="test") as inst: + assert inst.next_error == [-100, '"Command error"'] @pytest.mark.parametrize("method, write", (("clear", "*CLS"), ("reset", "*RST"), @@ -57,15 +65,8 @@ def test_SCPI_write_commands(self, method, write): with expected_protocol( self.SCPIInstrument, [(write, None)], - name="test") as instr: - getattr(instr, method)() - - def test_fetch_next_error(self): - with expected_protocol( - self.SCPIInstrument, - [("SYST:ERR?", '-100,"Command error"')], - name="test") as instr: - assert instr.fetch_next_error() == [-100, '"Command error"'] + name="test") as inst: + getattr(inst, method)() def test_check_errors(self): with expected_protocol( @@ -74,9 +75,9 @@ def test_check_errors(self): ("SYST:ERR?", '-222,"Data out of range"'), ("SYST:ERR?", '0,"No error"'), ], - name="test") as instr: - assert instr.check_errors() == [[-100, '"Command error"'], - [-222, '"Data out of range"']] + name="test") as inst: + assert inst.check_errors() == [[-100, '"Command error"'], + [-222, '"Data out of range"']] def test_SCPIunknownMixin(): From c1043db241a525edf507c70b71c87de90da0676b Mon Sep 17 00:00:00 2001 From: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> Date: Thu, 11 May 2023 09:56:03 +0200 Subject: [PATCH 016/181] Docstring test for generic types added. --- tests/instruments/test_all_instruments.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py index b3c9a12cf1..fc5c7cabaa 100644 --- a/tests/instruments/test_all_instruments.py +++ b/tests/instruments/test_all_instruments.py @@ -27,7 +27,7 @@ from unittest.mock import MagicMock from pymeasure import instruments -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, generic_types # Collect all instruments @@ -58,7 +58,16 @@ prop = getattr(device, property_name) if isinstance(prop, property): properties.append((device, property_name, prop)) - +for mixin in dir(generic_types): + if mixin in ("Instrument", "Channel", "CommonBase"): # exclucion list. + continue + elif mixin[0].isupper(): + # filter only classes + device = getattr(generic_types, mixin) + for property_name in dir(device): + prop = getattr(device, property_name) + if isinstance(prop, property): + properties.append((device, property_name, prop)) # Instruments unable to accept an Adapter instance. proper_adapters = [] From 21baca116f519900d76244beca6158724211311a Mon Sep 17 00:00:00 2001 From: bernhardlang <82966268+bernhardlang@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:53:34 +0200 Subject: [PATCH 017/181] Update contribute.rst according to request #980 --- docs/dev/contribute.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/dev/contribute.rst b/docs/dev/contribute.rst index 3b500fc140..f87934f25d 100644 --- a/docs/dev/contribute.rst +++ b/docs/dev/contribute.rst @@ -42,7 +42,10 @@ Install PyMeasure in the editable mode. pip install -e . This will allow you to edit the files of PyMeasure and see the changes reflected. Make sure to reset your notebook kernel or Python console when doing so. Now you have your own copy of the development version of PyMeasure installed! +Depending on your Python installation you may get an error messages saying that the file setup.py is missing or similar. Updating pip may solve the problem +.. code-block:: bash + python -m pip install pip --upgrade Working on a new feature ======================== From ac9f424e4f0b5eecfbfd827499c0e4e8818d1776 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:29:33 +0100 Subject: [PATCH 018/181] Change names to CamelCase for classes. --- CHANGES.rst | 2 +- pymeasure/instruments/generic_types.py | 6 +++--- pymeasure/instruments/instrument.py | 4 ++-- tests/instruments/test_generic_types.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4c541f45e5..946450f14e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Deprecated features ------------------- - Replaced :code:`directory_input` keyword-argument of :code:`ManagedWindowBase` by :code:`enable_file_input` (@CasperSchippers, #964) - Replaced :code:`celcius` attribute of :code:`LakeShoreTemperatureChannel` by :code:`celsius` (@afuetterer, #1003) -- Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPImixin` class if the device supports SCPI commands. +- Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPIMixin` class if the device supports SCPI commands. GUI --- diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index 4f3bb648e7..1fcdb3fb07 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -31,7 +31,7 @@ log.addHandler(logging.NullHandler()) -class SCPImixin: +class SCPIMixin: """Base class for SCPI instruments with the default implementations of SCPI commands.""" def __init__(self, *args, **kwargs): @@ -97,8 +97,8 @@ def check_errors(self): return errors -class SCPIunknownMixin(SCPImixin): - """Mixin which adds SCPI commands to an instrument from which it is not knwon, whether it +class SCPIUnknownMixin(SCPIMixin): + """Mixin which adds SCPI commands to an instrument from which it is not known, whether it supports SCPI commands or not. """ diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py index 6d37486a9e..f9973252a2 100644 --- a/pymeasure/instruments/instrument.py +++ b/pymeasure/instruments/instrument.py @@ -61,8 +61,8 @@ class Instrument(CommonBase): :param string name: The name of the instrument. Often the model designation by default. :param includeSCPI: A boolean, which toggles the inclusion of standard SCPI commands - .. deprecated:: 0.12 - Inherit the :class:`~pymeasure.instruments.generic_types.SCPImixin` class instead. + .. deprecated:: 0.14 + Inherit the :class:`~pymeasure.instruments.generic_types.SCPIMixin` class instead. :param preprocess_reply: An optional callable used to preprocess strings received from the instrument. The callable returns the diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py index d59c67f674..ab9ac19f01 100644 --- a/tests/instruments/test_generic_types.py +++ b/tests/instruments/test_generic_types.py @@ -26,12 +26,12 @@ from pymeasure.test import expected_protocol from pymeasure.adapters import ProtocolAdapter -from pymeasure.instruments.generic_types import SCPImixin, SCPIunknownMixin +from pymeasure.instruments.generic_types import SCPIMixin, SCPIUnknownMixin from pymeasure.instruments import Instrument class Test_SCPIMixin: - class SCPIInstrument(SCPImixin, Instrument): + class SCPIInstrument(SCPIMixin, Instrument): pass def test_init(self): @@ -81,7 +81,7 @@ def test_check_errors(self): def test_SCPIunknownMixin(): - class SCPIunknownInstrument(SCPIunknownMixin, Instrument): + class SCPIunknownInstrument(SCPIUnknownMixin, Instrument): pass with pytest.warns(FutureWarning): From 8e7329908f693362429335a9b7ac906415b8f4a3 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:30:25 +0100 Subject: [PATCH 019/181] Differentiate deprecation warnings. --- pymeasure/instruments/generic_types.py | 1 - pymeasure/instruments/instrument.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index 1fcdb3fb07..a29a1f6cb3 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -103,7 +103,6 @@ class SCPIUnknownMixin(SCPIMixin): """ def __init__(self, *args, **kwargs): - kwargs.setdefault("includeSCPI", False) # in order not to trigger the deprecation warning warn("It is not known, whether this device support SCPI commands or not. Please inform " "the pymeasure maintainers, if you know the answer.", FutureWarning) super().__init__(*args, **kwargs) diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py index f9973252a2..1f932db0d2 100644 --- a/pymeasure/instruments/instrument.py +++ b/pymeasure/instruments/instrument.py @@ -76,7 +76,7 @@ class Instrument(CommonBase): """ # noinspection PyPep8Naming - def __init__(self, adapter, name, includeSCPI=True, + def __init__(self, adapter, name, includeSCPI=None, preprocess_reply=None, **kwargs): # Setup communication before possible children require the adapter. @@ -87,10 +87,14 @@ def __init__(self, adapter, name, includeSCPI=True, raise Exception("Invalid Adapter provided for Instrument since" " PyVISA is not present") self.adapter = adapter - self.SCPI = includeSCPI - if includeSCPI: + if includeSCPI is True: warn("Defining SCPI base functionality with `includeSCPI=True` is deprecated, inherit " - "the `SCPImixin` class instead.", FutureWarning) + "the `SCPIMixin` class instead.", FutureWarning) + elif includeSCPI is None: + warn("Not defining SCPI base functionalitiy is deprecated, use " + "`includeSCPI=False` or inherit the `SCPIMixin` class instead.", FutureWarning) + includeSCPI = True + self.SCPI = includeSCPI self.isShutdown = False self.name = name From 0f6f2cca923696e89b4978a2afc6a34593069a64 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:25:57 +0100 Subject: [PATCH 020/181] Import generic instruments into `instruments`. --- pymeasure/instruments/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymeasure/instruments/__init__.py b/pymeasure/instruments/__init__.py index cef22004da..173fb269fe 100644 --- a/pymeasure/instruments/__init__.py +++ b/pymeasure/instruments/__init__.py @@ -27,6 +27,7 @@ from .instrument import Instrument from .resources import find_serial_port, list_resources from .validators import discreteTruncate +from .generic_types import SCPIMixin, SCPIUnknownMixin from . import activetechnologies from . import advantest From 58ae7677006fc69ad4fb9ec4fd345706d02b30c6 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:20:48 +0100 Subject: [PATCH 021/181] Adjust test to changed text. --- tests/instruments/test_instrument.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/instruments/test_instrument.py b/tests/instruments/test_instrument.py index 7941f8c0c6..5663e7726f 100644 --- a/tests/instruments/test_instrument.py +++ b/tests/instruments/test_instrument.py @@ -109,7 +109,8 @@ def test_not_defined_includeSCPI_raises_warning(self): with pytest.warns(FutureWarning) as record: Instrument(name="test", adapter=ProtocolAdapter()) msg = str(record[0].message) - assert msg == "It is deprecated to specify `includeSCPI` implicitly, declare it explicitly." + assert msg == ("It is deprecated to specify `includeSCPI` implicitly, use " + "`includeSCPI=False` or inherit the `SCPIMixin` class instead.") def test_not_defined_includeSCPI_is_interpreted_as_true(self): inst = Instrument(name="test", adapter=ProtocolAdapter()) From 31fd40c364598b56ff788785c9bc46041d3f0bd1 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:28:30 +0100 Subject: [PATCH 022/181] Add Mixins to instruments "a". --- .../instruments/activetechnologies/AWG401x.py | 4 ++-- .../instruments/advantest/advantestR3767CG.py | 4 ++-- .../instruments/advantest/advantestR624X.py | 4 ++-- pymeasure/instruments/agilent/agilent33220A.py | 4 ++-- pymeasure/instruments/agilent/agilent33500.py | 4 ++-- pymeasure/instruments/agilent/agilent34410A.py | 4 ++-- pymeasure/instruments/agilent/agilent34450A.py | 6 +++--- pymeasure/instruments/agilent/agilent4156.py | 4 ++-- pymeasure/instruments/agilent/agilent8257D.py | 12 ++++++------ pymeasure/instruments/agilent/agilent8722ES.py | 4 ++-- pymeasure/instruments/agilent/agilentB1500.py | 14 +++++++------- pymeasure/instruments/agilent/agilentE4408B.py | 18 +++++++++--------- pymeasure/instruments/agilent/agilentE4980.py | 6 +++--- pymeasure/instruments/aimtti/aimttiPL.py | 4 ++-- pymeasure/instruments/ametek/ametek7270.py | 4 ++-- pymeasure/instruments/ami/ami430.py | 4 ++-- pymeasure/instruments/anapico/apsin12G.py | 4 ++-- .../instruments/anritsu/anritsuMG3692C.py | 4 ++-- .../instruments/anritsu/anritsuMS2090A.py | 4 ++-- .../instruments/anritsu/anritsuMS464xB.py | 4 ++-- .../instruments/anritsu/anritsuMS9710C.py | 4 ++-- 21 files changed, 60 insertions(+), 60 deletions(-) diff --git a/pymeasure/instruments/activetechnologies/AWG401x.py b/pymeasure/instruments/activetechnologies/AWG401x.py index 0565130351..87773a2a62 100644 --- a/pymeasure/instruments/activetechnologies/AWG401x.py +++ b/pymeasure/instruments/activetechnologies/AWG401x.py @@ -31,7 +31,7 @@ import pprint -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, \ strict_range @@ -349,7 +349,7 @@ class ChannelAWG(ChannelBase): ) -class AWG401x_base(Instrument): +class AWG401x_base(SCPIUnknownMixin, Instrument): """AWG-401x base class""" def __init__(self, adapter, diff --git a/pymeasure/instruments/advantest/advantestR3767CG.py b/pymeasure/instruments/advantest/advantestR3767CG.py index 636883c825..54083ff8a9 100644 --- a/pymeasure/instruments/advantest/advantestR3767CG.py +++ b/pymeasure/instruments/advantest/advantestR3767CG.py @@ -22,11 +22,11 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range -class AdvantestR3767CG(Instrument): +class AdvantestR3767CG(SCPIUnknownMixin, Instrument): """ Represents the Advantest R3767CG VNA. Implements controls to change the analysis range and to retrieve the data for the trace. """ diff --git a/pymeasure/instruments/advantest/advantestR624X.py b/pymeasure/instruments/advantest/advantestR624X.py index 71c07b84d2..4e3a9d705a 100644 --- a/pymeasure/instruments/advantest/advantestR624X.py +++ b/pymeasure/instruments/advantest/advantestR624X.py @@ -23,7 +23,7 @@ import logging from enum import IntEnum, IntFlag -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set, \ strict_range @@ -429,7 +429,7 @@ def map_values(value, values): return values[strict_discrete_set(value, values)] -class AdvantestR624X(Instrument): +class AdvantestR624X(SCPIUnknownMixin, Instrument): """ Represents the Advantest R624X series (channel A and B) SourceMeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/agilent/agilent33220A.py b/pymeasure/instruments/agilent/agilent33220A.py index 51d1976f95..7d5c8da477 100644 --- a/pymeasure/instruments/agilent/agilent33220A.py +++ b/pymeasure/instruments/agilent/agilent33220A.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set,\ strict_range, joined_validators from time import time @@ -42,7 +42,7 @@ def capitalize_string(string: str, *args, **kwargs): string_validator = joined_validators(capitalize_string, strict_discrete_set) -class Agilent33220A(Instrument): +class Agilent33220A(SCPIUnknownMixin, Instrument): """Represents the Agilent 33220A Arbitrary Waveform Generator. .. code-block:: python diff --git a/pymeasure/instruments/agilent/agilent33500.py b/pymeasure/instruments/agilent/agilent33500.py index 9540e9d10a..b8729011b2 100644 --- a/pymeasure/instruments/agilent/agilent33500.py +++ b/pymeasure/instruments/agilent/agilent33500.py @@ -25,7 +25,7 @@ # Parts of this code were copied and adapted from the Agilent33220A class. import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range from time import time from pyvisa.errors import VisaIOError @@ -356,7 +356,7 @@ def data_arb(self, arb_name, data_points, data_format="DAC"): ) -class Agilent33500(Instrument): +class Agilent33500(SCPIUnknownMixin, Instrument): """ Represents the Agilent 33500 Function/Arbitrary Waveform Generator family. diff --git a/pymeasure/instruments/agilent/agilent34410A.py b/pymeasure/instruments/agilent/agilent34410A.py index e50a768e27..3f4bffe385 100644 --- a/pymeasure/instruments/agilent/agilent34410A.py +++ b/pymeasure/instruments/agilent/agilent34410A.py @@ -22,10 +22,10 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin -class Agilent34410A(Instrument): +class Agilent34410A(SCPIUnknownMixin, Instrument): """ Represent the HP/Agilent/Keysight 34410A and related multimeters. diff --git a/pymeasure/instruments/agilent/agilent34450A.py b/pymeasure/instruments/agilent/agilent34450A.py index daa21e6a33..8d3b4cd589 100644 --- a/pymeasure/instruments/agilent/agilent34450A.py +++ b/pymeasure/instruments/agilent/agilent34450A.py @@ -25,14 +25,14 @@ import re import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -class Agilent34450A(Instrument): +class Agilent34450A(SCPIUnknownMixin, Instrument): """ Represent the HP/Agilent/Keysight 34450A and related multimeters. @@ -458,7 +458,7 @@ def configure_resistance(self, resistance_range="AUTO", wires=2, resolution="DEF self.resistance_4w_range = resistance_range else: raise ValueError("Incorrect wires value, Agilent 34450A only supports 2 or 4 wire" - "resistance meaurement.") + "resistance measurement.") def configure_frequency(self, measured_from="voltage_ac", measured_from_range="AUTO", aperture="DEF"): diff --git a/pymeasure/instruments/agilent/agilent4156.py b/pymeasure/instruments/agilent/agilent4156.py index d4283b23f1..064efb62b0 100644 --- a/pymeasure/instruments/agilent/agilent4156.py +++ b/pymeasure/instruments/agilent/agilent4156.py @@ -28,7 +28,7 @@ import numpy as np import pandas as pd -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import (strict_discrete_set, truncated_discrete_set, strict_range) @@ -42,7 +42,7 @@ ###### -class Agilent4156(Instrument): +class Agilent4156(SCPIUnknownMixin, Instrument): """ Represents the Agilent 4155/4156 Semiconductor Parameter Analyzer and provides a high-level interface for taking current-voltage (I-V) measurements. diff --git a/pymeasure/instruments/agilent/agilent8257D.py b/pymeasure/instruments/agilent/agilent8257D.py index 257ded7e47..f87f9a6641 100644 --- a/pymeasure/instruments/agilent/agilent8257D.py +++ b/pymeasure/instruments/agilent/agilent8257D.py @@ -22,11 +22,11 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set -class Agilent8257D(Instrument): +class Agilent8257D(SCPIUnknownMixin, Instrument): """Represents the Agilent 8257D Signal Generator and provides a high-level interface for interacting with the instrument. @@ -119,7 +119,7 @@ class Agilent8257D(Instrument): amplitude_depth = Instrument.control( ":SOUR:AM:DEPT?", ":SOUR:AM:DEPT %g", """ A floating point property that controls the amplitude modulation - in precent, which can take values from 0 to 100 %. """, + in percent, which can take values from 0 to 100 %. """, validator=truncated_range, values=[0, 100] ) @@ -199,8 +199,8 @@ class Agilent8257D(Instrument): low_freq_out_source = Instrument.control( ":SOUR:LFO:SOUR?", ":SOUR:LFO:SOUR %s", """A string property which controls the source of the low frequency output, which - can take the values 'internal [2]' for the inernal source, or 'function [2]' for an internal - function generator which can be configured.""", + can take the values 'internal [2]' for the internal source, or 'function [2]' for an + internal function generator which can be configured.""", validator=strict_discrete_set, values=LOW_FREQUENCY_SOURCES, map_values=True @@ -275,7 +275,7 @@ def config_amplitude_modulation(self, frequency=1e3, depth=100.0, shape='sine'): """ Configures the amplitude modulation of the output signal. :param frequency: A modulation frequency for the internal oscillator - :param depth: A linear depth precentage + :param depth: A linear depth percentage :param shape: A string that describes the shape for the internal oscillator """ self.enable_amplitude_modulation() diff --git a/pymeasure/instruments/agilent/agilent8722ES.py b/pymeasure/instruments/agilent/agilent8722ES.py index 4f258a62f7..072a5e0c9a 100644 --- a/pymeasure/instruments/agilent/agilent8722ES.py +++ b/pymeasure/instruments/agilent/agilent8722ES.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument, discreteTruncate, RangeException +from pymeasure.instruments import Instrument, SCPIUnknownMixin, discreteTruncate, RangeException from pyvisa import VisaIOError import numpy as np @@ -31,7 +31,7 @@ import warnings -class Agilent8722ES(Instrument): +class Agilent8722ES(SCPIUnknownMixin, Instrument): """ Represents the Agilent8722ES Vector Network Analyzer and provides a high-level interface for taking scans of the scattering parameters. diff --git a/pymeasure/instruments/agilent/agilentB1500.py b/pymeasure/instruments/agilent/agilentB1500.py index a0273f3853..8afff56c3e 100644 --- a/pymeasure/instruments/agilent/agilentB1500.py +++ b/pymeasure/instruments/agilent/agilentB1500.py @@ -33,7 +33,7 @@ from pymeasure.instruments.validators import (strict_discrete_set, strict_range, strict_discrete_range) -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -43,7 +43,7 @@ ###################################### -class AgilentB1500(Instrument): +class AgilentB1500(SCPIUnknownMixin, Instrument): """ Represents the Agilent B1500 Semiconductor Parameter Analyzer and provides a high-level interface for taking different kinds of measurements. @@ -124,7 +124,7 @@ def query_modules(self): # i+1: channels start at 1 not at 0 except Exception: raise NotImplementedError( - f'Module {module[0]} is not implented yet!') + f'Module {module[0]} is not implemented yet!') return out def initialize_smu(self, channel, smu_type, name): @@ -165,7 +165,7 @@ def initialize_all_smus(self): i += 1 def pause(self, pause_seconds): - """ Pauses Command Excecution for given time in seconds (``PA``) + """ Pauses Command Execution for given time in seconds (``PA``) :param pause_seconds: Seconds to pause :type pause_seconds: int @@ -598,7 +598,7 @@ def meas_mode(self, mode, *args): # ADC Setup: AAD, AIT, AV, AZ def query_adc_setup(self): - """Read ADC settings (55, 56) from the intrument. + """Read ADC settings (55, 56) from the instrument. """ return {**self.query_learn_header(55), **self.query_learn_header(56)} @@ -652,7 +652,7 @@ def adc_averaging(self, number, mode='Auto'): @property def adc_auto_zero(self): - """ Enable/Disable ADC zero function. Halfs the + """ Enable/Disable ADC zero function. Halves the integration time, if off. (``AZ``) :type: bool @@ -1074,7 +1074,7 @@ def force(self, source_type, source_range, output, comp='', :param source_range: Output range index or name :type source_range: int or str :param output: Source output value in A or V - :type outout: float + :type output: float :param comp: Compliance value, defaults to previous setting :type comp: float, optional :param comp_polarity: Compliance polairty, defaults to auto diff --git a/pymeasure/instruments/agilent/agilentE4408B.py b/pymeasure/instruments/agilent/agilentE4408B.py index 914c61d435..83d69aedc8 100644 --- a/pymeasure/instruments/agilent/agilentE4408B.py +++ b/pymeasure/instruments/agilent/agilentE4408B.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range from io import StringIO @@ -30,12 +30,19 @@ import pandas as pd -class AgilentE4408B(Instrument): +class AgilentE4408B(SCPIUnknownMixin, Instrument): """ Represents the AgilentE4408B Spectrum Analyzer and provides a high-level interface for taking scans of high-frequency spectrums """ + def __init__(self, adapter, name="Agilent E4408B Spectrum Analyzer", **kwargs): + super().__init__( + adapter, + name, + **kwargs + ) + start_frequency = Instrument.control( ":SENS:FREQ:STAR?;", ":SENS:FREQ:STAR %e Hz;", """ A floating point property that represents the start frequency @@ -76,13 +83,6 @@ class AgilentE4408B(Instrument): """ ) - def __init__(self, adapter, name="Agilent E4408B Spectrum Analyzer", **kwargs): - super().__init__( - adapter, - name, - **kwargs - ) - @property def frequencies(self): """ Returns a numpy array of frequencies in Hz that diff --git a/pymeasure/instruments/agilent/agilentE4980.py b/pymeasure/instruments/agilent/agilentE4980.py index 2d9d2ce6ba..e8d8018456 100644 --- a/pymeasure/instruments/agilent/agilentE4980.py +++ b/pymeasure/instruments/agilent/agilentE4980.py @@ -23,12 +23,12 @@ # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range from pyvisa.errors import VisaIOError -class AgilentE4980(Instrument): +class AgilentE4980(SCPIUnknownMixin, Instrument): """Represents LCR meter E4980A/AL""" ac_voltage = Instrument.control(":VOLT:LEV?", ":VOLT:LEV %g", @@ -74,7 +74,7 @@ class AgilentE4980(Instrument): - LSQ: Seriesinductance [H] and quality factor [number] - LSRS: Series inductance [H] and series resistance [Ohm] - * RX: Resitance [Ohm] and reactance [Ohm] + * RX: Resistance [Ohm] and reactance [Ohm] * ZTD: Impedance, magnitude [Ohm] and phase [deg] * ZTR: Impedance, magnitude [Ohm] and phase [rad] * GB: Conductance [S] and susceptance [S] diff --git a/pymeasure/instruments/aimtti/aimttiPL.py b/pymeasure/instruments/aimtti/aimttiPL.py index 7a96d63116..aad9a4af85 100644 --- a/pymeasure/instruments/aimtti/aimttiPL.py +++ b/pymeasure/instruments/aimtti/aimttiPL.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range @@ -88,7 +88,7 @@ def __init__(self, parent, id, voltage_range: list = None, current_range: list = ) -class PLBase(Instrument): +class PLBase(SCPIUnknownMixin, Instrument): """Control AimTTI PL series power supplies. Model number ending with -P or P(G) support this remote interface. diff --git a/pymeasure/instruments/ametek/ametek7270.py b/pymeasure/instruments/ametek/ametek7270.py index e2b4bc8684..33693aaaea 100644 --- a/pymeasure/instruments/ametek/ametek7270.py +++ b/pymeasure/instruments/ametek/ametek7270.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import modular_range, truncated_discrete_set, truncated_range import logging @@ -41,7 +41,7 @@ def check_read_not_empty(value): return value -class Ametek7270(Instrument): +class Ametek7270(SCPIUnknownMixin, Instrument): """This is the class for the Ametek DSP 7270 lockin amplifier In this instrument, some measurements are defined only for specific modes, diff --git a/pymeasure/instruments/ami/ami430.py b/pymeasure/instruments/ami/ami430.py index 17c2ac527c..c50bd55e59 100644 --- a/pymeasure/instruments/ami/ami430.py +++ b/pymeasure/instruments/ami/ami430.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIMixin from time import sleep, time import logging @@ -30,7 +30,7 @@ log.addHandler(logging.NullHandler()) -class AMI430(Instrument): +class AMI430(SCPIMixin, Instrument): """ Represents the AMI 430 Power supply and provides a high-level for interacting with the instrument. diff --git a/pymeasure/instruments/anapico/apsin12G.py b/pymeasure/instruments/anapico/apsin12G.py index 4ec055bd7c..a011c04b3d 100644 --- a/pymeasure/instruments/anapico/apsin12G.py +++ b/pymeasure/instruments/anapico/apsin12G.py @@ -22,11 +22,11 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set -class APSIN12G(Instrument): +class APSIN12G(SCPIUnknownMixin, Instrument): """ Represents the Anapico APSIN12G Signal Generator with option 9K, HP and GPIB. """ FREQ_LIMIT = [9e3, 12e9] diff --git a/pymeasure/instruments/anritsu/anritsuMG3692C.py b/pymeasure/instruments/anritsu/anritsuMG3692C.py index e9a465fda8..9c9302ae00 100644 --- a/pymeasure/instruments/anritsu/anritsuMG3692C.py +++ b/pymeasure/instruments/anritsu/anritsuMG3692C.py @@ -22,10 +22,10 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin -class AnritsuMG3692C(Instrument): +class AnritsuMG3692C(SCPIUnknownMixin, Instrument): """ Represents the Anritsu MG3692C Signal Generator """ power = Instrument.control( diff --git a/pymeasure/instruments/anritsu/anritsuMS2090A.py b/pymeasure/instruments/anritsu/anritsuMS2090A.py index 43eb3a7608..866eb58fb0 100644 --- a/pymeasure/instruments/anritsu/anritsuMS2090A.py +++ b/pymeasure/instruments/anritsu/anritsuMS2090A.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, truncated_range, @@ -33,7 +33,7 @@ log.addHandler(logging.NullHandler()) -class AnritsuMS2090A(Instrument): +class AnritsuMS2090A(SCPIUnknownMixin, Instrument): """Anritsu MS2090A Handheld Spectrum Analyzer.""" def __init__(self, adapter, name="Anritsu MS2090A Handheld Spectrum Analyzer", **kwargs): diff --git a/pymeasure/instruments/anritsu/anritsuMS464xB.py b/pymeasure/instruments/anritsu/anritsuMS464xB.py index 6e7c5c21d2..79bd5b816f 100644 --- a/pymeasure/instruments/anritsu/anritsuMS464xB.py +++ b/pymeasure/instruments/anritsu/anritsuMS464xB.py @@ -23,7 +23,7 @@ # import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, strict_range @@ -33,7 +33,7 @@ log.addHandler(logging.NullHandler()) -class AnritsuMS464xB(Instrument): +class AnritsuMS464xB(SCPIUnknownMixin, Instrument): """ A class representing the Anritsu MS464xB Vector Network Analyzer (VNA) series. This family consists of the MS4642B, MS4644B, MS4645B, and MS4647B, which are represented in diff --git a/pymeasure/instruments/anritsu/anritsuMS9710C.py b/pymeasure/instruments/anritsu/anritsuMS9710C.py index aeb38f4a63..6ce0e0c984 100644 --- a/pymeasure/instruments/anritsu/anritsuMS9710C.py +++ b/pymeasure/instruments/anritsu/anritsuMS9710C.py @@ -24,7 +24,7 @@ import logging from time import sleep import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, truncated_discrete_set, @@ -64,7 +64,7 @@ def _parse_trace_peak(vals): return res -class AnritsuMS9710C(Instrument): +class AnritsuMS9710C(SCPIUnknownMixin, Instrument): """Anritsu MS9710C Optical Spectrum Analyzer.""" def __init__(self, adapter, name="Anritsu MS9710C Optical Spectrum Analyzer", **kwargs): From 6fdd2ea18820959dcf3151b79efd9f6c5c332a09 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:16:39 +0100 Subject: [PATCH 023/181] Add SCPIMixin to instruments from b to m. --- pymeasure/instruments/bkprecision/bkprecision9130b.py | 4 ++-- pymeasure/instruments/deltaelektronika/sm7045d.py | 4 ++-- pymeasure/instruments/fwbell/fwbell5080.py | 5 ++--- pymeasure/instruments/hcp/tc038.py | 6 ++++-- pymeasure/instruments/hp/hp33120A.py | 4 ++-- pymeasure/instruments/hp/hp34401A.py | 4 ++-- pymeasure/instruments/keithley/keithley2000.py | 4 ++-- pymeasure/instruments/keithley/keithley2200.py | 4 ++-- pymeasure/instruments/keithley/keithley2260B.py | 4 ++-- pymeasure/instruments/keithley/keithley2306.py | 4 ++-- pymeasure/instruments/keithley/keithley2400.py | 4 ++-- pymeasure/instruments/keithley/keithley2450.py | 4 ++-- pymeasure/instruments/keithley/keithley2600.py | 4 ++-- pymeasure/instruments/keithley/keithley2700.py | 4 ++-- pymeasure/instruments/keithley/keithley2750.py | 4 ++-- pymeasure/instruments/keithley/keithley6221.py | 4 ++-- pymeasure/instruments/keithley/keithley6517b.py | 4 ++-- pymeasure/instruments/keithley/keithleyDMM6500.py | 4 ++-- pymeasure/instruments/keysight/keysightDSOX1102G.py | 4 ++-- pymeasure/instruments/keysight/keysightE36312A.py | 4 ++-- pymeasure/instruments/keysight/keysightE3631A.py | 7 ++++--- pymeasure/instruments/keysight/keysightN5767A.py | 4 ++-- pymeasure/instruments/keysight/keysightN7776C.py | 4 ++-- pymeasure/instruments/lakeshore/lakeshore211.py | 4 ++-- pymeasure/instruments/lakeshore/lakeshore224.py | 4 ++-- pymeasure/instruments/lakeshore/lakeshore331.py | 4 ++-- pymeasure/instruments/lakeshore/lakeshore421.py | 4 ++-- pymeasure/instruments/lakeshore/lakeshore425.py | 4 ++-- pymeasure/instruments/teledyne/teledyne_oscilloscope.py | 4 ++-- 29 files changed, 62 insertions(+), 60 deletions(-) diff --git a/pymeasure/instruments/bkprecision/bkprecision9130b.py b/pymeasure/instruments/bkprecision/bkprecision9130b.py index b9f87a4425..9a7b97d607 100644 --- a/pymeasure/instruments/bkprecision/bkprecision9130b.py +++ b/pymeasure/instruments/bkprecision/bkprecision9130b.py @@ -22,13 +22,13 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, truncated_range CHANNEL_NUMS = [1, 2, 3] -class BKPrecision9130B(Instrument): +class BKPrecision9130B(SCPIUnknownMixin, Instrument): """ Represents the BK Precision 9130B DC Power Supply interface for interacting with the instrument. """ diff --git a/pymeasure/instruments/deltaelektronika/sm7045d.py b/pymeasure/instruments/deltaelektronika/sm7045d.py index 71447e2ea4..55b501b819 100644 --- a/pymeasure/instruments/deltaelektronika/sm7045d.py +++ b/pymeasure/instruments/deltaelektronika/sm7045d.py @@ -22,14 +22,14 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range from time import sleep from numpy import linspace -class SM7045D(Instrument): +class SM7045D(SCPIUnknownMixin, Instrument): """ This is the class for the SM 70-45 D power supply. .. code-block:: python diff --git a/pymeasure/instruments/fwbell/fwbell5080.py b/pymeasure/instruments/fwbell/fwbell5080.py index bc5ba7c7e1..81d4763af8 100644 --- a/pymeasure/instruments/fwbell/fwbell5080.py +++ b/pymeasure/instruments/fwbell/fwbell5080.py @@ -22,13 +22,13 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set from numpy import array, float64 from time import sleep -class FWBell5080(Instrument): +class FWBell5080(SCPIMixin, Instrument): """ Represents the F.W. Bell 5080 Handheld Gaussmeter and provides a high-level interface for interacting with the instrument @@ -54,7 +54,6 @@ def __init__(self, adapter, name="F.W. Bell 5080 Handheld Gaussmeter", **kwargs) super().__init__( adapter, name, - includeSCPI=True, **kwargs ) diff --git a/pymeasure/instruments/hcp/tc038.py b/pymeasure/instruments/hcp/tc038.py index 74b3f992bf..8d64b3729c 100644 --- a/pymeasure/instruments/hcp/tc038.py +++ b/pymeasure/instruments/hcp/tc038.py @@ -83,11 +83,13 @@ class TC038(Instrument): """ def __init__(self, adapter, name="TC038", address=1, timeout=1000, - includeSCPI=False, **kwargs): + **kwargs): super().__init__(adapter, name, timeout=timeout, write_termination="\r", read_termination="\r", - parity=Parity.even, **kwargs) + parity=Parity.even, + includeSCPI=False, + **kwargs) self.address = address self.set_monitored_quantity() # start to monitor the temperature diff --git a/pymeasure/instruments/hp/hp33120A.py b/pymeasure/instruments/hp/hp33120A.py index b09efdee4f..f63356ad0b 100644 --- a/pymeasure/instruments/hp/hp33120A.py +++ b/pymeasure/instruments/hp/hp33120A.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set import logging @@ -30,7 +30,7 @@ log.addHandler(logging.NullHandler()) -class HP33120A(Instrument): +class HP33120A(SCPIUnknownMixin, Instrument): """ Represents the Hewlett Packard 33120A Arbitrary Waveform Generator and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/hp/hp34401A.py b/pymeasure/instruments/hp/hp34401A.py index f167dd2046..f605c0cf29 100644 --- a/pymeasure/instruments/hp/hp34401A.py +++ b/pymeasure/instruments/hp/hp34401A.py @@ -23,7 +23,7 @@ # from warnings import warn -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set @@ -41,7 +41,7 @@ def func(x): return func -class HP34401A(Instrument): +class HP34401A(SCPIUnknownMixin, Instrument): """ Represents the HP / Agilent / Keysight 34401A Multimeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2000.py b/pymeasure/instruments/keithley/keithley2000.py index f467f6e78e..6c008c78d6 100644 --- a/pymeasure/instruments/keithley/keithley2000.py +++ b/pymeasure/instruments/keithley/keithley2000.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( truncated_range, truncated_discrete_set, strict_discrete_set @@ -35,7 +35,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2000(KeithleyBuffer, Instrument): +class Keithley2000(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ Represents the Keithley 2000 Multimeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2200.py b/pymeasure/instruments/keithley/keithley2200.py index b1a61c3102..75be0dcdbf 100644 --- a/pymeasure/instruments/keithley/keithley2200.py +++ b/pymeasure/instruments/keithley/keithley2200.py @@ -23,7 +23,7 @@ # import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) @@ -97,7 +97,7 @@ def insert_id(self, command): return f"INST:SEL CH{self.id};{command}" -class Keithley2200(Instrument): +class Keithley2200(SCPIUnknownMixin, Instrument): """Represents the Keithley 2200 Power Supply.""" def __init__(self, adapter, name="Keithley2200", **kwargs): diff --git a/pymeasure/instruments/keithley/keithley2260B.py b/pymeasure/instruments/keithley/keithley2260B.py index 969fc3e649..19c67bf76e 100644 --- a/pymeasure/instruments/keithley/keithley2260B.py +++ b/pymeasure/instruments/keithley/keithley2260B.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set import logging @@ -32,7 +32,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2260B(Instrument): +class Keithley2260B(SCPIUnknownMixin, Instrument): """ Represents the Keithley 2260B Power Supply (minimal implementation) and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2306.py b/pymeasure/instruments/keithley/keithley2306.py index a964177dd3..7cf1ade750 100644 --- a/pymeasure/instruments/keithley/keithley2306.py +++ b/pymeasure/instruments/keithley/keithley2306.py @@ -23,7 +23,7 @@ # import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set log = logging.getLogger(__name__) @@ -568,7 +568,7 @@ class Relay(Channel): ) -class Keithley2306(Instrument): +class Keithley2306(SCPIUnknownMixin, Instrument): """ Represents the Keithley 2306 Dual Channel Battery/Charger Simulator. """ diff --git a/pymeasure/instruments/keithley/keithley2400.py b/pymeasure/instruments/keithley/keithley2400.py index 746ceae38e..a9c3f61dae 100644 --- a/pymeasure/instruments/keithley/keithley2400.py +++ b/pymeasure/instruments/keithley/keithley2400.py @@ -28,7 +28,7 @@ import numpy as np -from pymeasure.instruments import Instrument, RangeException +from pymeasure.instruments import Instrument, RangeException, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer @@ -38,7 +38,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2400(KeithleyBuffer, Instrument): +class Keithley2400(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ Represents the Keithley 2400 SourceMeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2450.py b/pymeasure/instruments/keithley/keithley2450.py index 3e151adab6..376db37a05 100644 --- a/pymeasure/instruments/keithley/keithley2450.py +++ b/pymeasure/instruments/keithley/keithley2450.py @@ -28,7 +28,7 @@ import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer @@ -37,7 +37,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2450(KeithleyBuffer, Instrument): +class Keithley2450(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ Represents the Keithley 2450 SourceMeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2600.py b/pymeasure/instruments/keithley/keithley2600.py index 7916641d08..1d2dbfd54e 100644 --- a/pymeasure/instruments/keithley/keithley2600.py +++ b/pymeasure/instruments/keithley/keithley2600.py @@ -26,7 +26,7 @@ from warnings import warn import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set # Setup logging @@ -34,7 +34,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2600(Instrument): +class Keithley2600(SCPIUnknownMixin, Instrument): """Represents the Keithley 2600 series (channel A and B) SourceMeter""" def __init__(self, adapter, name="Keithley 2600 SourceMeter", **kwargs): diff --git a/pymeasure/instruments/keithley/keithley2700.py b/pymeasure/instruments/keithley/keithley2700.py index 28f8254a67..c0ab44c7a5 100644 --- a/pymeasure/instruments/keithley/keithley2700.py +++ b/pymeasure/instruments/keithley/keithley2700.py @@ -25,7 +25,7 @@ import logging from warnings import warn -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from .buffer import KeithleyBuffer @@ -87,7 +87,7 @@ def text_length_validator(value, values): return value[:values] -class Keithley2700(KeithleyBuffer, Instrument): +class Keithley2700(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ Represents the Keithley 2700 Multimeter/Switch System and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2750.py b/pymeasure/instruments/keithley/keithley2750.py index 08eada2810..27e15eefb9 100644 --- a/pymeasure/instruments/keithley/keithley2750.py +++ b/pymeasure/instruments/keithley/keithley2750.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin def clean_closed_channels(output): @@ -50,7 +50,7 @@ def clean_closed_channels(output): raise ValueError("`output` must be a string or list.") -class Keithley2750(Instrument): +class Keithley2750(SCPIUnknownMixin, Instrument): """ Represents the Keithley2750 multimeter/switch system and provides a high-level interface for interacting with the instrument. """ diff --git a/pymeasure/instruments/keithley/keithley6221.py b/pymeasure/instruments/keithley/keithley6221.py index 6fcd80553f..dcbee525e0 100644 --- a/pymeasure/instruments/keithley/keithley6221.py +++ b/pymeasure/instruments/keithley/keithley6221.py @@ -28,7 +28,7 @@ import numpy as np -from pymeasure.instruments import Instrument, RangeException +from pymeasure.instruments import Instrument, RangeException, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer @@ -37,7 +37,7 @@ log.addHandler(logging.NullHandler()) -class Keithley6221(KeithleyBuffer, Instrument): +class Keithley6221(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ Represents the Keithley 6221 AC and DC current source and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley6517b.py b/pymeasure/instruments/keithley/keithley6517b.py index 5beae98395..b566a1cbce 100644 --- a/pymeasure/instruments/keithley/keithley6517b.py +++ b/pymeasure/instruments/keithley/keithley6517b.py @@ -29,7 +29,7 @@ import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range from .buffer import KeithleyBuffer @@ -37,7 +37,7 @@ log.addHandler(logging.NullHandler()) -class Keithley6517B(KeithleyBuffer, Instrument): +class Keithley6517B(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ Represents the Keithley 6517B ElectroMeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithleyDMM6500.py b/pymeasure/instruments/keithley/keithleyDMM6500.py index b9795fef6e..d9fab89b3b 100644 --- a/pymeasure/instruments/keithley/keithleyDMM6500.py +++ b/pymeasure/instruments/keithley/keithleyDMM6500.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIMixin from pymeasure.instruments.validators import ( truncated_range, truncated_discrete_set, @@ -153,7 +153,7 @@ def write(self, command): super().write(command) -class KeithleyDMM6500(Instrument): +class KeithleyDMM6500(SCPIMixin, Instrument): """Represent the Keithely DMM6500 6½-Digit Multimeter and provide a high-level interface for interacting with the instrument. This class only uses "SCPI" command set (see also :attr:`command_set`) to diff --git a/pymeasure/instruments/keysight/keysightDSOX1102G.py b/pymeasure/instruments/keysight/keysightDSOX1102G.py index b70303a5a3..9a6dcb4856 100644 --- a/pymeasure/instruments/keysight/keysightDSOX1102G.py +++ b/pymeasure/instruments/keysight/keysightDSOX1102G.py @@ -25,7 +25,7 @@ import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) @@ -218,7 +218,7 @@ def current_configuration(self): return ch_setup_dict -class KeysightDSOX1102G(Instrument): +class KeysightDSOX1102G(SCPIUnknownMixin, Instrument): """ Represents the Keysight DSOX1102G Oscilloscope interface for interacting with the instrument. diff --git a/pymeasure/instruments/keysight/keysightE36312A.py b/pymeasure/instruments/keysight/keysightE36312A.py index 6e32bd4331..f9018bb0a9 100644 --- a/pymeasure/instruments/keysight/keysightE36312A.py +++ b/pymeasure/instruments/keysight/keysightE36312A.py @@ -25,7 +25,7 @@ import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) @@ -71,7 +71,7 @@ class VoltageChannel(Channel): ) -class KeysightE36312A(Instrument): +class KeysightE36312A(SCPIUnknownMixin, Instrument): """ Represents the Keysight E36312A Power supply interface for interacting with the instrument. diff --git a/pymeasure/instruments/keysight/keysightE3631A.py b/pymeasure/instruments/keysight/keysightE3631A.py index 1a224fb3b0..df9df43c28 100644 --- a/pymeasure/instruments/keysight/keysightE3631A.py +++ b/pymeasure/instruments/keysight/keysightE3631A.py @@ -25,7 +25,7 @@ import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) @@ -64,7 +64,7 @@ class VoltageChannel(Channel): ) -class KeysightE3631A(Instrument): +class KeysightE3631A(SCPIMixin, Instrument): """ Represents the Keysight E3631A Triple Output DC Power Supply interface for interacting with the instrument. @@ -85,7 +85,8 @@ class KeysightE3631A(Instrument): def __init__(self, adapter, name="Keysight E3631A", **kwargs): super().__init__( - adapter, name, includeSCPI=True, **kwargs + adapter, name, + **kwargs ) self.channels[1].voltage_setpoint_values = [0, 6] self.channels[1].current_limit_values = [0, 5] diff --git a/pymeasure/instruments/keysight/keysightN5767A.py b/pymeasure/instruments/keysight/keysightN5767A.py index 7cac1d807e..c684f9d913 100644 --- a/pymeasure/instruments/keysight/keysightN5767A.py +++ b/pymeasure/instruments/keysight/keysightN5767A.py @@ -25,7 +25,7 @@ import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range from pymeasure.adapters import VISAAdapter @@ -34,7 +34,7 @@ log.addHandler(logging.NullHandler()) -class KeysightN5767A(Instrument): +class KeysightN5767A(SCPIUnknownMixin, Instrument): """ Represents the Keysight N5767A Power supply interface for interacting with the instrument. """ diff --git a/pymeasure/instruments/keysight/keysightN7776C.py b/pymeasure/instruments/keysight/keysightN7776C.py index 0400377403..5f0a3f6c52 100644 --- a/pymeasure/instruments/keysight/keysightN7776C.py +++ b/pymeasure/instruments/keysight/keysightN7776C.py @@ -24,7 +24,7 @@ import logging import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) @@ -34,7 +34,7 @@ LOCK_PW = 1234 -class KeysightN7776C(Instrument): +class KeysightN7776C(SCPIUnknownMixin, Instrument): """ This represents the Keysight N7776C Tunable Laser Source interface. diff --git a/pymeasure/instruments/lakeshore/lakeshore211.py b/pymeasure/instruments/lakeshore/lakeshore211.py index 89cf0d1dd3..dfd0ef1db6 100644 --- a/pymeasure/instruments/lakeshore/lakeshore211.py +++ b/pymeasure/instruments/lakeshore/lakeshore211.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set from pyvisa.constants import Parity from enum import IntEnum @@ -33,7 +33,7 @@ log.addHandler(logging.NullHandler()) -class LakeShore211(Instrument): +class LakeShore211(SCPIUnknownMixin, Instrument): """ Represents the Lake Shore 211 Temperature Monitor and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/lakeshore/lakeshore224.py b/pymeasure/instruments/lakeshore/lakeshore224.py index c0714c2c02..7f0c532c83 100644 --- a/pymeasure/instruments/lakeshore/lakeshore224.py +++ b/pymeasure/instruments/lakeshore/lakeshore224.py @@ -23,14 +23,14 @@ # import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.lakeshore.lakeshore_base import LakeShoreTemperatureChannel log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -class LakeShore224(Instrument): +class LakeShore224(SCPIUnknownMixin, Instrument): """ Represents the Lakeshore 224 Temperature monitor and provides a high-level interface for interacting with the instrument. Note that the 224 provides 12 temperature input channels (A, B, C1-5, D1-5). This driver makes use of the :ref:`LakeShoreChannels` diff --git a/pymeasure/instruments/lakeshore/lakeshore331.py b/pymeasure/instruments/lakeshore/lakeshore331.py index 56e1cb4f53..638f5899cc 100644 --- a/pymeasure/instruments/lakeshore/lakeshore331.py +++ b/pymeasure/instruments/lakeshore/lakeshore331.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.lakeshore.lakeshore_base import LakeShoreTemperatureChannel, \ LakeShoreHeaterChannel @@ -32,7 +32,7 @@ log.addHandler(logging.NullHandler()) -class LakeShore331(Instrument): +class LakeShore331(SCPIUnknownMixin, Instrument): """ Represents the Lake Shore 331 Temperature Controller and provides a high-level interface for interacting with the instrument. Note that the 331 provides two input channels (A and B) and two output channels (1 and 2). diff --git a/pymeasure/instruments/lakeshore/lakeshore421.py b/pymeasure/instruments/lakeshore/lakeshore421.py index 533479ba3a..ddd4a9075e 100644 --- a/pymeasure/instruments/lakeshore/lakeshore421.py +++ b/pymeasure/instruments/lakeshore/lakeshore421.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set @@ -30,7 +30,7 @@ from time import time, sleep -class LakeShore421(Instrument): +class LakeShore421(SCPIUnknownMixin, Instrument): """ Represents the Lake Shore 421 Gaussmeter and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/lakeshore/lakeshore425.py b/pymeasure/instruments/lakeshore/lakeshore425.py index a866c3be68..3138084ecd 100644 --- a/pymeasure/instruments/lakeshore/lakeshore425.py +++ b/pymeasure/instruments/lakeshore/lakeshore425.py @@ -22,14 +22,14 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, truncated_discrete_set from time import sleep import numpy as np -class LakeShore425(Instrument): +class LakeShore425(SCPIUnknownMixin, Instrument): """ Represents the LakeShore 425 Gaussmeter and provides a high-level interface for interacting with the instrument diff --git a/pymeasure/instruments/teledyne/teledyne_oscilloscope.py b/pymeasure/instruments/teledyne/teledyne_oscilloscope.py index 3fa651ea16..0ee51677d4 100644 --- a/pymeasure/instruments/teledyne/teledyne_oscilloscope.py +++ b/pymeasure/instruments/teledyne/teledyne_oscilloscope.py @@ -29,7 +29,7 @@ from decimal import Decimal import numpy as np -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range, \ strict_discrete_range @@ -458,7 +458,7 @@ def current_configuration(self): return ch_setup -class TeledyneOscilloscope(Instrument, metaclass=ABCMeta): +class TeledyneOscilloscope(SCPIUnknownMixin, Instrument, metaclass=ABCMeta): """A base abstract class for any Teledyne Lecroy oscilloscope. All Teledyne oscilloscopes have a very similar interface, hence this base class to combine From 15b93fcbc90a3db9f6ffff1d9278388ad1e7eb3a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:06:36 +0100 Subject: [PATCH 024/181] Add SCPIMixin to instruments n-z. --- pymeasure/instruments/parker/parkerGV6.py | 4 ++-- pymeasure/instruments/pendulum/cnt91.py | 4 ++-- .../instruments/razorbill/razorbillRP100.py | 4 ++-- pymeasure/instruments/rohdeschwarz/fsl.py | 7 ++++--- pymeasure/instruments/rohdeschwarz/hmp.py | 7 ++++--- pymeasure/instruments/rohdeschwarz/sfm.py | 19 +++++++++---------- .../siglenttechnologies/siglent_spdbase.py | 4 ++-- pymeasure/instruments/srs/sg380.py | 4 ++-- pymeasure/instruments/srs/sr570.py | 4 ++-- pymeasure/instruments/srs/sr830.py | 4 ++-- pymeasure/instruments/srs/sr860.py | 4 ++-- pymeasure/instruments/tektronix/afg3152c.py | 4 ++-- pymeasure/instruments/tektronix/tds2000.py | 6 +++--- .../instruments/teledyne/teledyneT3AFG.py | 4 ++-- .../instruments/temptronic/temptronic_base.py | 4 ++-- .../instruments/thorlabs/thorlabspm100usb.py | 4 ++-- .../instruments/thorlabs/thorlabspro8000.py | 4 ++-- .../instruments/yokogawa/yokogawa7651.py | 4 ++-- .../instruments/yokogawa/yokogawags200.py | 4 ++-- 19 files changed, 50 insertions(+), 49 deletions(-) diff --git a/pymeasure/instruments/parker/parkerGV6.py b/pymeasure/instruments/parker/parkerGV6.py index 82d2455dd2..90456fb0ca 100644 --- a/pymeasure/instruments/parker/parkerGV6.py +++ b/pymeasure/instruments/parker/parkerGV6.py @@ -22,12 +22,12 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from time import sleep import re -class ParkerGV6(Instrument): +class ParkerGV6(SCPIUnknownMixin, Instrument): """ Represents the Parker Gemini GV6 Servo Motor Controller and provides a high-level interface for interacting with the instrument diff --git a/pymeasure/instruments/pendulum/cnt91.py b/pymeasure/instruments/pendulum/cnt91.py index 5a75475390..d8b5b309ec 100644 --- a/pymeasure/instruments/pendulum/cnt91.py +++ b/pymeasure/instruments/pendulum/cnt91.py @@ -26,7 +26,7 @@ from time import sleep from warnings import warn -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, strict_range, @@ -43,7 +43,7 @@ MAX_BUFFER_SIZE = 10000 # Programmer's guide 8-39 -class CNT91(Instrument): +class CNT91(SCPIUnknownMixin, Instrument): """Represents a Pendulum CNT-91 frequency counter.""" CHANNELS = {"A": 1, "B": 2, "C": 3, "E": 4, "INTREF": 6} diff --git a/pymeasure/instruments/razorbill/razorbillRP100.py b/pymeasure/instruments/razorbill/razorbillRP100.py index e55273b067..727424cb0b 100644 --- a/pymeasure/instruments/razorbill/razorbillRP100.py +++ b/pymeasure/instruments/razorbill/razorbillRP100.py @@ -23,12 +23,12 @@ # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import (strict_discrete_set, strict_range) -class razorbillRP100(Instrument): +class razorbillRP100(SCPIUnknownMixin, Instrument): """Represents Razorbill RP100 strain cell controller .. code-block:: python diff --git a/pymeasure/instruments/rohdeschwarz/fsl.py b/pymeasure/instruments/rohdeschwarz/fsl.py index eb3f8d90b7..0835d9f6ff 100644 --- a/pymeasure/instruments/rohdeschwarz/fsl.py +++ b/pymeasure/instruments/rohdeschwarz/fsl.py @@ -26,7 +26,7 @@ import numpy as np from pymeasure.instruments.validators import strict_discrete_set -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIMixin log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -41,7 +41,7 @@ def _number_or_auto(value): return " " + str(value) -class FSL(Instrument): +class FSL(SCPIMixin, Instrument): """ Represents a Rohde&Schwarz FSL spectrum analyzer. @@ -52,7 +52,8 @@ class FSL(Instrument): def __init__(self, adapter, name="Rohde&Schwarz FSL", **kwargs): super().__init__( - adapter, name, includeSCPI=True, **kwargs + adapter, name, + **kwargs ) # Frequency settings ------------------------------------------------------ diff --git a/pymeasure/instruments/rohdeschwarz/hmp.py b/pymeasure/instruments/rohdeschwarz/hmp.py index a504dcc735..c0afc25f8f 100644 --- a/pymeasure/instruments/rohdeschwarz/hmp.py +++ b/pymeasure/instruments/rohdeschwarz/hmp.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import (strict_discrete_set, truncated_range) @@ -54,13 +54,14 @@ def process_sequence(sequence): return sequence -class HMP4040(Instrument): +class HMP4040(SCPIMixin, Instrument): """Represents a Rohde&Schwarz HMP4040 power supply.""" def __init__(self, adapter, **kwargs): kwargs.setdefault("name", "Rohde&Schwarz HMP4040") super().__init__( - adapter, includeSCPI=True, **kwargs + adapter, + **kwargs ) # System Setting Commands ------------------------------------------------- diff --git a/pymeasure/instruments/rohdeschwarz/sfm.py b/pymeasure/instruments/rohdeschwarz/sfm.py index fcea496617..6f05960af3 100644 --- a/pymeasure/instruments/rohdeschwarz/sfm.py +++ b/pymeasure/instruments/rohdeschwarz/sfm.py @@ -23,7 +23,7 @@ # import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) @@ -34,7 +34,7 @@ class Sound_Channel: """ Class object for the two sound channels - refere also to chapter 3.6.6.7 of the user manual + refer also to chapter 3.6.6.7 of the user manual """ modulation_degree = Instrument.control( "AUD:DEGR?", @@ -189,7 +189,7 @@ def read(self): self.instrument.read() -class SFM(Instrument): +class SFM(SCPIMixin, Instrument): """ Represents the Rohde&Schwarz SFM TV test transmitter interface for interacting with the instrument. @@ -198,7 +198,7 @@ class SFM(Instrument): Further source extension for system 2-6 would be required. - The intermodulation subsystem is also not yet implmented. + The intermodulation subsystem is also not yet implemented. """ @@ -206,7 +206,6 @@ def __init__(self, adapter, name="Rohde&Schwarz SFM", **kwargs): super().__init__( adapter, name, - includeSCPI=True, **kwargs ) self.sound1 = Sound_Channel(self, 1) @@ -447,7 +446,7 @@ def channel_down_relative(self): ====== ======= Value Meaning ====== ======= - CW Continous wave mode + CW Continuous wave mode FIXED fixed frequency mode CHSW Channel sweep RFSW Frequency sweep @@ -567,7 +566,7 @@ def channel_down_relative(self): ====== ==================== ================= NORM Normal mode +6 dBm LOWN low noise mode +10 dBm - CONT continous mode +10 dBm + CONT continuous mode +10 dBm LOWD low distortion mode +0 dBm ====== ==================== ================= @@ -759,7 +758,7 @@ def coder_adjust(self): nicam_bit_error_rate = Instrument.control( "SOUR:TEL:MOD:NIC:BIT?", "SOUR:TEL:MOD:NIC:BIT %g", - """ A float property that controls the artifical bit error rate. + """ A float property that controls the artificial bit error rate. valid range: 1.2E-7 .. 2E-3 """, @@ -770,7 +769,7 @@ def coder_adjust(self): nicam_bit_error_enabled = Instrument.control( "SOUR:TEL:MOD:NIC:BIT:STAT?", "SOUR:TEL:MOD:NIC:BIT:STAT %d", - """ A bool property that controls the status of an artifical bit error rate to be applied + """ A bool property that controls the status of an artificial bit error rate to be applied """, validator=strict_discrete_set, @@ -852,7 +851,7 @@ def coder_adjust(self): ====== ======= INT Internal audio generator(s) EXT External audio source - CW Continous wave signal + CW Continuous wave signal RAND Random data stream TEST Test signal ====== ======= diff --git a/pymeasure/instruments/siglenttechnologies/siglent_spdbase.py b/pymeasure/instruments/siglenttechnologies/siglent_spdbase.py index 3afc51d9ad..410250bbf0 100644 --- a/pymeasure/instruments/siglenttechnologies/siglent_spdbase.py +++ b/pymeasure/instruments/siglenttechnologies/siglent_spdbase.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # import logging -from pymeasure.instruments.instrument import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.channel import Channel from pymeasure.instruments.validators import (strict_discrete_range, strict_discrete_set, @@ -155,7 +155,7 @@ def configure_timer(self, step, voltage, current, duration): self.write(f'TIME:SET CH{{ch}},{step:d},{voltage:1.3f},{current:1.3f},{duration:d}') -class SPDBase(Instrument): +class SPDBase(SCPIUnknownMixin, Instrument): """ The base class for Siglent SPDxxxxX instruments. Uses :class:`SPDChannel` for measurement channels. diff --git a/pymeasure/instruments/srs/sg380.py b/pymeasure/instruments/srs/sg380.py index 6f161e8edc..64da144a43 100644 --- a/pymeasure/instruments/srs/sg380.py +++ b/pymeasure/instruments/srs/sg380.py @@ -22,11 +22,11 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range -class SG380(Instrument): +class SG380(SCPIUnknownMixin, Instrument): MOD_TYPES_VALUES = ['AM', 'FM', 'PM', 'SWEEP', 'PULSE', 'BLANK', 'IQ'] diff --git a/pymeasure/instruments/srs/sr570.py b/pymeasure/instruments/srs/sr570.py index 9729cd3da9..e16840dcf7 100644 --- a/pymeasure/instruments/srs/sr570.py +++ b/pymeasure/instruments/srs/sr570.py @@ -22,12 +22,12 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range -class SR570(Instrument): +class SR570(SCPIUnknownMixin, Instrument): def __init__(self, adapter, name="Stanford Research Systems SR570 Lock-in amplifier", **kwargs): diff --git a/pymeasure/instruments/srs/sr830.py b/pymeasure/instruments/srs/sr830.py index c1688e8bbf..5d717c4e42 100644 --- a/pymeasure/instruments/srs/sr830.py +++ b/pymeasure/instruments/srs/sr830.py @@ -26,7 +26,7 @@ import time import numpy as np from enum import IntFlag -from pymeasure.instruments import Instrument, discreteTruncate +from pymeasure.instruments import Instrument, discreteTruncate, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range @@ -57,7 +57,7 @@ class ERRStatus(IntFlag): MATH_ERR = 128 -class SR830(Instrument): +class SR830(SCPIUnknownMixin, Instrument): SAMPLE_FREQUENCIES = [ 62.5e-3, 125e-3, 250e-3, 500e-3, 1, 2, 4, 8, 16, diff --git a/pymeasure/instruments/srs/sr860.py b/pymeasure/instruments/srs/sr860.py index c58cd01c4e..63658ecde5 100644 --- a/pymeasure/instruments/srs/sr860.py +++ b/pymeasure/instruments/srs/sr860.py @@ -24,10 +24,10 @@ from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin -class SR860(Instrument): +class SR860(SCPIUnknownMixin, Instrument): SENSITIVITIES = [ 1e-9, 2e-9, 5e-9, 10e-9, 20e-9, 50e-9, 100e-9, 200e-9, diff --git a/pymeasure/instruments/tektronix/afg3152c.py b/pymeasure/instruments/tektronix/afg3152c.py index 12739c6b99..6f7cb1ee7b 100644 --- a/pymeasure/instruments/tektronix/afg3152c.py +++ b/pymeasure/instruments/tektronix/afg3152c.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # from math import sqrt, log10 -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set @@ -146,7 +146,7 @@ def waveform( self.write("voltage:offset %eV" % offset) -class AFG3152C(Instrument): +class AFG3152C(SCPIUnknownMixin, Instrument): """Represents the Tektronix AFG 3000 series (one or two channels) arbitrary function generator and provides a high-level for interacting with the instrument. diff --git a/pymeasure/instruments/tektronix/tds2000.py b/pymeasure/instruments/tektronix/tds2000.py index be729c60de..51aaa0131b 100644 --- a/pymeasure/instruments/tektronix/tds2000.py +++ b/pymeasure/instruments/tektronix/tds2000.py @@ -22,10 +22,10 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin -class TDS2000(Instrument): +class TDS2000(SCPIUnknownMixin, Instrument): """ Represents the Tektronix TDS 2000 Oscilloscope and provides a high-level for interacting with the instrument """ @@ -85,7 +85,7 @@ def unit(self, value): raise ValueError("Invalid unit ('{}') provided to {}".format( self.parent, value)) - def __init__(self, adapter, name="Tektronix TDS 2000 Oscilliscope", **kwargs): + def __init__(self, adapter, name="Tektronix TDS 2000 Oscilloscope", **kwargs): super().__init__( adapter, name, diff --git a/pymeasure/instruments/teledyne/teledyneT3AFG.py b/pymeasure/instruments/teledyne/teledyneT3AFG.py index 55f9156a63..a73268383c 100644 --- a/pymeasure/instruments/teledyne/teledyneT3AFG.py +++ b/pymeasure/instruments/teledyne/teledyneT3AFG.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) @@ -122,7 +122,7 @@ class SignalChannel(Channel): ) -class TeledyneT3AFG(Instrument): +class TeledyneT3AFG(SCPIUnknownMixin, Instrument): """Represents the Teledyne T3AFG series of arbitrary waveform generator interface for interacting with the instrument. diff --git a/pymeasure/instruments/temptronic/temptronic_base.py b/pymeasure/instruments/temptronic/temptronic_base.py index 87aeeb9360..f5ced01040 100644 --- a/pymeasure/instruments/temptronic/temptronic_base.py +++ b/pymeasure/instruments/temptronic/temptronic_base.py @@ -39,7 +39,7 @@ """ import logging import time -from pymeasure.instruments.instrument import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import (strict_discrete_set, truncated_range, strict_range @@ -121,7 +121,7 @@ class ErrorCode(IntFlag): OK = 0 # ok state -class ATSBase(Instrument): +class ATSBase(SCPIUnknownMixin, Instrument): """The base class for Temptronic ATSXXX instruments. """ diff --git a/pymeasure/instruments/thorlabs/thorlabspm100usb.py b/pymeasure/instruments/thorlabs/thorlabspm100usb.py index 3db89e13b6..d9d11f8a4e 100644 --- a/pymeasure/instruments/thorlabs/thorlabspm100usb.py +++ b/pymeasure/instruments/thorlabs/thorlabspm100usb.py @@ -24,14 +24,14 @@ import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -class ThorlabsPM100USB(Instrument): +class ThorlabsPM100USB(SCPIUnknownMixin, Instrument): """Represents Thorlabs PM100USB powermeter.""" def __init__(self, adapter, name="ThorlabsPM100USB powermeter", **kwargs): diff --git a/pymeasure/instruments/thorlabs/thorlabspro8000.py b/pymeasure/instruments/thorlabs/thorlabspro8000.py index 03bbcc1341..6dbf62ebc2 100644 --- a/pymeasure/instruments/thorlabs/thorlabspro8000.py +++ b/pymeasure/instruments/thorlabs/thorlabspro8000.py @@ -22,11 +22,11 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set -class ThorlabsPro8000(Instrument): +class ThorlabsPro8000(SCPIUnknownMixin, Instrument): """Represents Thorlabs Pro 8000 modular laser driver""" SLOTS = range(1, 9) LDC_POLARITIES = ['AG', 'CG'] diff --git a/pymeasure/instruments/yokogawa/yokogawa7651.py b/pymeasure/instruments/yokogawa/yokogawa7651.py index 6e667e2e23..acf74bb3f3 100644 --- a/pymeasure/instruments/yokogawa/yokogawa7651.py +++ b/pymeasure/instruments/yokogawa/yokogawa7651.py @@ -27,7 +27,7 @@ import numpy as np -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( truncated_discrete_set, strict_discrete_set, truncated_range @@ -37,7 +37,7 @@ log.addHandler(logging.NullHandler()) -class Yokogawa7651(Instrument): +class Yokogawa7651(SCPIUnknownMixin, Instrument): """ Represents the Yokogawa 7651 Programmable DC Source and provides a high-level for interacting with the instrument. diff --git a/pymeasure/instruments/yokogawa/yokogawags200.py b/pymeasure/instruments/yokogawa/yokogawags200.py index c0fad49f95..a5729ffee3 100644 --- a/pymeasure/instruments/yokogawa/yokogawags200.py +++ b/pymeasure/instruments/yokogawa/yokogawags200.py @@ -23,7 +23,7 @@ # import logging -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, truncated_discrete_set, truncated_range ) @@ -34,7 +34,7 @@ MIN_RAMP_TIME = 0.1 # seconds -class YokogawaGS200(Instrument): +class YokogawaGS200(SCPIUnknownMixin, Instrument): """ Represents the Yokogawa GS200 source and provides a high-level interface for interacting with the instrument. """ From 36a69c29c87b0699c96c2dd14b867e344908f906 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:10:33 +0100 Subject: [PATCH 025/181] Remove exception for old instruments from test_all_instruments. Fix instruments not having SCPIUnknownMixin. --- pymeasure/instruments/agilent/agilent4156.py | 10 +-- pymeasure/instruments/newport/esp300.py | 8 +- tests/instruments/test_all_instruments.py | 88 -------------------- 3 files changed, 9 insertions(+), 97 deletions(-) diff --git a/pymeasure/instruments/agilent/agilent4156.py b/pymeasure/instruments/agilent/agilent4156.py index 064efb62b0..0b508bcd50 100644 --- a/pymeasure/instruments/agilent/agilent4156.py +++ b/pymeasure/instruments/agilent/agilent4156.py @@ -426,7 +426,7 @@ def get_data(self, path=None): ########## -class SMU(Instrument): +class SMU(SCPIUnknownMixin, Instrument): def __init__(self, adapter, channel, **kwargs): super().__init__( adapter, @@ -649,7 +649,7 @@ def __validate_compl(self): return values -class VMU(Instrument): +class VMU(SCPIUnknownMixin, Instrument): def __init__(self, adapter, channel, **kwargs): super().__init__( adapter, @@ -707,7 +707,7 @@ def channel_mode(self, mode): self.check_errors() -class VSU(Instrument): +class VSU(SCPIUnknownMixin, Instrument): def __init__(self, adapter, channel, **kwargs): super().__init__( adapter, @@ -803,7 +803,7 @@ def channel_function(self, function): ################# -class VARX(Instrument): +class VARX(SCPIUnknownMixin, Instrument): """ Base class to define sweep variable settings """ def __init__(self, adapter, var_name, **kwargs): @@ -964,7 +964,7 @@ def __init__(self, adapter, **kwargs): ) -class VARD(Instrument): +class VARD(SCPIUnknownMixin, Instrument): """ Class to handle all the definitions needed for VARD. VARD is always defined in relation to VAR1. """ diff --git a/pymeasure/instruments/newport/esp300.py b/pymeasure/instruments/newport/esp300.py index 5979adfdd9..27c8ee7c05 100644 --- a/pymeasure/instruments/newport/esp300.py +++ b/pymeasure/instruments/newport/esp300.py @@ -24,7 +24,7 @@ from time import sleep -from pymeasure.instruments import Instrument +from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set @@ -160,7 +160,7 @@ class Axis: units = Instrument.control( "SN?", "SN%d", """ A string property that controls the displacement units of the - axis, which can take values of: enconder count, motor step, millimeter, + axis, which can take values of: encoder count, motor step, millimeter, micrometer, inches, milli-inches, micro-inches, degree, gradient, radian, milliradian, and microradian. """, @@ -231,13 +231,13 @@ def wait_for_stop(self, delay=0, interval=0.05): sleep(interval) -class ESP300(Instrument): +class ESP300(SCPIUnknownMixin, Instrument): """ Represents the Newport ESP 300 Motion Controller and provides a high-level for interacting with the instrument. By default this instrument is constructed with x, y, and phi attributes that represent axes 1, 2, and 3. Custom implementations - can overwrite this depending on the avalible axes. Axes are controlled + can overwrite this depending on the available axes. Axes are controlled through an :class:`Axis ` class. """ diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py index 150606017e..41ea63a50b 100644 --- a/tests/instruments/test_all_instruments.py +++ b/tests/instruments/test_all_instruments.py @@ -198,92 +198,6 @@ def find_devices_in_module(module): "ChannelAWG", "ChannelAFG", ] -# Instruments which do not YET define `includeSCPI` explicitly -grandfathered_includeSCPI_instruments = [ - "AdvantestR624X", - "AdvantestR6245", - "AdvantestR6246", - "AdvantestR3767CG", - "AFG3152C", - "Agilent33521A", - "Agilent8722ES", - "AgilentB1500", - "AgilentE4980", - "AgilentE4408B", - "Agilent33500", - "Agilent8257D", - "Agilent34410A", - "Agilent33220A", - "Agilent4156", - "Ametek7270", - "AMI430", - "AnritsuMS4645B", - "AnritsuMS4647B", - "AnritsuMS4644B", - "AnritsuMS464xB", - "AnritsuMS4642B", - "AnritsuMS9740A", - "AnritsuMS2090A", - "AnritsuMS9710C", - "AnritsuMG3692C", - "APSIN12G", - "ATSBase", - "ATS545", - "ATS525", - "AWG401x_base", - "BKPrecision9130B", - "CNT91", - "ECO560", - "ESP300", - "HP33120A", - "HP34401A", - "Keithley2000", - "Keithley2200", - "Keithley2400", - "Keithley2600", - "Keithley2260B", - "Keithley2306", - "Keithley2750", - "Keithley6221", - "Keithley6517B", - "Keithley2450", - "KeysightDSOX1102G", - "KeysightN7776C", - "KeysightN5767A", - "KeysightE36312A", - "LakeShore211", - "LakeShore224", - "LakeShore331", - "LakeShore421", - "LakeShore425", - "LeCroyT3DSO1204", - "ParkerGV6", - "PL303P", - "PL303QMTP", - "PL303QMDP", - "PLBase", - "PL068P", - "PL601P", - "PL155P", - "razorbillRP100", - "SG380", - "SM7045D", - "SPDBase", - "SPDSingleChannelBase", - "SPD1168X", - "SPD1305X", - "SR860", - "SR830", - "SR570", - "TDS2000", - "TeledyneMAUI", - "TeledyneOscilloscope", - "TexioPSW360L30", - "ThorlabsPro8000", - "VellemanK8090", - "Yokogawa7651", - "YokogawaGS200", -] @pytest.mark.parametrize("cls", devices) @@ -342,8 +256,6 @@ def test_includeSCPI_explicitly_set(cls): pytest.skip(f"{cls.__name__} cannot be tested without communication.") elif cls.__name__ in channel_as_instrument_subclass: pytest.skip(f"{cls.__name__} is a channel, not an instrument.") - elif cls.__name__ in grandfathered_includeSCPI_instruments: - pytest.skip(f"{cls.__name__} is in the codebase and needs information about SCPI.") elif cls.__name__ == "Instrument": pytest.skip("`Instrument` requires a `name` parameter.") From a865c9313ee1382fbc8ac7ec4096b2366e7e0cb6 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:02:41 +0100 Subject: [PATCH 026/181] Add API documentation. --- docs/api/instruments/generic_types.rst | 9 +++++++++ docs/api/instruments/index.rst | 1 + pymeasure/instruments/generic_types.py | 2 +- tests/instruments/test_generic_types.py | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 docs/api/instruments/generic_types.rst diff --git a/docs/api/instruments/generic_types.rst b/docs/api/instruments/generic_types.rst new file mode 100644 index 0000000000..28c1a6eebf --- /dev/null +++ b/docs/api/instruments/generic_types.rst @@ -0,0 +1,9 @@ +############################### +Generic Instrument Types Mixins +############################### + +You can use these Mixins as a additional parent classes for your instrument. + + +.. autoclass:: pymeasure.instruments.generic_types.SCPIMixin + :members: diff --git a/docs/api/instruments/index.rst b/docs/api/instruments/index.rst index cb3f1ab0e4..6936874bc4 100644 --- a/docs/api/instruments/index.rst +++ b/docs/api/instruments/index.rst @@ -10,6 +10,7 @@ This section contains documentation on the instrument classes. :maxdepth: 2 instruments + generic_types validators comedi resources diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index a29a1f6cb3..d88a80776e 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -1,7 +1,7 @@ # # This file is part of the PyMeasure package. # -# Copyright (c) 2013-2023 PyMeasure Developers +# Copyright (c) 2013-2024 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py index ab9ac19f01..13e9b8f019 100644 --- a/tests/instruments/test_generic_types.py +++ b/tests/instruments/test_generic_types.py @@ -1,7 +1,7 @@ # # This file is part of the PyMeasure package. # -# Copyright (c) 2013-2023 PyMeasure Developers +# Copyright (c) 2013-2024 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal From c4d745b4739bbce7518ae9b1e7a73a6edfa9a949 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:29:41 +0100 Subject: [PATCH 027/181] Fix typo. --- docs/api/instruments/generic_types.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/instruments/generic_types.rst b/docs/api/instruments/generic_types.rst index 28c1a6eebf..d22e91c880 100644 --- a/docs/api/instruments/generic_types.rst +++ b/docs/api/instruments/generic_types.rst @@ -2,7 +2,7 @@ Generic Instrument Types Mixins ############################### -You can use these Mixins as a additional parent classes for your instrument. +You can use these Mixins as additional parent classes for your instrument. .. autoclass:: pymeasure.instruments.generic_types.SCPIMixin From 528dd559a89ab3c4cb822a136e5cb90d1e522082 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:36:48 +0100 Subject: [PATCH 028/181] Add documentation regarding SCPI-Mixin. --- docs/api/instruments/generic_types.rst | 1 + docs/dev/adding_instruments/instrument.rst | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/api/instruments/generic_types.rst b/docs/api/instruments/generic_types.rst index d22e91c880..a2501fdc5f 100644 --- a/docs/api/instruments/generic_types.rst +++ b/docs/api/instruments/generic_types.rst @@ -3,6 +3,7 @@ Generic Instrument Types Mixins ############################### You can use these Mixins as additional parent classes for your instrument. +For more informations, see :ref:`common_instrument_types`. .. autoclass:: pymeasure.instruments.generic_types.SCPIMixin diff --git a/docs/dev/adding_instruments/instrument.rst b/docs/dev/adding_instruments/instrument.rst index 71fb8bc976..4cedb0ebca 100644 --- a/docs/dev/adding_instruments/instrument.rst +++ b/docs/dev/adding_instruments/instrument.rst @@ -123,6 +123,8 @@ In principle, you are free to write any methods that are necessary for interacti In practice, we have developed a number of best practices for making instruments easy to write and maintain. The following sections detail these, which are highly encouraged to follow. +.. _common_instrument_types: + Common instrument types *********************** There are a number of categories that many instruments fit into. @@ -130,6 +132,21 @@ In the future, pymeasure should gain an abstraction layer based on that, see `th Until that is ready, here are a couple of guidelines towards a more uniform API. Note that not all already available instruments follow these, but expect this to be harmonized in the future. +Generic types mixins +-------------------- +The :doc:`generic_types <../../api/instruments/generic_types>` module contains mixin classes for common types. +For example, if an instrument complies to SCPI standards, you can add :class:`~pymeasure.instruments.generic_types.SCPIMixin` to your instrument: + +.. testcode:: + + from pymeasure.instruments.generic_types import SCPIMixin + + class SomeSCPIInstrument(SCPIMixin, Instrument): + """This instrument has properties and methods defined for all SCPI instruments""" + +This mixin adds default SCPI properties like :attr:`~pymeasure.instruments.generic_types.SCPIMixin.id`, :attr:`~pymeasure.instruments.generic_types.SCPIMixin.status` and default methods like :meth:`~pymeasure.instruments.generic_types.SCPIMixin.clear` and :meth:`~pymeasure.instruments.generic_types.SCPIMixin.reset` to :code:`SomeSCPIInstrument`. + + Frequent properties ------------------- If your instrument has an **output** that can be switched on and off, use a :ref:`boolean property ` called :code:`output_enabled`. From b72fd7a8bc85053f679b204a45ee5878ba483968 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:38:54 +0100 Subject: [PATCH 029/181] Add codecov.yml with small coverage drop allowed. --- codecov.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..538b4890ae --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +coverage: + status: + project: + default: + # basic + target: auto + threshold: 0.25% # allows a small drop of coverage for flaky tests From aa4150a074308678bc0b48e02038417431ba8c0e Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:13:38 +0100 Subject: [PATCH 030/181] Disable codecov status checks. --- codecov.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/codecov.yml b/codecov.yml index 538b4890ae..35cde5cd5e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,4 @@ coverage: status: - project: - default: - # basic - target: auto - threshold: 0.25% # allows a small drop of coverage for flaky tests + project: off + patch: off From ce6e551f69d5d6025a5cf53666f641b717d81db9 Mon Sep 17 00:00:00 2001 From: Connor Carr <67078909+ConnorGCarr@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:37:59 -0500 Subject: [PATCH 031/181] Dev/proterial rod4 (#1044) * Created Proterial ROD-4 instrument * Add proterial to instruments init * Linting * Replace Instrument methods with Channel methods in channel class. * pytest and linting * pytest, bugfix, added documentation * fix init read_termination * Fix docstrings * add proterial to instruments toc * Change set-only commands to setting property, fix instrument-level commands, update docs and copyright year. * change ROD4 property keyboard to keyboard_locked * Remove Proterial from instruments __init__, formatting to driver * add set error checking * update tests --- docs/api/instruments/index.rst | 1 + docs/api/instruments/proterial/index.rst | 12 ++ docs/api/instruments/proterial/rod4.rst | 12 ++ pymeasure/instruments/proterial/__init__.py | 25 ++++ pymeasure/instruments/proterial/rod4.py | 140 ++++++++++++++++++++ tests/instruments/proterial/test_rod4.py | 64 +++++++++ 6 files changed, 254 insertions(+) create mode 100644 docs/api/instruments/proterial/index.rst create mode 100644 docs/api/instruments/proterial/rod4.rst create mode 100644 pymeasure/instruments/proterial/__init__.py create mode 100644 pymeasure/instruments/proterial/rod4.py create mode 100644 tests/instruments/proterial/test_rod4.py diff --git a/docs/api/instruments/index.rst b/docs/api/instruments/index.rst index cb3f1ab0e4..ea26245f8e 100644 --- a/docs/api/instruments/index.rst +++ b/docs/api/instruments/index.rst @@ -54,6 +54,7 @@ Instruments by manufacturer: oxfordinstruments/index parker/index pendulum/index + proterial/index racal/index razorbill/index redpitaya/index diff --git a/docs/api/instruments/proterial/index.rst b/docs/api/instruments/proterial/index.rst new file mode 100644 index 0000000000..fa8c5871d8 --- /dev/null +++ b/docs/api/instruments/proterial/index.rst @@ -0,0 +1,12 @@ +.. module:: pymeasure.instruments.proterial + +######### +Proterial +######### + +This section contains specific documentation on the Proterial (formerly Hitachi Metals) instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. + +.. toctree:: + :maxdepth: 2 + + rod4 diff --git a/docs/api/instruments/proterial/rod4.rst b/docs/api/instruments/proterial/rod4.rst new file mode 100644 index 0000000000..f8919690f4 --- /dev/null +++ b/docs/api/instruments/proterial/rod4.rst @@ -0,0 +1,12 @@ +#################### +ROD-4 MFC Controller +#################### + +.. autoclass:: pymeasure.instruments.proterial.ROD4 + :members: + :show-inheritance: + :inherited-members: CommonBase + +.. autoclass:: pymeasure.instruments.proterial.rod4.ROD4Channel + :members: + :show-inheritance: \ No newline at end of file diff --git a/pymeasure/instruments/proterial/__init__.py b/pymeasure/instruments/proterial/__init__.py new file mode 100644 index 0000000000..c6ecb71979 --- /dev/null +++ b/pymeasure/instruments/proterial/__init__.py @@ -0,0 +1,25 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from .rod4 import ROD4 diff --git a/pymeasure/instruments/proterial/rod4.py b/pymeasure/instruments/proterial/rod4.py new file mode 100644 index 0000000000..aa8430f8b8 --- /dev/null +++ b/pymeasure/instruments/proterial/rod4.py @@ -0,0 +1,140 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging + +from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments.validators import (truncated_range, + strict_discrete_set) + + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +class ROD4Channel(Channel): + """Implementation of a ROD-4 MFC channel.""" + + actual_flow = Channel.measurement( + "\x020{ch}RFX", + """Measure the actual flow in %.""" + ) + + setpoint = Channel.control( + "\x020{ch}RFD", "\x020{ch}SFD%.1f", + """Control the setpoint in % of MFC range.""", + validator=truncated_range, + values=[0, 100], + check_set_errors=True + ) + + mfc_range = Channel.control( + "\x020{ch}RFK", "\x020{ch}SFK%d", + """Control the MFC range in sccm. + Upper limit is 200 slm.""", + validator=truncated_range, + values=[0, 200000], + check_set_errors=True + ) + + ramp_time = Channel.control( + "\x020{ch}RRT", "\x020{ch}SRT%.1f", + """Control the MFC setpoint ramping time in seconds.""", + validator=truncated_range, + values=[0, 200000], + check_set_errors=True + ) + + valve_mode = Channel.control( + "\x020{ch}RVM", "\x020{ch}SVM%d", + """Control the MFC valve mode. + Valid options are `flow`, `close`, and `open`. """, + validator=strict_discrete_set, + values={'flow': 0, 'close': 1, 'open': 2}, + map_values=True, + check_set_errors=True + ) + + flow_unit_display = Channel.setting( + "\x020{ch}SFU%d", + """Set the flow units on the front display. + Valid options are %, sccm, or slm. + Display in absolute units is in sccm for control range < 10 slm.""", + validator=strict_discrete_set, + values={'%': 0, 'sccm': 1, 'slm': 1}, + map_values=True, + check_set_errors=True + ) + + +class ROD4(Instrument): + """Represents the Proterial ROD-4(A) operator for mass flow controllers + and provides a high-level interface for interacting with the instrument. + User must specify which channel to control (1-4). + + .. code-block:: python + + rod4 = ROD4("ASRL1::INSTR") + + print(rod4.version) # Print version and series number + rod4.ch_1.mfc_range = 500 # Sets Channel 1 MFC range to 500 sccm + rod4.ch_2.valve_mode = 'flow' # Sets Channel 2 MFC to flow control + rod4.ch_3.setpoint = 50 # Sets Channel 3 MFC to flow at 50% of full range + print(rod4.ch_4.actual_flow) # Prints Channel 4 actual MFC flow in % + + """ + + def __init__(self, adapter, name="ROD-4 MFC Controller", **kwargs): + super().__init__( + adapter, name, read_termination='\r', write_termination='\r', + includeSCPI=False, **kwargs + ) + + ch_1 = Instrument.ChannelCreator(ROD4Channel, 1) + ch_2 = Instrument.ChannelCreator(ROD4Channel, 2) + ch_3 = Instrument.ChannelCreator(ROD4Channel, 3) + ch_4 = Instrument.ChannelCreator(ROD4Channel, 4) + + version = Instrument.measurement( + "\x0201RVN", + """Get the version and series number. Returns x.xxS/N """ + ) + + keyboard_locked = Instrument.setting( + "\x0201SKO%d", + """Set the front keyboard lock status.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True, + check_set_errors=True + ) + + def check_set_errors(self): + """Read 'OK' from ROD-4 after setting.""" + response = self.read() + if response != 'OK': + errors = ["Error setting ROD-4.",] + else: + errors = [] + return errors diff --git a/tests/instruments/proterial/test_rod4.py b/tests/instruments/proterial/test_rod4.py new file mode 100644 index 0000000000..bdfb8c89f0 --- /dev/null +++ b/tests/instruments/proterial/test_rod4.py @@ -0,0 +1,64 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.test import expected_protocol +from pymeasure.instruments.proterial.rod4 import ROD4 + + +def test_mfc_range(): + with expected_protocol( + ROD4, + [("\x0201SFK400", "OK"), + ("\x0202RFK", "200")], + ) as inst: + inst.ch_1.mfc_range = 400 + assert inst.ch_2.mfc_range == 200 + + +def test_valve_mode(): + with expected_protocol( + ROD4, + [("\x0203SVM0", "OK"), + ("\x0204RVM", "1")], + ) as inst: + inst.ch_3.valve_mode = 'flow' + assert inst.ch_4.valve_mode == 'close' + + +def test_setpoint(): + with expected_protocol( + ROD4, + [("\x0201SFD33.3", "OK"), + ("\x0202RFD", "50.4")], + ) as inst: + inst.ch_1.setpoint = 33.3 + assert inst.ch_2.setpoint == 50.4 + + +def test_actual_flow(): + with expected_protocol( + ROD4, + [("\x0203RFX", "40.1")], + ) as inst: + assert inst.ch_3.actual_flow == 40.1 From 359a7e34d5a74a8c28f2b3028d035e549d36ca9b Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:41:59 +0100 Subject: [PATCH 032/181] Use property creators for properties and rework documentation. --- pymeasure/instruments/generic_types.py | 43 ++++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index d88a80776e..348976f07a 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -32,37 +32,41 @@ class SCPIMixin: - """Base class for SCPI instruments with the default implementations of SCPI commands.""" + """Mixin class for SCPI instruments with the default implementation of base SCPI commands.""" def __init__(self, *args, **kwargs): kwargs.setdefault("includeSCPI", False) # in order not to trigger the deprecation warning super().__init__(*args, **kwargs) # SCPI default properties - @property - def complete(self): + complete = Instrument.measurement( + "*OPC?", """Get the synchronization bit. This property allows synchronization between a controller and a device. The Operation Complete query places an ASCII character 1 into the device's Output Queue when all pending selected device operations have been finished. - """ - return self.ask("*OPC?").strip() + """, + cast=str, + ) - @property - def status(self): - """ Get the status byte and Master Summary Status bit. """ - return self.ask("*STB?").strip() + status = Instrument.measurement( + "*STB?", + """Get the status byte and Master Summary Status bit.""", + cast=str, + ) - @property - def options(self): - """ Get the device options installed. """ - return self.ask("*OPT?").strip() + options = Instrument.measurement( + "*OPT?", + """Get the device options installed.""", + cast=str, + ) - @property - def id(self): - """ Get the identification of the instrument. """ - return self.ask("*IDN?").strip() + id = Instrument.measurement( + "*IDN?", + """Get the identification of the instrument.""", + cast=str, + ) next_error = Instrument.measurement( "SYST:ERR?", @@ -73,12 +77,11 @@ def id(self): # SCPI default methods def clear(self): - """ Clears the instrument status byte - """ + """Clear the instrument status byte.""" self.write("*CLS") def reset(self): - """ Resets the instrument. """ + """Reset the instrument.""" self.write("*RST") def check_errors(self): From e39a829d202a2fa978b37fb7007a63954d85a6bb Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:40:15 +0100 Subject: [PATCH 033/181] Fix id to not split strings. --- pymeasure/instruments/generic_types.py | 1 + tests/instruments/test_generic_types.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index 348976f07a..117af7deee 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -66,6 +66,7 @@ def __init__(self, *args, **kwargs): "*IDN?", """Get the identification of the instrument.""", cast=str, + maxsplit=0, ) next_error = Instrument.measurement( diff --git a/tests/instruments/test_generic_types.py b/tests/instruments/test_generic_types.py index 13e9b8f019..b297cc8fce 100644 --- a/tests/instruments/test_generic_types.py +++ b/tests/instruments/test_generic_types.py @@ -39,7 +39,7 @@ def test_init(self): assert inst.SCPI is False # should not be set by the new init @pytest.mark.parametrize("method, write, reply", ( - ("id", "*IDN?", "xyz"), + ("id", "*IDN?", "xyz, abc"), ("complete", "*OPC?", "1"), ("status", "*STB?", "189"), ("options", "*OPT?", "a9"), From 584c6b129eb2bf9e236471ae026417dd1315de8b Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:00:32 +0100 Subject: [PATCH 034/181] Add test for all instruments, that deprecated `includeSCPI=True` is not used. --- tests/instruments/test_all_instruments.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py index 41ea63a50b..f09ac2b8c4 100644 --- a/tests/instruments/test_all_instruments.py +++ b/tests/instruments/test_all_instruments.py @@ -250,7 +250,8 @@ def test_kwargs_to_adapter(cls): @pytest.mark.parametrize("cls", devices) -@pytest.mark.filterwarnings("error:It is deprecated to specify `includeSCPI`:FutureWarning") +@pytest.mark.filterwarnings( + "error:It is deprecated to specify `includeSCPI` implicitly:FutureWarning") def test_includeSCPI_explicitly_set(cls): if cls.__name__ in (*proper_adapters, *need_init_communication): pytest.skip(f"{cls.__name__} cannot be tested without communication.") @@ -263,6 +264,21 @@ def test_includeSCPI_explicitly_set(cls): # assert that no error is raised +@pytest.mark.parametrize("cls", devices) +@pytest.mark.filterwarnings( + "error:Defining SCPI base functionality with `includeSCPI=True` is deprecated:FutureWarning") +def test_includeSCPI_not_set_to_True(cls): + if cls.__name__ in (*proper_adapters, *need_init_communication): + pytest.skip(f"{cls.__name__} cannot be tested without communication.") + elif cls.__name__ in channel_as_instrument_subclass: + pytest.skip(f"{cls.__name__} is a channel, not an instrument.") + elif cls.__name__ == "Instrument": + pytest.skip("`Instrument` requires a `name` parameter.") + + cls(adapter=MagicMock()) + # assert that no error is raised + + def property_name_to_id(value): """Create a test id from `value`.""" device, property_name, prop = value From 67754f30abec6026411dd57e54ba2695c7fde9d4 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:11:33 +0100 Subject: [PATCH 035/181] Change last instruments to SCPIMixin. --- .../instruments/keithley/keithley2260B.py | 5 ++--- .../instruments/keithley/keithley2400.py | 5 ++--- .../instruments/keithley/keithley2450.py | 18 ++++++++--------- .../instruments/keithley/keithley2700.py | 20 +++++++++---------- .../instruments/keithley/keithley6221.py | 17 ++++++++-------- .../instruments/keithley/keithley6517b.py | 17 ++++++++-------- .../instruments/keithley/keithleyDMM6500.py | 8 +++++--- .../instruments/redpitaya/redpitaya_scpi.py | 5 ++--- .../instruments/teledyne/teledyneT3AFG.py | 7 ++++--- 9 files changed, 50 insertions(+), 52 deletions(-) diff --git a/pymeasure/instruments/keithley/keithley2260B.py b/pymeasure/instruments/keithley/keithley2260B.py index 19c67bf76e..ec45857ba8 100644 --- a/pymeasure/instruments/keithley/keithley2260B.py +++ b/pymeasure/instruments/keithley/keithley2260B.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set import logging @@ -32,7 +32,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2260B(SCPIUnknownMixin, Instrument): +class Keithley2260B(SCPIMixin, Instrument): """ Represents the Keithley 2260B Power Supply (minimal implementation) and provides a high-level interface for interacting with the instrument. @@ -56,7 +56,6 @@ def __init__(self, adapter, name="Keithley 2260B DC Power Supply", super().__init__( adapter, name, - includeSCPI=True, read_termination=read_termination, **kwargs ) diff --git a/pymeasure/instruments/keithley/keithley2400.py b/pymeasure/instruments/keithley/keithley2400.py index 3af5d977b4..6f218ee5c4 100644 --- a/pymeasure/instruments/keithley/keithley2400.py +++ b/pymeasure/instruments/keithley/keithley2400.py @@ -28,7 +28,7 @@ import numpy as np -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.errors import RangeException from pymeasure.instruments.validators import truncated_range, strict_discrete_set @@ -39,7 +39,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2400(KeithleyBuffer, SCPIUnknownMixin, Instrument): +class Keithley2400(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 2400 SourceMeter and provides a high-level interface for interacting with the instrument. @@ -374,7 +374,6 @@ class Keithley2400(KeithleyBuffer, SCPIUnknownMixin, Instrument): def __init__(self, adapter, name="Keithley 2400 SourceMeter", **kwargs): super().__init__( adapter, name, - includeSCPI=True, **kwargs ) diff --git a/pymeasure/instruments/keithley/keithley2450.py b/pymeasure/instruments/keithley/keithley2450.py index 376db37a05..a90006303b 100644 --- a/pymeasure/instruments/keithley/keithley2450.py +++ b/pymeasure/instruments/keithley/keithley2450.py @@ -28,7 +28,7 @@ import numpy as np -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer @@ -37,7 +37,7 @@ log.addHandler(logging.NullHandler()) -class Keithley2450(KeithleyBuffer, SCPIUnknownMixin, Instrument): +class Keithley2450(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 2450 SourceMeter and provides a high-level interface for interacting with the instrument. @@ -60,6 +60,13 @@ class Keithley2450(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ + def __init__(self, adapter, name="Keithley 2450 SourceMeter", **kwargs): + super().__init__( + adapter, + name, + **kwargs + ) + source_mode = Instrument.control( ":SOUR:FUNC?", ":SOUR:FUNC %s", """ A string property that controls the source mode, which can @@ -366,13 +373,6 @@ class Keithley2450(KeithleyBuffer, SCPIUnknownMixin, Instrument): # Methods # #################### - def __init__(self, adapter, name="Keithley 2450 SourceMeter", **kwargs): - super().__init__( - adapter, name, - includeSCPI=True, - **kwargs - ) - def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ diff --git a/pymeasure/instruments/keithley/keithley2700.py b/pymeasure/instruments/keithley/keithley2700.py index c0ab44c7a5..02f5803cbf 100644 --- a/pymeasure/instruments/keithley/keithley2700.py +++ b/pymeasure/instruments/keithley/keithley2700.py @@ -99,6 +99,16 @@ class Keithley2700(KeithleyBuffer, SCPIUnknownMixin, Instrument): CLIST_VALUES = list(range(101, 300)) + def __init__(self, adapter, name="Keithley 2700 MultiMeter/Switch System", **kwargs): + super().__init__( + adapter, + name, + **kwargs + ) + + self.check_errors() + self.determine_valid_channels() + # Routing commands closed_channels = Instrument.control( "ROUTe:MULTiple:CLOSe?", "ROUTe:MULTiple:CLOSe %s", @@ -140,16 +150,6 @@ def open_all_channels(self): """ self.write(":ROUTe:OPEN:ALL") - def __init__(self, adapter, name="Keithley 2700 MultiMeter/Switch System", **kwargs): - super().__init__( - adapter, name, - includeSCPI=True, - **kwargs - ) - - self.check_errors() - self.determine_valid_channels() - def determine_valid_channels(self): """ Determine what cards are installed into the Keithley 2700 and from that determine what channels are valid. diff --git a/pymeasure/instruments/keithley/keithley6221.py b/pymeasure/instruments/keithley/keithley6221.py index 31867c0583..b2dfcacfe1 100644 --- a/pymeasure/instruments/keithley/keithley6221.py +++ b/pymeasure/instruments/keithley/keithley6221.py @@ -28,7 +28,7 @@ import numpy as np -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.errors import RangeException from pymeasure.instruments.validators import truncated_range, strict_discrete_set @@ -38,7 +38,7 @@ log.addHandler(logging.NullHandler()) -class Keithley6221(KeithleyBuffer, SCPIUnknownMixin, Instrument): +class Keithley6221(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 6221 AC and DC current source and provides a high-level interface for interacting with the instrument. @@ -73,6 +73,12 @@ class Keithley6221(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ + def __init__(self, adapter, name="Keithley 6221 SourceMeter", **kwargs): + super().__init__( + adapter, + name, + **kwargs) + ########## # OUTPUT # ########## @@ -285,13 +291,6 @@ def define_arbitary_waveform(self, datapoints, location=1): # Select the newly made arbitrary waveform as waveform function self.waveform_function = "arbitrary%d" % location - def __init__(self, adapter, name="Keithley 6221 SourceMeter", **kwargs): - super().__init__( - adapter, name, - includeSCPI=True, - **kwargs - ) - def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ diff --git a/pymeasure/instruments/keithley/keithley6517b.py b/pymeasure/instruments/keithley/keithley6517b.py index b566a1cbce..5215151826 100644 --- a/pymeasure/instruments/keithley/keithley6517b.py +++ b/pymeasure/instruments/keithley/keithley6517b.py @@ -29,7 +29,7 @@ import numpy as np -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import truncated_range from .buffer import KeithleyBuffer @@ -37,7 +37,7 @@ log.addHandler(logging.NullHandler()) -class Keithley6517B(KeithleyBuffer, SCPIUnknownMixin, Instrument): +class Keithley6517B(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 6517B ElectroMeter and provides a high-level interface for interacting with the instrument. @@ -61,6 +61,12 @@ class Keithley6517B(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ + def __init__(self, adapter, name="Keithley 6517B Electrometer/High Resistance Meter", **kwargs): + super().__init__( + adapter, name, + **kwargs + ) + source_enabled = Instrument.measurement( "OUTPUT?", """ Reads a boolean value that is True if the source is enabled. """, @@ -193,13 +199,6 @@ def extract_value(result): # Methods # #################### - def __init__(self, adapter, name="Keithley 6517B Electrometer/High Resistance Meter", **kwargs): - super().__init__( - adapter, name, - includeSCPI=True, - **kwargs - ) - def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ diff --git a/pymeasure/instruments/keithley/keithleyDMM6500.py b/pymeasure/instruments/keithley/keithleyDMM6500.py index d9fab89b3b..3925bb3f41 100644 --- a/pymeasure/instruments/keithley/keithleyDMM6500.py +++ b/pymeasure/instruments/keithley/keithleyDMM6500.py @@ -220,9 +220,11 @@ class KeithleyDMM6500(SCPIMixin, Instrument): def __init__( self, adapter, name="Keithley DMM6500 6½-Digit Multimeter", read_termination="\n", **kwargs ): - super().__init__(adapter, name, read_termination=read_termination, - includeSCPI=True, - **kwargs) + super().__init__( + adapter, + name, + read_termination=read_termination, + **kwargs) self.command_set = "SCPI" def __exit__(self, exc_type, exc_value, traceback): diff --git a/pymeasure/instruments/redpitaya/redpitaya_scpi.py b/pymeasure/instruments/redpitaya/redpitaya_scpi.py index c31ff4bea5..08db8d9a9c 100644 --- a/pymeasure/instruments/redpitaya/redpitaya_scpi.py +++ b/pymeasure/instruments/redpitaya/redpitaya_scpi.py @@ -26,7 +26,7 @@ import datetime import numpy as np -from pymeasure.instruments import Instrument, Channel +from pymeasure.instruments import Instrument, Channel, SCPIMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set import logging @@ -158,7 +158,7 @@ def _read_from_binary(self) -> np.ndarray: return max_range * data / (2**16 - 1) - max_range / 2 -class RedPitayaScpi(Instrument): +class RedPitayaScpi(SCPIMixin, Instrument): """This is the class for the Redpitaya reconfigurable board The instrument is accessed using a TCP/IP Socket communication, that is an adapter in the form: @@ -197,7 +197,6 @@ def __init__(self, name, read_termination=read_termination, write_termination=write_termination, - includeSCPI=True, **kwargs) dioN = Instrument.MultiChannelCreator(DigitalChannelN, list(range(7)), prefix='dioN') diff --git a/pymeasure/instruments/teledyne/teledyneT3AFG.py b/pymeasure/instruments/teledyne/teledyneT3AFG.py index a73268383c..d78d529dd7 100644 --- a/pymeasure/instruments/teledyne/teledyneT3AFG.py +++ b/pymeasure/instruments/teledyne/teledyneT3AFG.py @@ -24,7 +24,7 @@ import logging -from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin +from pymeasure.instruments import Instrument, Channel, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) @@ -122,7 +122,7 @@ class SignalChannel(Channel): ) -class TeledyneT3AFG(SCPIUnknownMixin, Instrument): +class TeledyneT3AFG(SCPIMixin, Instrument): """Represents the Teledyne T3AFG series of arbitrary waveform generator interface for interacting with the instrument. @@ -153,7 +153,8 @@ class TeledyneT3AFG(SCPIUnknownMixin, Instrument): def __init__(self, adapter, name="Teledyne T3AFG", **kwargs): super().__init__( - adapter, name, includeSCPI=True, + adapter, + name, tcpip={'read_termination': '\n'}, **kwargs ) From a176dcc3c1efd1e9c356aed5c3defe2e7e0e26cf Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:22:47 +0100 Subject: [PATCH 036/181] Format init. --- pymeasure/instruments/hcp/tc038.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pymeasure/instruments/hcp/tc038.py b/pymeasure/instruments/hcp/tc038.py index 8d64b3729c..13773ac918 100644 --- a/pymeasure/instruments/hcp/tc038.py +++ b/pymeasure/instruments/hcp/tc038.py @@ -84,12 +84,16 @@ class TC038(Instrument): def __init__(self, adapter, name="TC038", address=1, timeout=1000, **kwargs): - - super().__init__(adapter, name, timeout=timeout, - write_termination="\r", read_termination="\r", - parity=Parity.even, - includeSCPI=False, - **kwargs) + super().__init__( + adapter, + name, + timeout=timeout, + write_termination="\r", + read_termination="\r", + parity=Parity.even, + includeSCPI=False, + **kwargs, + ) self.address = address self.set_monitored_quantity() # start to monitor the temperature From 8553d7b9a06a33cfc3d99ce1ced4443e93f1319c Mon Sep 17 00:00:00 2001 From: mxz50 Date: Sat, 2 Mar 2024 19:52:38 -0800 Subject: [PATCH 037/181] add HP33120A class vars for burst modulation parameters --- pymeasure/instruments/hp/hp33120A.py | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pymeasure/instruments/hp/hp33120A.py b/pymeasure/instruments/hp/hp33120A.py index b09efdee4f..db517f9a88 100644 --- a/pymeasure/instruments/hp/hp33120A.py +++ b/pymeasure/instruments/hp/hp33120A.py @@ -112,6 +112,54 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** values=AMPLITUDE_UNITS, map_values=True ) + burst_state = Instrument.control( + "BM:STATE?", "BM:STATE %d", + """Enable or disable burst modulation""", + validator=strict_discrete_set, + values=[0, 1], + ) + burst_source = Instrument.control( + "BM:SOURCE?", "BM:SOURCE %s", + """Select an internal or external gate source for burst modulation""", + validator=strict_discrete_set, + values=['INT', 'EXT'], + ) + burst_count = Instrument.control( + "BM:NCYC?", "BM:NCYC %d", + """Sets the number of cycles per burst (1 to 50,000 cycles)""", + ) + min_burst_count = Instrument.measurement( + "BM:NCYC? MIN", + """Get the minimum :attr:`~.HP33120A.burst_count`""" + ) + max_burst_count = Instrument.measurement( + "BM:NCYC? MAX", + """Get the maximum :attr:`~.HP33120A.burst_count`""" + ) + burst_rate = Instrument.control( + "BM:INT:RATE?", "BM:INT:RATE %g", + """Set the burst rate in Hz fo an internal burst source""" + ) + min_burst_rate = Instrument.measurement( + "BM:INT:RATE? MIN", + """Get the minimum :attr:`~.HP33120A.burst_rate`""" + ) + max_burst_rate = Instrument.measurement( + "BM:INT:RATE? MAX", + """Get the maximum :attr:`~.HP33120A.burst_rate`""" + ) + burst_phase = Instrument.control( + "BM:PHAS?", "BM:PHAS %g", + """Set the starting phase angle of a burst (-360 to +360 degrees)""" + ) + min_burst_phase = Instrument.measurement( + "BM:PHAS? MIN", + """Get the minimum :attr:`~.HP33120A.burst_phase`""" + ) + max_burst_phase = Instrument.measurement( + "BM:PHAS? MAX", + """Get the maximum :attr:`~.HP33120A.burst_phase`""" + ) def beep(self): """ Causes a system beep. """ From 900d4a112c5216f07a61a9f97cf3e66c791424df Mon Sep 17 00:00:00 2001 From: Matthew Zenaldin Date: Wed, 6 Mar 2024 17:44:43 -0700 Subject: [PATCH 038/181] change burst_state to burst_enabled; add value maps --- pymeasure/instruments/hp/hp33120A.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/hp/hp33120A.py b/pymeasure/instruments/hp/hp33120A.py index db517f9a88..ceac5cf2c0 100644 --- a/pymeasure/instruments/hp/hp33120A.py +++ b/pymeasure/instruments/hp/hp33120A.py @@ -57,6 +57,7 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** values=SHAPES, map_values=True ) + frequency = Instrument.control( "SOUR:FREQ?", "SOUR:FREQ %g", """Control the frequency of the @@ -64,14 +65,17 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** and can be queried with :attr:`~.max_frequency` and :attr:`~.min_frequency`. (float)""" ) + max_frequency = Instrument.measurement( "SOUR:FREQ? MAX", """ Get the maximum :attr:`~.HP33120A.frequency` in Hz for the given shape """ ) + min_frequency = Instrument.measurement( "SOUR:FREQ? MIN", """ Get the minimum :attr:`~.HP33120A.frequency` in Hz for the given shape """ ) + amplitude = Instrument.control( "SOUR:VOLT?", "SOUR:VOLT %g", """ Control the voltage amplitude of the @@ -80,28 +84,34 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** on the waveform shape and can be queried with :attr:`~.max_amplitude` and :attr:`~.min_amplitude`. (float)""" ) + max_amplitude = Instrument.measurement( "SOUR:VOLT? MAX", """ Get the maximum :attr:`~.amplitude` in Volts for the given shape """ ) + min_amplitude = Instrument.measurement( "SOUR:VOLT? MIN", """ Get the minimum :attr:`~.amplitude` in Volts for the given shape """ ) + offset = Instrument.control( "SOUR:VOLT:OFFS?", "SOUR:VOLT:OFFS %g", """ Control the amplitude voltage offset in Volts. The allowed range depends on the waveform shape and can be queried with :attr:`~.max_offset` and :attr:`~.min_offset`. """ ) + max_offset = Instrument.measurement( "SOUR:VOLT:OFFS? MAX", """ Get the maximum :attr:`~.offset` in Volts for the given shape """ ) + min_offset = Instrument.measurement( "SOUR:VOLT:OFFS? MIN", """ Get the minimum :attr:`~.offset` in Volts for the given shape """ ) + AMPLITUDE_UNITS = {'Vpp': 'VPP', 'Vrms': 'VRMS', 'dBm': 'DBM', 'default': 'DEF'} amplitude_units = Instrument.control( "SOUR:VOLT:UNIT?", "SOUR:VOLT:UNIT %s", @@ -112,55 +122,68 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** values=AMPLITUDE_UNITS, map_values=True ) - burst_state = Instrument.control( + + burst_enabled = Instrument.control( "BM:STATE?", "BM:STATE %d", """Enable or disable burst modulation""", validator=strict_discrete_set, - values=[0, 1], + values={True: 1, False: 0}, + map_values=True ) + burst_source = Instrument.control( "BM:SOURCE?", "BM:SOURCE %s", """Select an internal or external gate source for burst modulation""", validator=strict_discrete_set, values=['INT', 'EXT'], ) + burst_count = Instrument.control( "BM:NCYC?", "BM:NCYC %d", """Sets the number of cycles per burst (1 to 50,000 cycles)""", ) + min_burst_count = Instrument.measurement( "BM:NCYC? MIN", """Get the minimum :attr:`~.HP33120A.burst_count`""" ) + max_burst_count = Instrument.measurement( "BM:NCYC? MAX", """Get the maximum :attr:`~.HP33120A.burst_count`""" ) + burst_rate = Instrument.control( "BM:INT:RATE?", "BM:INT:RATE %g", """Set the burst rate in Hz fo an internal burst source""" ) + min_burst_rate = Instrument.measurement( "BM:INT:RATE? MIN", """Get the minimum :attr:`~.HP33120A.burst_rate`""" ) + max_burst_rate = Instrument.measurement( "BM:INT:RATE? MAX", """Get the maximum :attr:`~.HP33120A.burst_rate`""" ) + burst_phase = Instrument.control( "BM:PHAS?", "BM:PHAS %g", """Set the starting phase angle of a burst (-360 to +360 degrees)""" ) + min_burst_phase = Instrument.measurement( "BM:PHAS? MIN", """Get the minimum :attr:`~.HP33120A.burst_phase`""" ) + max_burst_phase = Instrument.measurement( "BM:PHAS? MAX", """Get the maximum :attr:`~.HP33120A.burst_phase`""" ) + def beep(self): """ Causes a system beep. """ self.write("SYST:BEEP") From c9190d9c9a1ade79f01433d6ba49e3242ba57c8f Mon Sep 17 00:00:00 2001 From: Matthew Zenaldin Date: Wed, 6 Mar 2024 17:45:36 -0700 Subject: [PATCH 039/181] protocol test for hp33120a awg --- tests/instruments/hp/test_hp33120A.py | 224 ++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 tests/instruments/hp/test_hp33120A.py diff --git a/tests/instruments/hp/test_hp33120A.py b/tests/instruments/hp/test_hp33120A.py new file mode 100644 index 0000000000..1d10326bd4 --- /dev/null +++ b/tests/instruments/hp/test_hp33120A.py @@ -0,0 +1,224 @@ +import pytest + +from pymeasure.test import expected_protocol +from pymeasure.instruments.hp import HP33120A + + +def test_init(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None)], + ): + pass # Verify the expected communication. + + +def test_amplitude_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT 4', None)], + ) as inst: + inst.amplitude = 4 + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT?', b'+4.00000E+00')], + 4.0), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT?', b'+4.00000E+00')], + 4.0), +)) +def test_amplitude_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.amplitude == value + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT:UNIT?', b'VPP')], + 'Vpp'), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT:UNIT?', b'VPP')], + 'Vpp'), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT:UNIT?', b'VPP')], + 'Vpp'), +)) +def test_amplitude_units_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.amplitude_units == value + + +def test_burst_count_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:NCYC 500', None)], + ) as inst: + inst.burst_count = 500 + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:NCYC?', b'+1.00000E+00')], + 1.0), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:NCYC?', b'+5.00000E+02')], + 500.0), +)) +def test_burst_count_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.burst_count == value + + +def test_burst_enabled_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:STATE 1', None)], + ) as inst: + inst.burst_enabled = True + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:STATE?', b'1')], + True), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:STATE?', b'1')], + True), +)) +def test_burst_enabled_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.burst_enabled == value + + +def test_burst_phase_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:PHAS 20', None)], + ) as inst: + inst.burst_phase = 20 + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:PHAS?', b'+0.00000E+00')], + 0.0), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:PHAS?', b'+2.00000E+01')], + 20.0), +)) +def test_burst_phase_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.burst_phase == value + + +def test_burst_rate_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:INT:RATE 250', None)], + ) as inst: + inst.burst_rate = 250 + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:INT:RATE?', b'+1.00000E+02')], + 100.0), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:INT:RATE?', b'+2.50000E+02')], + 250.0), +)) +def test_burst_rate_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.burst_rate == value + + +def test_burst_source_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:SOURCE INT', None)], + ) as inst: + inst.burst_source = 'INT' + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:SOURCE?', b'INT')], + 'INT'), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'BM:SOURCE?', b'INT')], + 'INT'), +)) +def test_burst_source_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.burst_source == value + + +def test_frequency_setter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:FREQ 2000', None)], + ) as inst: + inst.frequency = 2000.0 + + +@pytest.mark.parametrize("comm_pairs, value", ( + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:FREQ?', b'+2.00000000000E+03')], + 2000.0), + ([(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:FREQ?', b'+2.00000000000E+03')], + 2000.0), +)) +def test_frequency_getter(comm_pairs, value): + with expected_protocol( + HP33120A, + comm_pairs, + ) as inst: + assert inst.frequency == value + + +def test_offset_getter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:VOLT:OFFS?', b'+0.00000E+00')], + ) as inst: + assert inst.offset == 0.0 + + +def test_shape_getter(): + with expected_protocol( + HP33120A, + [(b'SOUR:VOLT:UNIT VPP', None), + (b'SOUR:FUNC:SHAP?', b'SIN')], + ) as inst: + assert inst.shape == 'sinusoid' From 8563d078a8fda19546246e950ce1f4cdbdb1b21a Mon Sep 17 00:00:00 2001 From: Matthew Zenaldin Date: Wed, 6 Mar 2024 17:50:55 -0700 Subject: [PATCH 040/181] update control method docstrings to follow pymeasure convention --- pymeasure/instruments/hp/hp33120A.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/hp/hp33120A.py b/pymeasure/instruments/hp/hp33120A.py index ceac5cf2c0..563d883f98 100644 --- a/pymeasure/instruments/hp/hp33120A.py +++ b/pymeasure/instruments/hp/hp33120A.py @@ -125,7 +125,7 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** burst_enabled = Instrument.control( "BM:STATE?", "BM:STATE %d", - """Enable or disable burst modulation""", + """Control state of burst modulation""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True @@ -133,14 +133,14 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** burst_source = Instrument.control( "BM:SOURCE?", "BM:SOURCE %s", - """Select an internal or external gate source for burst modulation""", + """Control internal or external gate source for burst modulation""", validator=strict_discrete_set, values=['INT', 'EXT'], ) burst_count = Instrument.control( "BM:NCYC?", "BM:NCYC %d", - """Sets the number of cycles per burst (1 to 50,000 cycles)""", + """Control the number of cycles per burst (1 to 50,000 cycles)""", ) min_burst_count = Instrument.measurement( @@ -155,7 +155,7 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** burst_rate = Instrument.control( "BM:INT:RATE?", "BM:INT:RATE %g", - """Set the burst rate in Hz fo an internal burst source""" + """Control the burst rate in Hz fo an internal burst source""" ) min_burst_rate = Instrument.measurement( @@ -170,7 +170,7 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** burst_phase = Instrument.control( "BM:PHAS?", "BM:PHAS %g", - """Set the starting phase angle of a burst (-360 to +360 degrees)""" + """Control the starting phase angle of a burst (-360 to +360 degrees)""" ) min_burst_phase = Instrument.measurement( From 3dfd51a8b9c036cfe2a0e900991fce236da1156e Mon Sep 17 00:00:00 2001 From: Matthew Zenaldin Date: Wed, 6 Mar 2024 18:35:07 -0700 Subject: [PATCH 041/181] add name to authors.txt --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index d31d2ffff0..487bcfc17d 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -72,3 +72,4 @@ Heinz-Alexander Fütterer Per-Olof Svensson Karl Komierowski Alec Vercruysse +Matthew Zenaldin \ No newline at end of file From 1510807378ec26d03efc0a70ce9e35b0e83af4c2 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Mar 2024 07:32:55 -0800 Subject: [PATCH 042/181] delete superfluous new line --- pymeasure/instruments/hp/hp33120A.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pymeasure/instruments/hp/hp33120A.py b/pymeasure/instruments/hp/hp33120A.py index 563d883f98..176582d1ac 100644 --- a/pymeasure/instruments/hp/hp33120A.py +++ b/pymeasure/instruments/hp/hp33120A.py @@ -183,7 +183,6 @@ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", ** """Get the maximum :attr:`~.HP33120A.burst_phase`""" ) - def beep(self): """ Causes a system beep. """ self.write("SYST:BEEP") From 97c2090580a90033eebaf048a74e9179ac1d9e93 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Mar 2024 07:36:19 -0800 Subject: [PATCH 043/181] add author --- AUTHORS.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 487bcfc17d..3d3c51f6ab 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -72,4 +72,5 @@ Heinz-Alexander Fütterer Per-Olof Svensson Karl Komierowski Alec Vercruysse -Matthew Zenaldin \ No newline at end of file +Matthew Zenaldin +Canyon Clark From ffc63b0b4b04c0de094fa1465dcd5d18aae1c0c2 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 8 Mar 2024 07:34:11 -0800 Subject: [PATCH 044/181] remove repetitive tests --- tests/instruments/hp/test_hp33120A.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/instruments/hp/test_hp33120A.py b/tests/instruments/hp/test_hp33120A.py index 1d10326bd4..b1533faa3f 100644 --- a/tests/instruments/hp/test_hp33120A.py +++ b/tests/instruments/hp/test_hp33120A.py @@ -25,9 +25,6 @@ def test_amplitude_setter(): ([(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:VOLT?', b'+4.00000E+00')], 4.0), - ([(b'SOUR:VOLT:UNIT VPP', None), - (b'SOUR:VOLT?', b'+4.00000E+00')], - 4.0), )) def test_amplitude_getter(comm_pairs, value): with expected_protocol( @@ -41,12 +38,6 @@ def test_amplitude_getter(comm_pairs, value): ([(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:VOLT:UNIT?', b'VPP')], 'Vpp'), - ([(b'SOUR:VOLT:UNIT VPP', None), - (b'SOUR:VOLT:UNIT?', b'VPP')], - 'Vpp'), - ([(b'SOUR:VOLT:UNIT VPP', None), - (b'SOUR:VOLT:UNIT?', b'VPP')], - 'Vpp'), )) def test_amplitude_units_getter(comm_pairs, value): with expected_protocol( @@ -95,8 +86,8 @@ def test_burst_enabled_setter(): (b'BM:STATE?', b'1')], True), ([(b'SOUR:VOLT:UNIT VPP', None), - (b'BM:STATE?', b'1')], - True), + (b'BM:STATE?', b'0')], + False), )) def test_burst_enabled_getter(comm_pairs, value): with expected_protocol( @@ -169,9 +160,6 @@ def test_burst_source_setter(): ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:SOURCE?', b'INT')], 'INT'), - ([(b'SOUR:VOLT:UNIT VPP', None), - (b'BM:SOURCE?', b'INT')], - 'INT'), )) def test_burst_source_getter(comm_pairs, value): with expected_protocol( From 9358939a1a6406af2e78f74ba6a7edb7f2f4cdbd Mon Sep 17 00:00:00 2001 From: Connor Carr Date: Mon, 11 Mar 2024 10:14:26 -0400 Subject: [PATCH 045/181] Initial Agilent 4284A driver --- pymeasure/instruments/agilent/agilent4284A.py | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 pymeasure/instruments/agilent/agilent4284A.py diff --git a/pymeasure/instruments/agilent/agilent4284A.py b/pymeasure/instruments/agilent/agilent4284A.py new file mode 100644 index 0000000000..ea1c085a25 --- /dev/null +++ b/pymeasure/instruments/agilent/agilent4284A.py @@ -0,0 +1,193 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.instruments import Instrument +from pymeasure.instruments.agilent import AgilentE4980 +from pymeasure.instruments.validators import strict_discrete_set, truncated_range +from time import sleep + +IMPEDANCE_MODES = ( + "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", + "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR" +) + + +class Agilent4284A(AgilentE4980): + """Represents the Agilent 4284A precision LCR meter. + + Most attributes are inherited from the Agilent E4980; a couple are overwritten + to accomodate slight differences between the instruments. + + .. code-block:: python + + agilent = Agilent4284A("GPIB::1") + + agilent.mode = 'ZTR' # Set impedance mode to measure + impedance magnitude [Ohm] and phase [rad] + agilent. + """ + + def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): + super().__init__(adapter, name, includeSCPI=True, **kwargs) + + frequency = Instrument.control( + "FREQ?", "FREQ %g", + """Control AC frequency in Hertz.""", + validator=truncated_range, + values=(20, 1e6), + ) + + ac_current = Instrument.control( + "CURR:LEV?", "CURR:LEV %g", + """"Control AC current level in Amps. Range is 50 uA to 20 mA for default, 50 uA + to 200 mA in high-power mode.""", + validator=truncated_range, + values=(50e-6, 0.02), + dynamic=True + ) + + ac_voltage = Instrument.control( + "VOLT:LEV?", "VOLT:LEV %g", + """"Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to + 20 V in high-power mode.""", + validator=truncated_range, + values=(0.005, 0.02), + dynamic=True + ) + + high_power_enabled = Instrument.control( + "OUTP:HPOW?", "OUTP:HPOW %d", + """Control whether the high-power mode is enabled. + Requires Option 001 (power amplifier / DC bias) is installed.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True + ) + + bias_enabled = Instrument.control( + "BIAS:STAT?", "BIAS:STAT %d", + """Control whether DC bias is enabled.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True + ) + + bias_voltage = Instrument.control( + "BIAS:VOLT?", "BIAS:VOLT %g", + """Control the DC bias voltage in Volts. + Maximum is 2 V by default, 40 V in high-power mode.""", + validator=truncated_range, + values=(0, 2), + ) + + bias_current = Instrument.control( + "BIAS:CURR?", "BIAS:CURR %g", + """Control the DC bias current in Amps. + Requires Option 001 (power amplifier / DC bias) is installed.""", + validator=truncated_range, + values=(0, 0.1) + ) + + impedance_mode = Instrument.control( + "FUNC:IMP?", "FUNC:IMP %s", + """Control impedance measurement function. + + * CPD: Parallel capacitance [F] and dissipation factor [number] + * CPQ: Parallel capacitance [F] and quality factor [number] + * CPG: Parallel capacitance [F] and parallel conductance [S] + * CPRP: Parallel capacitance [F] and parallel resistance [Ohm] + + * CSD: Series capacitance [F] and dissipation factor [number] + * CSQ: Series capacitance [F] and quality factor [number] + * CSRS: Series capacitance [F] and series resistance [Ohm] + + * LPQ: Parallel inductance [H] and quality factor [number] + * LPD: Parallel inductance [H] and dissipation factor [number] + * LPG: Parallel inductance [H] and parallel conductance [S] + * LPRP: Parallel inductance [H] and parallel resistance [Ohm] + + * LSD: Series inductance [H] and dissipation factor [number] + * LSQ: Seriesinductance [H] and quality factor [number] + * LSRS: Series inductance [H] and series resistance [Ohm] + + * RX: Resistance [Ohm] and reactance [Ohm] + * ZTD: Impedance, magnitude [Ohm] and phase [deg] + * ZTR: Impedance, magnitude [Ohm] and phase [rad] + * GB: Conductance [S] and susceptance [S] + * YTD: Admittance, magnitude [Ohm] and phase [deg] + * YTR: Admittance magnitude [Ohm] and phase [rad]""", + validator=strict_discrete_set, + values=IMPEDANCE_MODES + ) + + impedance_range = Instrument.control( + "FUNC:IMP:RANG?", "FUNC:IMP:RANG %g", + """Control the impedance measurement range. The 4284A will select an appropriate + measurement range for the setting value.""" + ) + + auto_range_enabled = Instrument.control( + "FUNC:IMP:RANG:AUTO?", "FUNC:IMP:RANG:AUTO %d", + """Control whether the impedance auto range is enabled.""", + validator=strict_discrete_set, + values={False: 0, True: 1} + ) + + def freq_sweep(self, freq_list, return_freq=False): + """Run frequency list sweep using sequential trigger. + + :param freq_list: list of frequencies + :param return_freq: if True, returns the frequencies read from the instrument + + Returns values as configured with :attr:`~.Agilent4284A.mode` + """ + # self.write("*RST;*CLS") + self.write("TRIG:SOUR BUS") + self.write("DISP:PAGE LIST") + self.write("FORM ASC") + # trigger in sequential mode + self.write("LIST:MODE SEQ") + lista_str = ",".join(['%e' % f for f in freq_list]) + self.write("LIST:FREQ %s" % lista_str) + # trigger + self.write("INIT:CONT ON") + self.write("TRIG:IMM") + while True: + status = self.read("STAT:OPER?") + if (status & 8) == 8: # bit no. 3 is list sweep measurement complete bit + break + else: + sleep(1) + continue + measured = self.values("FETCH?") + # at the end return to manual trigger + self.write(":TRIG:SOUR HOLD") + # gets 4-ples of numbers, first two are data A and B + a_data = [measured[_] for _ in range(0, 4 * len(freq_list), 4)] + b_data = [measured[_] for _ in range(1, 4 * len(freq_list), 4)] + if return_freq: + read_freqs = self.values("LIST:FREQ?") + return a_data, b_data, read_freqs + else: + return a_data, b_data From fb3eec188d59860872fcba50345edbd1fe162886 Mon Sep 17 00:00:00 2001 From: Connor Carr <67078909+ConnorGCarr@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:19:34 -0400 Subject: [PATCH 046/181] dev/keithley 2182 (#1043) * Create Keithley 2182, added self to authors * Bug fix buffer, edits to 2182 * Implemented Keithley 2182 channels. * Added documentation, pytest; linting and bug fixing * add keithley 2182 to doc index * driver bug fix * Fix command string formatting * Fix docstrings, additional edits for clarity. * Fix auto line frequency, copyright year * Remove redundant SCPI commands, clarify boolean properties, formatting * remove redundant properties, fix docstring linting * fix tests * fix tests * edits to driver * Create Keithley 2182, added self to authors * Bug fix buffer, edits to 2182 * Implemented Keithley 2182 channels. * Added documentation, pytest; linting and bug fixing * add keithley 2182 to doc index * driver bug fix * Fix command string formatting * Fix docstrings, additional edits for clarity. * Fix auto line frequency, copyright year * Remove redundant SCPI commands, clarify boolean properties, formatting * remove redundant properties, fix docstring linting * fix tests * fix tests * edits to driver * SCPI mixin --- AUTHORS.txt | 1 + docs/api/instruments/keithley/index.rst | 1 + .../api/instruments/keithley/keithley2182.rst | 12 + pymeasure/instruments/keithley/__init__.py | 1 + pymeasure/instruments/keithley/buffer.py | 11 +- .../instruments/keithley/keithley2182.py | 389 ++++++++++++++++++ .../instruments/keithley/test_keithley2182.py | 104 +++++ 7 files changed, 513 insertions(+), 6 deletions(-) create mode 100644 docs/api/instruments/keithley/keithley2182.rst create mode 100644 pymeasure/instruments/keithley/keithley2182.py create mode 100644 tests/instruments/keithley/test_keithley2182.py diff --git a/AUTHORS.txt b/AUTHORS.txt index 3d3c51f6ab..af14a2e339 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -74,3 +74,4 @@ Karl Komierowski Alec Vercruysse Matthew Zenaldin Canyon Clark +Connor Carr diff --git a/docs/api/instruments/keithley/index.rst b/docs/api/instruments/keithley/index.rst index 08eaf1cf59..a88255fffb 100644 --- a/docs/api/instruments/keithley/index.rst +++ b/docs/api/instruments/keithley/index.rst @@ -21,3 +21,4 @@ This section contains specific documentation on the Keithley instruments that ar keithley2600 keithley2200 keithleyDMM6500 + keithley2182 diff --git a/docs/api/instruments/keithley/keithley2182.rst b/docs/api/instruments/keithley/keithley2182.rst new file mode 100644 index 0000000000..522c2923d3 --- /dev/null +++ b/docs/api/instruments/keithley/keithley2182.rst @@ -0,0 +1,12 @@ +########################### +Keithley 2182 Nanovoltmeter +########################### + +.. autoclass:: pymeasure.instruments.keithley.Keithley2182 + :members: + :show-inheritance: + :inherited-members: CommonBase + +.. autoclass:: pymeasure.instruments.keithley.keithley2182.Keithley2182Channel + :members: + :show-inheritance: \ No newline at end of file diff --git a/pymeasure/instruments/keithley/__init__.py b/pymeasure/instruments/keithley/__init__.py index 412e384a73..c41a651302 100644 --- a/pymeasure/instruments/keithley/__init__.py +++ b/pymeasure/instruments/keithley/__init__.py @@ -34,3 +34,4 @@ from .keithley6517b import Keithley6517B from .keithley2200 import Keithley2200 from .keithleyDMM6500 import KeithleyDMM6500 +from .keithley2182 import Keithley2182 diff --git a/pymeasure/instruments/keithley/buffer.py b/pymeasure/instruments/keithley/buffer.py index d405c1e2b5..7cd7662808 100644 --- a/pymeasure/instruments/keithley/buffer.py +++ b/pymeasure/instruments/keithley/buffer.py @@ -41,9 +41,8 @@ class KeithleyBuffer: buffer_points = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", - """ An integer property that controls the number of buffer points. This - does not represent actual points in the buffer, but the configuration - value instead. """, + """ Control the number of buffer points. This does not represent actual points + in the buffer, but the configuration value instead. """, validator=truncated_range, values=[2, 1024], cast=int @@ -68,8 +67,8 @@ def config_buffer(self, points=64, delay=0): def is_buffer_full(self): """ Returns True if the buffer is full of measurements. """ - status_bit = int(self.ask("*STB?")) - return status_bit == 65 + status_byte = int(self.ask("*STB?")) + return (status_byte & 65) == 65 def wait_for_buffer(self, should_stop=lambda: False, timeout=60, interval=0.1): @@ -93,7 +92,7 @@ def wait_for_buffer(self, should_stop=lambda: False, @property def buffer_data(self): - """ Returns a numpy array of values from the buffer. """ + """ Get a numpy array of values from the buffer. """ self.write(":FORM:DATA ASCII") return np.array(self.values(":TRAC:DATA?"), dtype=np.float64) diff --git a/pymeasure/instruments/keithley/keithley2182.py b/pymeasure/instruments/keithley/keithley2182.py new file mode 100644 index 0000000000..28f2555309 --- /dev/null +++ b/pymeasure/instruments/keithley/keithley2182.py @@ -0,0 +1,389 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging + +from pymeasure.instruments import Instrument, Channel, SCPIMixin +from pymeasure.instruments.validators import strict_discrete_set, strict_range + +from .buffer import KeithleyBuffer + + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +class Keithley2182Channel(Channel): + """Implementation of a Keithley 2182 channel. + + Channel 1 is the fundamental measurement channel, while channel 2 provides + sense measurements. Channel 2 inputs are referenced to Channel 1 LO. + Possible configurations are + + Voltage (Channel 1) + Temperature (Channel 1) + Voltage (Channel 1) and Voltage (Channel 2) + Voltage (Channel 1) and Temperature (Channel 2) + """ + + def __init__(self, parent, id): + """Set max voltage depending on channel.""" + if id == 1: + self.voltage_range_values = (0, 120) + self.voltage_offset_values = (-120, 120) + else: + self.voltage_range_values = (0, 12) + self.voltage_offset_values = (-12, 12) + super().__init__(parent, id) + + voltage_range = Channel.control( + ":SENS:VOLT:CHAN{ch}:RANG?", ":SENS:VOLT:CHAN{ch}:RANG %g", + """Control the positive full-scale measurement voltage range in Volts. + The Keithley 2182 selects a measurement range based on the expected voltage. + DCV1 has five ranges: 10 mV, 100 mV, 1 V, 10 V, and 100 V. + DCV2 has three ranges: 100 mV, 1 V, and 10 V. + Valid limits are from 0 to 120 V for Ch. 1, and 0 to 12 V for Ch. 2. + Auto-range is automatically disabled when this property is set.""", + validator=strict_range, + values=(0, 120), + dynamic=True, + ) + + voltage_range_auto_enabled = Channel.control( + ":SENS:VOLT:CHAN{ch}:RANG:AUTO?", ":SENS:VOLT:CHAN{ch}:RANG:AUTO %d", + """Control the auto voltage ranging option (bool).""", + validator=strict_discrete_set, + values={True: 1, False: 0}, + map_values=True, + ) + + voltage_offset = Channel.control( + ":SENS:VOLT:CHAN{ch}:REF?", ":SENS:VOLT:CHAN{ch}:REF %g", + """Control the relative offset for measuring voltage. + Displayed value = actual value - offset value. + Valid ranges are -120 V to +120 V for Ch. 1, and -12 V to +12 V for Ch. 2.""", + validator=strict_range, + values=(-120, 120), + dynamic=True, + ) + + temperature_offset = Channel.control( + ":SENS:TEMP:CHAN{ch}:REF?", ":SENS:TEMP:CHAN{ch}:REF %g", + """Control the relative offset for measuring temperature. + Displayed value = actual value - offset value. + Valid values are -273 C to 1800 C.""", + validator=strict_range, + values=(-273, 1800), + ) + + voltage_offset_enabled = Channel.control( + ":SENS:VOLT:CHAN{ch}:REF:STAT?", ":SENS:VOLT:CHAN{ch}:REF:STAT %s", + """Control whether voltage is measured as a relative or absolute value (bool). + Enabled by default for Ch. 2 voltage, which is measured relative to Ch. 1 + voltage.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True + ) + + temperature_offset_enabled = Channel.control( + ":SENS:TEMP:CHAN{ch}:REF:STAT?", ":SENS:TEMP:CHAN{ch}:REF:STAT %s", + """Control whether temperature is measured as a relative or absolute value (bool). + Disabled by default.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True + ) + + def setup_voltage(self, auto_range=True, nplc=5): + """Set active channel and configure channel for voltage measurement. + + :param auto_range: Enables auto_range if True, else uses set voltage + range + :param nplc: Number of power line cycles (NPLC) from 0.01 to 50/60 + """ + self.write(":SENS:CHAN {ch};" + ":SENS:FUNC 'VOLT';" + f":SENS:VOLT:NPLC {nplc};") + if auto_range: + self.write(":SENS:VOLT:RANG:AUTO 1") + self.check_errors() + + def setup_temperature(self, nplc=5): + """Change active channel and configure channel for temperature measurement. + + :param nplc: Number of power line cycles (NPLC) from 0.01 to 50/60 + """ + self.write(":SENS:CHAN {ch};" + ":SENS:FUNC 'TEMP';" + f":SENS:TEMP:NPLC {nplc}") + self.check_errors() + + def acquire_temperature_reference(self): + """Acquire a temperature measurement and store it as the relative offset value. + + Only acquires reference if temperature offset is enabled. + """ + self.write(":SENS:TEMP:CHAN{ch}:REF:ACQ") + + def acquire_voltage_reference(self): + """Acquire a voltage measurement and store it as the relative offset value. + + Only acquires reference if voltage offset is enabled. + """ + self.write(":SENS:VOLT:CHAN{ch}:REF:ACQ") + + +class Keithley2182(SCPIMixin, KeithleyBuffer, Instrument): + """Represents the Keithley 2182 Nanovoltmeter and provides a + high-level interface for interacting with the instrument. + + .. code-block:: python + + keithley = Keithley2182("GPIB::1") + + keithley.reset() # Return instrument settings to default + values + keithley.thermocouple = 'S' # Sets thermocouple type to S + keithley.active_channel = 1 # Sets channel 1 for active measurement + keithley.channel_function = 'voltage' # Configures active channel for voltage + measurement + print(keithley.voltage) # Prints the voltage in volts + + keithley.ch_1.setup_voltage() # Set channel 1 active and prepare + voltage measurement + keithley.ch_2.setup_temperature() # Set channel 2 active and prepare + temperature measurement + + """ + + def __init__(self, adapter, name="Keithley 2182 Nanovoltmeter", + read_termination='\r', **kwargs): + super().__init__(adapter, name, read_termination=read_termination, **kwargs) + + ch_1 = Instrument.ChannelCreator(Keithley2182Channel, 1) + ch_2 = Instrument.ChannelCreator(Keithley2182Channel, 2) + + ################# + # Configuration # + ################# + + auto_zero_enabled = Instrument.control( + ":SYST:AZER:STAT?", ":SYST:AZER:STAT %d", + """Control the auto zero option (bool).""", + validator=strict_discrete_set, + values={True: 1, False: 0}, + map_values=True, + ) + + line_frequency = Instrument.measurement( + ":SYST:LFR?", + """Get the line frequency in Hertz. Values are 50 or 60. + Cannot be set on 2182.""", + ) + + display_enabled = Instrument.control( + ":DISP:ENAB?", ":DISP:ENAB %d", + """Control whether the front display of the voltmeter is enabled. + Valid values are True and False.""", + validator=strict_discrete_set, + values={True: 1, False: 0}, + map_values=True, + ) + + active_channel = Instrument.control( + ":SENS:CHAN?", ":SENS:CHAN %d", + """Control which channel is active for measurement. + Valid values are 0 (internal temperature sensor), 1, and 2.""", + validator=strict_discrete_set, + values=(0, 1, 2), + cast=int + ) + + channel_function = Instrument.control( + ":SENS:FUNC?", "SENS:FUNC %s", + """Control the measurement mode of the active channel. + Valid options are `voltage` and `temperature`.""", + validator=strict_discrete_set, + values={'voltage': '"VOLT:DC"', 'temperature': '"TEMP"'}, + map_values=True + ) + + ############### + # Voltage (V) # + ############### + + voltage = Instrument.measurement( + ":READ?", + """Measure the voltage in Volts, if active channel is configured for this + reading.""" + ) + + voltage_nplc = Instrument.control( + ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", + """Control the number of power line cycles (NPLC) for voltage measurements, + which sets the integration period and measurement speed. + Valid values are from 0.01 to 50 or 60, depending on the line frequency. + Default is 5.""", + validator=strict_range, + values=(0.01, 60), + dynamic=True, + ) + + ################### + # Temperature (C) # + ################### + + temperature = Instrument.measurement( + ":READ?", + """Measure the temperature in Celsius, if active channel is configured for this + reading.""" + ) + + thermocouple = Instrument.control( + ":SENS:TEMP:TC?", ":SENS:TEMP:TC %s", + """Control the thermocouple type for temperature measurements. + Valid options are B, E, J, K, N, R, S, and T.""", + validator=strict_discrete_set, + values=('B', 'E', 'J', 'K', 'N', 'R', 'S', 'T') + ) + + temperature_nplc = Instrument.control( + ":SENS:TEMP:NPLC?", ":SENS:TEMP:NPLC %g", + """Control the number of power line cycles (NPLC) for temperature measurements, + which sets the integration period and measurement speed. + Valid values are from 0.01 to 50 or 60, depending on the line frequency. + Default is 5.""", + validator=strict_range, + values=(0.01, 60), + dynamic=True, + ) + + temperature_reference_junction = Instrument.control( + ":SENS:TEMP:RJUN:RSEL?", ":SENS:TEMP:RJUN:RSEL %s", + """Control whether the thermocouple reference junction is internal (INT) or + simulated (SIM). Default is INT.""", + validator=strict_discrete_set, + values=('SIM', 'INT'), + ) + + temperature_simulated_reference = Instrument.control( + ":SENS:TEMP:RJUN:SIM?", ":SENS:TEMP:RJUN:SIM %g", + """Control the value of the simulated thermocouple reference junction in + Celsius. Default is 23 C.""", + validator=strict_range, + values=(0, 60), + ) + + internal_temperature = Instrument.measurement( + ":SENS:TEMP:RTEM?", + """Measure the internal temperature in Celsius.""" + ) + + ############## + # Statistics # + ############## + + mean = Instrument.measurement( + ":CALC2:FORM MEAN;:CALC2:STAT ON;:CALC2:IMM?;", + """Get the calculated mean (average) from the buffer data.""" + ) + + maximum = Instrument.measurement( + ":CALC2:FORM MAX;:CALC2:STAT ON;:CALC2:IMM?;", + """Get the calculated maximum from the buffer data.""" + ) + + minimum = Instrument.measurement( + ":CALC2:FORM MIN;:CALC2:STAT ON;:CALC2:IMM?;", + """Get the calculated minimum from the buffer data.""" + ) + + standard_dev = Instrument.measurement( + ":CALC2:FORM SDEV;:CALC2:STAT ON;:CALC2:IMM?;", + """Get the calculated standard deviation from the buffer data.""" + ) + + ########### + # Trigger # + ########### + + trigger_count = Instrument.control( + ":TRIG:COUN?", ":TRIG:COUN %d", + """Control the trigger count which can take values from 1 to 9,999. + Default is 1.""", + validator=strict_range, + values=(1, 9999), + cast=int + ) + + trigger_delay = Instrument.control( + ":TRIG:DEL?", ":TRIG:DEL %g", + """Control the trigger delay in seconds, which can take values from 0 to + 999999.999 s. Default is 0.""", + validator=strict_range, + values=(0, 999999.999) + ) + + ########### + # Methods # + ########### + + def auto_line_frequency(self): + """Set appropriate limits for NPLC voltage and temperature readings.""" + if self.line_frequency == 50: + self.temperature_nplc_values = (0.01, 50) + self.voltage_nplc_values = (0.01, 50) + else: + self.temperature_nplc_values = (0.01, 60) + self.voltage_nplc_values = (0.01, 60) + + def reset(self): + """Reset the instrument and clear the queue.""" + self.write("status:queue:clear;*RST;:stat:pres;:*CLS;") + + def trigger(self): + """Execute a bus trigger, which can be used when :meth:`~.trigger_on_bus` + is configured. + """ + return self.write("*TRG") + + def trigger_immediately(self): + """Configure measurements to be taken with the internal trigger at the maximum + sampling rate. + """ + self.write(":TRIG:SOUR IMM;") + + def trigger_on_bus(self): + """Configure the trigger to detect events based on the bus trigger, which can be + activated by :meth:`~.trigger`. + """ + self.write(":TRIG:SOUR BUS") + + def sample_continuously(self): + """Configure the instrument to continuously read samples + and turn off any buffer or output triggering. + """ + self.disable_buffer() + self.trigger_immediately() diff --git a/tests/instruments/keithley/test_keithley2182.py b/tests/instruments/keithley/test_keithley2182.py new file mode 100644 index 0000000000..16d6ce72ee --- /dev/null +++ b/tests/instruments/keithley/test_keithley2182.py @@ -0,0 +1,104 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import pytest + +from pymeasure.test import expected_protocol +from pymeasure.instruments.keithley.keithley2182 import Keithley2182 + + +def test_voltage_read(): + with expected_protocol( + Keithley2182, + [(":READ?", "3.1415")], + ) as inst: + assert inst.voltage == pytest.approx(3.1415) + + +def test_voltage_range(): + with expected_protocol( + Keithley2182, + [(":SENS:VOLT:CHAN1:RANG?", "92"), + (":SENS:VOLT:CHAN1:RANG 2", None) + ], + ) as inst: + assert inst.ch_1.voltage_range == 92 + inst.ch_1.voltage_range = 2 + + +def test_voltage_range_trunc(): + with expected_protocol( + Keithley2182, + [(":SENS:VOLT:CHAN2:RANG 15", None), + (":SENS:VOLT:CHAN2:RANG?", "12"), + ], + ) as inst: + inst.ch_2.voltage_range = 15 # too large, gets truncated + assert inst.ch_2.voltage_range == 12 + + +def test_active_channel(): + with expected_protocol( + Keithley2182, + [(":SENS:CHAN?", "1"), + (":SENS:CHAN 2", None), + ], + ) as inst: + assert inst.active_channel == 1 + inst.active_channel = 2 + + +def test_thermocouple(): + with expected_protocol( + Keithley2182, + [(":SENS:TEMP:TC?", "S"), + (":SENS:TEMP:TC K", None), + ], + ) as inst: + assert inst.thermocouple == 'S' + inst.thermocouple = 'K' + + +def test_setup_voltage(): + with expected_protocol( + Keithley2182, + [(":SENS:CHAN 1;" + ":SENS:FUNC 'VOLT';" + ":SENS:VOLT:NPLC 5;", None), + (":SENS:VOLT:RANG:AUTO 1", None), + ("SYST:ERR?", '0,"No error"'), + ], + ) as inst: + inst.ch_1.setup_voltage() + + +def test_setup_temperature(): + with expected_protocol( + Keithley2182, + [(":SENS:CHAN 2;" + ":SENS:FUNC 'TEMP';" + ":SENS:TEMP:NPLC 5", None), + ("SYST:ERR?", '0,"No error"'), + ], + ) as inst: + inst.ch_2.setup_temperature() From 41ff77f7a201e198be1eda896cdec3a0a73a2d24 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 11 Mar 2024 19:13:02 +0100 Subject: [PATCH 047/181] Change indexing method from `[]` to `iat` --- pymeasure/display/widgets/table_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/display/widgets/table_widget.py b/pymeasure/display/widgets/table_widget.py index fa73340742..1e46263228 100644 --- a/pymeasure/display/widgets/table_widget.py +++ b/pymeasure/display/widgets/table_widget.py @@ -173,8 +173,8 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): if index.isValid() and role in (QtCore.Qt.ItemDataRole.DisplayRole, SORT_ROLE): try: results, row, col = self.translate_to_local(index.row(), index.column()) - value = results.data.iloc[row][col] - column_type = results.data.dtypes[col] + value = results.data.iat[row, col] + column_type = results.data.dtypes.iat[col] # Cast to column type value_render = column_type.type(value) except (IndexError, ValueError, TypeError): From f4c090f5487b9d3f09212f0b36e009a16de1300b Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 11 Mar 2024 19:13:27 +0100 Subject: [PATCH 048/181] Caught bug in gui_table.py example --- examples/Basic/gui_table.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/Basic/gui_table.py b/examples/Basic/gui_table.py index 89d24948c0..a238336341 100644 --- a/examples/Basic/gui_table.py +++ b/examples/Basic/gui_table.py @@ -94,8 +94,7 @@ def __init__(self): inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], widget_list=widget_list, - filename_input=False, - directory_input=False, + enable_file_input=False, ) logging.getLogger().addHandler(widget_list[1].handler) log.setLevel(self.log_level) From cab4b700d55c3e363ca2317a8ea811b70c885cbf Mon Sep 17 00:00:00 2001 From: Connor Carr Date: Tue, 12 Mar 2024 16:18:30 -0400 Subject: [PATCH 049/181] add tests and documentation --- docs/api/instruments/agilent/agilent4284A.rst | 7 + docs/api/instruments/agilent/index.rst | 1 + pymeasure/instruments/agilent/__init__.py | 1 + pymeasure/instruments/agilent/agilent4284A.py | 134 ++++++++++++------ .../instruments/agilent/test_agilent4284A.py | 110 ++++++++++++++ 5 files changed, 211 insertions(+), 42 deletions(-) create mode 100644 docs/api/instruments/agilent/agilent4284A.rst create mode 100644 tests/instruments/agilent/test_agilent4284A.py diff --git a/docs/api/instruments/agilent/agilent4284A.rst b/docs/api/instruments/agilent/agilent4284A.rst new file mode 100644 index 0000000000..d497dbf93c --- /dev/null +++ b/docs/api/instruments/agilent/agilent4284A.rst @@ -0,0 +1,7 @@ +############################## +Agilent 4284A LCR Meter +############################## + +.. autoclass:: pymeasure.instruments.agilent.Agilent4284A + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/instruments/agilent/index.rst b/docs/api/instruments/agilent/index.rst index 2be7342776..d58b92af5f 100644 --- a/docs/api/instruments/agilent/index.rst +++ b/docs/api/instruments/agilent/index.rst @@ -22,3 +22,4 @@ If the instrument you are looking for is not here, also check :doc:`HP<../hp/ind agilent33500 agilent33521A agilentB1500 + agilent4284A diff --git a/pymeasure/instruments/agilent/__init__.py b/pymeasure/instruments/agilent/__init__.py index 64570bf0bd..7aa511b7b6 100644 --- a/pymeasure/instruments/agilent/__init__.py +++ b/pymeasure/instruments/agilent/__init__.py @@ -33,3 +33,4 @@ from .agilent33500 import Agilent33500 from .agilent33521A import Agilent33521A from .agilentB1500 import AgilentB1500 +from .agilent4284A import Agilent4284A diff --git a/pymeasure/instruments/agilent/agilent4284A.py b/pymeasure/instruments/agilent/agilent4284A.py index ea1c085a25..2cb9782042 100644 --- a/pymeasure/instruments/agilent/agilent4284A.py +++ b/pymeasure/instruments/agilent/agilent4284A.py @@ -22,18 +22,22 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument -from pymeasure.instruments.agilent import AgilentE4980 -from pymeasure.instruments.validators import strict_discrete_set, truncated_range +import logging from time import sleep +from pymeasure.instruments import Instrument, SCPIMixin +from pymeasure.instruments.validators import strict_discrete_set, strict_range + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + IMPEDANCE_MODES = ( "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR" ) -class Agilent4284A(AgilentE4980): +class Agilent4284A(SCPIMixin, Instrument): """Represents the Agilent 4284A precision LCR meter. Most attributes are inherited from the Agilent E4980; a couple are overwritten @@ -43,26 +47,35 @@ class Agilent4284A(AgilentE4980): agilent = Agilent4284A("GPIB::1") + agilent.reset() # Return instrument settings to default + values + agilent.frequency = 10e3 # Set frequency to 10 kHz + agilent.voltage = 0.02 # Set AC voltage to 20 mV agilent.mode = 'ZTR' # Set impedance mode to measure - impedance magnitude [Ohm] and phase [rad] - agilent. + impedance magnitude [Ohm] and phase + [rad] + agilent.trigger_on_bus() + print(agilent.trigger()) # Trigger and print a measurement + agilent.enable_high_power() # Enable upper current, voltage, and + bias limits, if properly configured. + """ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): - super().__init__(adapter, name, includeSCPI=True, **kwargs) + super().__init__(adapter, name, **kwargs) frequency = Instrument.control( "FREQ?", "FREQ %g", - """Control AC frequency in Hertz.""", - validator=truncated_range, + """Control AC frequency in Hertz, from 20 Hz to 1 MHz.""", + validator=strict_range, values=(20, 1e6), ) ac_current = Instrument.control( "CURR:LEV?", "CURR:LEV %g", - """"Control AC current level in Amps. Range is 50 uA to 20 mA for default, 50 uA - to 200 mA in high-power mode.""", - validator=truncated_range, + """"Control AC current level in Amps. Valid range is 50 uA to 20 mA for default, + 50 uA to 200 mA in high-power mode.""", + validator=strict_range, values=(50e-6, 0.02), dynamic=True ) @@ -71,16 +84,15 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): "VOLT:LEV?", "VOLT:LEV %g", """"Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to 20 V in high-power mode.""", - validator=truncated_range, - values=(0.005, 0.02), + validator=strict_range, + values=(0.005, 2), dynamic=True ) - high_power_enabled = Instrument.control( - "OUTP:HPOW?", "OUTP:HPOW %d", - """Control whether the high-power mode is enabled. - Requires Option 001 (power amplifier / DC bias) is installed.""", - validator=strict_discrete_set, + high_power_enabled = Instrument.measurement( + "OUTP:HPOW?", + """Get whether the high-power mode is enabled. + High power requires Option 001 (power amplifier / DC bias) is installed.""", values={False: 0, True: 1}, map_values=True ) @@ -97,16 +109,19 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): "BIAS:VOLT?", "BIAS:VOLT %g", """Control the DC bias voltage in Volts. Maximum is 2 V by default, 40 V in high-power mode.""", - validator=truncated_range, + validator=strict_range, values=(0, 2), + dynamic=True ) bias_current = Instrument.control( "BIAS:CURR?", "BIAS:CURR %g", """Control the DC bias current in Amps. - Requires Option 001 (power amplifier / DC bias) is installed.""", - validator=truncated_range, - values=(0, 0.1) + Requires Option 001 (power amplifier / DC bias) is installed. + Maximum is 100 mA.""", + validator=strict_range, + values=(0, 0), + dynamic=True ) impedance_mode = Instrument.control( @@ -154,32 +169,53 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): values={False: 0, True: 1} ) - def freq_sweep(self, freq_list, return_freq=False): + def enable_high_power(self): + """Enable high power mode. + + Requires option 001 (power amplifier / DC bias) is installed. + """ + if self.options[0] == 0: + log.warning("Agilent 4284A power amplifier is not installed.") + return + self.ac_current_values = (50e-6, 0.2) + self.ac_voltage_values = (0.005, 20) + self.bias_voltage_values = (0, 40) + self.bias_current_values = (0, 0.1) + self.write("OUTP:HPOW 1") + + def disable_high_power(self): + """Disable high power mode.""" + self.ac_current_values = (50e-6, 0.02) + self.ac_voltage_values = (0.005, 2) + self.bias_voltage_values = (0, 2) + self.bias_current_values = (0, 0) + self.write("OUTP:HPOW 0") + + def frequency_sweep(self, freq_list, return_freq=True): """Run frequency list sweep using sequential trigger. :param freq_list: list of frequencies :param return_freq: if True, returns the frequencies read from the instrument - Returns values as configured with :attr:`~.Agilent4284A.mode` + Returns values as configured with :attr:`~.Agilent4284A.impedance_mode` and list + of frequencies in format ([val A], [val B], [frequency]) """ - # self.write("*RST;*CLS") - self.write("TRIG:SOUR BUS") - self.write("DISP:PAGE LIST") - self.write("FORM ASC") - # trigger in sequential mode - self.write("LIST:MODE SEQ") - lista_str = ",".join(['%e' % f for f in freq_list]) - self.write("LIST:FREQ %s" % lista_str) - # trigger + if min(freq_list) < 20 or max(freq_list) > 1e6: + log.warning("Agilent 4284A valid frequency range is 20 Hz to 1 MHz.") + return + freq_str = ",".join(['%g' % f for f in freq_list]) + self.reset() + self.clear() + self.write(f"TRIG:SOUR BUS;FORM ASC;LIST:MODE SEQ;LIST:FREQ {freq_str}") self.write("INIT:CONT ON") self.write("TRIG:IMM") - while True: - status = self.read("STAT:OPER?") - if (status & 8) == 8: # bit no. 3 is list sweep measurement complete bit - break - else: - sleep(1) - continue + # TODO: see if this while block is necessary + # while True: + # if (self.status & 8) == 8: # bit 3 is list sweep measurement complete bit + # break + # else: + # sleep(1) + # continue measured = self.values("FETCH?") # at the end return to manual trigger self.write(":TRIG:SOUR HOLD") @@ -188,6 +224,20 @@ def freq_sweep(self, freq_list, return_freq=False): b_data = [measured[_] for _ in range(1, 4 * len(freq_list), 4)] if return_freq: read_freqs = self.values("LIST:FREQ?") + self.check_errors() return a_data, b_data, read_freqs else: - return a_data, b_data + self.check_errors() + return a_data, b_data, freq_list + + def trigger(self): + """Execute a bus trigger, which can be used when :meth:`~.trigger_on_bus` + is configured. Returns result of triggered measurement. + """ + return self.write("*TRG") + + def trigger_on_bus(self): + """Configure the trigger to detect events based on the bus trigger, which can be + activated by :meth:`~.trigger`. + """ + self.write("TRIG:SOUR BUS") diff --git a/tests/instruments/agilent/test_agilent4284A.py b/tests/instruments/agilent/test_agilent4284A.py new file mode 100644 index 0000000000..1a0c3b1f10 --- /dev/null +++ b/tests/instruments/agilent/test_agilent4284A.py @@ -0,0 +1,110 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest +from pymeasure.test import expected_protocol +from pymeasure.instruments.agilent import Agilent4284A + + +@pytest.mark.parametrize("frequency", [20, 100, 1e4, 1e6]) +def test_frequency(frequency): + with expected_protocol( + Agilent4284A, + [("FREQ?", frequency), + (f"FREQ {frequency:g}", None),], + ) as inst: + assert frequency == inst.frequency + inst.frequency = frequency + + +def test_frequency_limit(): + with pytest.raises(ValueError): + with expected_protocol( + Agilent4284A, + [("FREQ 1", None)], + ) as inst: + inst.frequency = 1 + + +@pytest.mark.parametrize("power_mode", [0, 1]) +def test_high_power_mode(power_mode): + with expected_protocol( + Agilent4284A, + [("OUTP:HPOW?", power_mode),], + ) as inst: + assert power_mode == inst.high_power_enabled + + +@pytest.mark.parametrize("impedance_mode", [ + "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", + "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR"]) +def test_impedance_mode(impedance_mode): + with expected_protocol( + Agilent4284A, + [("FUNC:IMP?", impedance_mode), + (f"FUNC:IMP {impedance_mode}", None),], + ) as inst: + assert impedance_mode == inst.impedance_mode + inst.impedance_mode = impedance_mode + + +def test_enable_high_power(): + with expected_protocol( + Agilent4284A, + [("*OPT?", "1,0,0,0,0"), + ("OUTP:HPOW 1", None), + ("VOLT:LEV 5", None),], + ) as inst: + inst.enable_high_power() + inst.ac_voltage = 5 + + +def test_disable_high_power(): + with pytest.raises(ValueError): + with expected_protocol( + Agilent4284A, + [("OUTP:HPOW 0", None), + ("VOLT:LEV 5", None)], + ) as inst: + inst.disable_high_power() + inst.ac_voltage = 5 + + +def test_frequency_sweep(): + with expected_protocol( + Agilent4284A, + [("*RST", None), + ("*CLS", None), + ("TRIG:SOUR BUS;FORM ASC;LIST:MODE SEQ;LIST:FREQ 50,10000,500000", None), + ("INIT:CONT ON", None), + ("TRIG:IMM", None), + ("FETCH?", "0.5,-0.785,+0,+0,1.5,-0.785,+0,+0,2.5,-0.785,+0,+0"), + (":TRIG:SOUR HOLD", None), + ("LIST:FREQ?", "48,1.01e4,4.89e5"), + ("SYST:ERR?", '0,"No error"')], + ) as inst: + results = inst.frequency_sweep([50, 1e4, 5e5], return_freq=True) + assert results[0] == [0.5, 1.5, 2.5] + assert results[1] == [-0.785, -0.785, -0.785] + assert results[2] == [48, 1.01e4, 4.89e5] From 931cb1bcb972c44a790babcc1e1d46f846991420 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:00:40 +0100 Subject: [PATCH 050/181] Fix toptica ibeasmart referencing removed adapter function. --- pymeasure/instruments/toptica/ibeamsmart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymeasure/instruments/toptica/ibeamsmart.py b/pymeasure/instruments/toptica/ibeamsmart.py index c707b7b89b..2dcfdf65f3 100644 --- a/pymeasure/instruments/toptica/ibeamsmart.py +++ b/pymeasure/instruments/toptica/ibeamsmart.py @@ -146,7 +146,7 @@ def read(self): before the '[OK]' Value extraction: extract from 'name = [unit]'. - If can not be identified the orignal string is returned. + If can not be identified the original string is returned. :return: string containing the ASCII response of the instrument (without '[OK]'). """ @@ -164,7 +164,7 @@ def read(self): except VisaIOError: reply = '\n'.join(msg) try: - self.adapter.connection.flush_read_buffer() + self.adapter.flush_read_buffer() except AttributeError: log.warning("Adapter does not have 'flush_read_buffer' method.") raise ValueError(f"Flush buffer failed after '{reply}'") @@ -185,7 +185,7 @@ def check_set_errors(self): reply = self.read() if reply: # anything else than '[OK]'. - self.adapter.connection.flush_read_buffer() + self.adapter.flush_read_buffer() log.error(f"Setting a property failed with reply '{reply}'.") raise ValueError(f"Setting a property failed with reply '{reply}'.") return [] From 8ec0d460af767fa7337db43afa66bfcfb00ba95f Mon Sep 17 00:00:00 2001 From: jnnskls <99598309+jnnskls@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:43:08 +0100 Subject: [PATCH 051/181] Add Optical Spectral Analyzer "Yokogawa AQ6370D" (#1059) * add Yokogawa AQ6370D * add and fix methods * set up protcol tests * set up protcol tests * update tests * fix some docstrings * fix tests and add sample point control * remove test generator * fix documentation * added instrument to index * use new SCPIMixin class as suggested https://github.com/pymeasure/pymeasure/pull/1059#discussion_r1513254160 * reclassified to aq6370 series as suggested https://github.com/pymeasure/pymeasure/pull/1059#discussion_r1513267534 * added analysis readout https://github.com/pymeasure/pymeasure/pull/1059#discussion_r1513266473 * added mapping * added mapping TRA -> A * fixed toctree * fixed toctree (hopefully) * more concise mapping (https://github.com/pymeasure/pymeasure/pull/1059#discussion_r1519361124), deleted generator and fixed import in test * updated test * implemented aq6370series class and subclasses for respective instruments (aq6370c,aq6370d, aq6373...) * fixed index https://github.com/pymeasure/pymeasure/pull/1059#issuecomment-1992096784 * fix capitalization * fix captitalization of filenames * fix capitalization in toctree * fix title overline length --------- Co-authored-by: Bastian Leykauf --- AUTHORS.txt | 2 + .../api/instruments/yokogawa/aq6370series.rst | 31 ++ docs/api/instruments/yokogawa/index.rst | 6 +- .../instruments/yokogawa/aq6370series.py | 321 ++++++++++++ tests/instruments/yokogawa/test_aq6370d.py | 483 ++++++++++++++++++ 5 files changed, 841 insertions(+), 2 deletions(-) create mode 100644 docs/api/instruments/yokogawa/aq6370series.rst create mode 100644 pymeasure/instruments/yokogawa/aq6370series.py create mode 100644 tests/instruments/yokogawa/test_aq6370d.py diff --git a/AUTHORS.txt b/AUTHORS.txt index af14a2e339..fc3ec44127 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -75,3 +75,5 @@ Alec Vercruysse Matthew Zenaldin Canyon Clark Connor Carr +Jannis Kleine-Schönepauck + diff --git a/docs/api/instruments/yokogawa/aq6370series.rst b/docs/api/instruments/yokogawa/aq6370series.rst new file mode 100644 index 0000000000..1fcf6f297a --- /dev/null +++ b/docs/api/instruments/yokogawa/aq6370series.rst @@ -0,0 +1,31 @@ +#################################################### +Yokogawa AQ6370 Series of Optical Spectrum Analyzers +#################################################### + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6370Series + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6370D + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6370C + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6373 + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6373B + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6375 + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6375B + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/instruments/yokogawa/index.rst b/docs/api/instruments/yokogawa/index.rst index 56211432e4..e2b460b33f 100644 --- a/docs/api/instruments/yokogawa/index.rst +++ b/docs/api/instruments/yokogawa/index.rst @@ -7,7 +7,9 @@ Yokogawa This section contains specific documentation on the Yokogawa instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: - :maxdepth: 2 + :maxdepth: 3 yokogawa7651 - yokogawags200 \ No newline at end of file + yokogawags200 + aq6370series + diff --git a/pymeasure/instruments/yokogawa/aq6370series.py b/pymeasure/instruments/yokogawa/aq6370series.py new file mode 100644 index 0000000000..ea1505f18a --- /dev/null +++ b/pymeasure/instruments/yokogawa/aq6370series.py @@ -0,0 +1,321 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging + +from pymeasure.instruments import Instrument, SCPIMixin +from pymeasure.instruments.validators import strict_discrete_set, strict_range + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +class AQ6370Series(SCPIMixin, Instrument): + """Represents Yokogawa AQ6370 Series of optical spectrum analyzer.""" + + def __init__(self, adapter, name="Yokogawa AQ3670D OSA", **kwargs): + super().__init__(adapter, name, **kwargs) + + # Initiate and abort sweep --------------------------------------------------------------------- + + def abort(self): + """Stop operations such as measurements and calibration.""" + self.write(":ABORt") + + def initiate_sweep(self): + """Initiate a sweep.""" + self.write(":INITiate:IMMediate") + + # Leveling ------------------------------------------------------------------------------------- + + reference_level = Instrument.control( + ":DISPlay:TRACe:Y1:SCALe:RLEVel?", + ":DISPlay:TRACe:Y1:SCALe:RLEVel %g", + "Control the reference level of main scale of level axis (float in dBm).", + validator=strict_range, + values=[-90, 30], + ) + + level_position = Instrument.control( + ":DISPlay:TRACe:Y1:RPOSition?", + ":DISPlay:TRACe:Y1:RPOSition %g", + """Control the reference level position regarding divisions + (int, smaller than total number of divisions which is either 8, 10 or 12).""", + validator=strict_range, + values=[0, 12], + dynamic=True, + get_process=lambda x: int(x), + ) + + def set_level_position_to_max(self): + """Set the reference level position to the maximum value.""" + self.write(":CALCulate:MARKer:MAXimum:SRLevel") + + # Sweep settings ------------------------------------------------------------------------------- + + sweep_mode = Instrument.control( + ":INITiate:SMODe?", + ":INITiate:SMODe %s", + "Control the sweep mode (str 'SINGLE', 'REPEAT', 'AUTO', 'SEGMENT').", + validator=strict_discrete_set, + map_values=True, + values={"SINGLE": 1, "REPEAT": 2, "AUTO": 3, "SEGMENT": 4}, + ) + + sweep_time_interval = Instrument.control( + ":SENSe:SWEep:TIME:INTerval?", + ":SENSe:SWEep:TIME:INTerval %g", + "Control the sweep time interval (int from 0 to 99999 s).", + validator=strict_range, + values=[0, 99999], + ) + + automatic_sample_number = Instrument.control( + ":SENSe:SWEep:POINts:AUTO?", + ":SENSe:SWEep:POINts:AUTO %d", + "Control the automatic sample number (bool).", + validator=strict_discrete_set, + map_values=True, + values={False: 0, True: 1}, + ) + + sample_number = Instrument.control( + ":SENSe:SWEep:POINts?", + ":SENSe:SWEep:POINts %d", + "Control the sample number (int from 51 to 50001).", + validator=strict_range, + values=[101, 50001], + get_process=lambda x: int(x), + ) + + # Wavelength settings (all assuming wavelength mode, not frequency mode) ----------------------- + + wavelength_center = Instrument.control( + ":SENSe:WAVelength:CENTer?", + ":SENSe:WAVelength:CENTer %g", + "Control measurement condition center wavelength (float in m).", + validator=strict_range, + values=[600e-9, 1700e-9], + dynamic=True, + ) + + wavelength_span = Instrument.control( + ":SENSe:WAVelength:SPAN?", + ":SENSe:WAVelength:SPAN %g", + "Control wavelength span (float from 0 to 1100e-9 m).", + validator=strict_range, + values=[0, 1100e-9], + dynamic=True, + ) + + wavelength_start = Instrument.control( + ":SENSe:WAVelength:STARt?", + ":SENSe:WAVelength:STARt %g", + "Control the measurement start wavelength (float from 50e-9 to 2250e-9 in m).", + validator=strict_range, + values=[50e-9, 1700 - 9], + dynamic=True, + ) + + wavelength_stop = Instrument.control( + ":SENSe:WAVelength:STOP?", + ":SENSe:WAVelength:STOP %g", + "Control the measurement stop wavelength (float from 50e-9 to 2250e-9 in m).", + validator=strict_range, + values=[600e-9, 2250e-9], + dynamic=True, + ) + + # Trace operations ----------------------------------------------------------------------------- + + active_trace = Instrument.control( + ":TRACe:ACTive?", + ":TRACe:ACTive %d", + "Control the active trace (str 'A', 'B', 'C', ...).", + ) + + def copy_trace(self, source, destination): + """ + Copy the data of specified trace to the another trace. + + :param source: Source trace (str 'A', 'B', 'C', ...). + :param destination: Destination trace (str 'A', 'B', 'C', ...). + """ + + self.write(f":TRACe:COPY TR{source.replace('TR', '')},TR{destination.replace('TR', '')}") + + def delete_trace(self, trace): + """ + Delete the specified trace. + + :param trace: Trace to be deleted (str 'ALL', 'A', 'B', 'C', ...). + """ + + if trace == "ALL": + self.write(":TRACe:DELete:ALL") + else: + self.write(f":TRACe:DELete TR{trace.replace('TR', '')}") + + def get_xdata(self, trace="TRA"): + """ + Measure the x-axis data of specified trace, output wavelength in m. + + :param trace: Trace to measure (str 'A', 'B', 'C', ...). + :return: The x-axis data of specified trace. + """ + + return self.values(f":TRACe:X? TR{trace.replace('TR', '')}") + + def get_ydata(self, trace="TRA"): + """ + Measure the y-axis data of specified trace, output power in dBm. + + :param trace: Trace to measure (str 'A', 'B', 'C', ...). + :return: The y-axis data of specified trace. + """ + + return self.values(f":TRACe:Y? TR{trace.replace('TR', '')}") + + # Analysis ------------------------------------------------------------------------------------- + + def execute_analysis(self): + """Execute the analysis with the current analysis settings.""" + self.write(":CALCulate") + + def get_analysis(self): + """ + Query the analysis results of latest analysis. If no analysis has been + performed, returns query error. + """ + return self.write(":CALCulate:DATA?") + + # Resolution ----------------------------------------------------------------------------------- + + resolution_bandwidth = Instrument.control( + ":SENSe:BWIDth:RESolution?", + ":SENSe:BWIDth:RESolution %g", + """Control the measurement resolution + (float in m, discrete values: [0.02e-9, 0.05e-9, 0.1e-9, 0.2e-9, 0.5e-9, 1e-9, 2e-9] m).""", + validator=strict_discrete_set, + values=[0.02e-9, 0.05e-9, 0.1e-9, 0.2e-9, 0.5e-9, 1e-9, 2e-9], + dynamic=True, + ) + + +# subclasses of specific instruments --------------------------------------------------------------- + + +class AQ6370D(AQ6370Series): + """Represents Yokogawa AQ6370D optical spectrum analyzer.""" + + sweep_speed = Instrument.control( + ":SENSe:SWEep:SPEed?", + ":SENSe:SWEep:SPEed %d", + "Control the sweep speed (str '1x' or '2x' for double speed).", + validator=strict_discrete_set, + map_values=True, + values={"1x": 0, "2x": 1}, + ) + pass + + +class AQ6370C(AQ6370Series): + """Represents Yokogawa AQ6370C optical spectrum analyzer.""" + + sweep_speed = Instrument.control( + ":SENSe:SWEep:SPEed?", + ":SENSe:SWEep:SPEed %d", + "Control the sweep speed (str '1x' or '2x' for double speed).", + validator=strict_discrete_set, + map_values=True, + values={"1x": 0, "2x": 1}, + ) + pass + + +class AQ6373(AQ6370Series): + """Represents Yokogawa AQ6373 optical spectrum analyzer.""" + + wavelength_center_values = [350e-9, 1200e-9] # 0.001 steps + wavelength_start_values = [1e-9, 1200e-9] # 0.001 steps + wavelength_stop_values = [350e-9, 1625e-9] # 0.001 steps + wavelength_span_values = [0, 850e-9] # 0.1 steps + resolution_bandwidth_values = [ + 0.01e-9, + 0.02e-9, + 0.05e-9, + 0.1e-9, + 0.2e-9, + 0.5e-9, + 1e-9, + 2e-9, + 5e-9, + 10e-9, + ] + pass + + +class AQ6373B(AQ6373): + """Represents Yokogawa AQ6373B variant optical spectrum analyzer.""" + + sweep_speed = Instrument.control( + ":SENSe:SWEep:SPEed?", + ":SENSe:SWEep:SPEed %d", + "Control the sweep speed (str '1x' or '2x' for double speed).", + validator=strict_discrete_set, + map_values=True, + values={"1x": 0, "2x": 1}, + ) + pass + + +class AQ6375(AQ6370Series): + """Represents Yokogawa AQ6375 optical spectrum analyzer.""" + + wavelength_center_values = [1200e-9, 2400e-9] # 0.001 steps + wavelength_start_values = [600e-9, 2400e-9] # 0.001 steps + wavelength_stop_values = [1200e-9, 3000e-9] # 0.001 steps + wavelength_span_values = [0, 1200e-9] # 0.1 steps + resolution_bandwidth_values = [ + 0.05e-9, + 0.1e-9, + 0.2e-9, + 0.5e-9, + 1e-9, + 2e-9, + ] + pass + + +class AQ6375B(AQ6375): + """Represents Yokogawa AQ6375B variant optical spectrum analyzer.""" + + sweep_speed = Instrument.control( + ":SENSe:SWEep:SPEed?", + ":SENSe:SWEep:SPEed %d", + "Control the sweep speed (str '1x' or '2x' for double speed).", + validator=strict_discrete_set, + map_values=True, + values={"1x": 0, "2x": 1}, + ) + pass diff --git a/tests/instruments/yokogawa/test_aq6370d.py b/tests/instruments/yokogawa/test_aq6370d.py new file mode 100644 index 0000000000..571cac0975 --- /dev/null +++ b/tests/instruments/yokogawa/test_aq6370d.py @@ -0,0 +1,483 @@ +import pytest + +from pymeasure.instruments.yokogawa.aq6370series import AQ6370D +from pymeasure.test import expected_protocol + + +def test_init(): + with expected_protocol( + AQ6370D, + [], + ): + pass # Verify the expected communication. + + +def test_automatic_sample_number_setter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:SWEep:POINts:AUTO 0", None)], + ) as inst: + inst.automatic_sample_number = False + + +def test_level_position_getter(): + with expected_protocol( + AQ6370D, + [(b":DISPlay:TRACe:Y1:RPOSition?", b"8\n")], + ) as inst: + assert inst.level_position == 8 + + +def test_reference_level_setter(): + with expected_protocol( + AQ6370D, + [(b":DISPlay:TRACe:Y1:SCALe:RLEVel -10", None)], + ) as inst: + inst.reference_level = -10 + + +def test_reference_level_getter(): + with expected_protocol( + AQ6370D, + [(b":DISPlay:TRACe:Y1:SCALe:RLEVel?", b"-1.00000000E+001\n")], + ) as inst: + assert inst.reference_level == -10.0 + + +def test_resolution_bandwidth_setter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:BWIDth:RESolution 1e-09", None)], + ) as inst: + inst.resolution_bandwidth = 1e-09 + + +def test_sample_number_setter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:SWEep:POINts 101", None)], + ) as inst: + inst.sample_number = 101 + + +def test_sweep_mode_setter(): + with expected_protocol( + AQ6370D, + [(b":INITiate:SMODe 2", None)], + ) as inst: + inst.sweep_mode = "REPEAT" + + +def test_sweep_mode_getter(): + with expected_protocol( + AQ6370D, + [(b":INITiate:SMODe?", b"2\n")], + ) as inst: + assert inst.sweep_mode == "REPEAT" + + +@pytest.mark.parametrize( + "comm_pairs, value", + ( + ([(b":SENSe:SWEep:SPEed 0", None)], "1x"), + ([(b":SENSe:SWEep:SPEed 1", None)], "2x"), + ), +) +def test_sweep_speed_setter(comm_pairs, value): + with expected_protocol( + AQ6370D, + comm_pairs, + ) as inst: + inst.sweep_speed = value + + +@pytest.mark.parametrize( + "comm_pairs, value", + ( + ([(b":SENSe:SWEep:SPEed?", b"0\n")], "1x"), + ([(b":SENSe:SWEep:SPEed?", b"1\n")], "2x"), + ), +) +def test_sweep_speed_getter(comm_pairs, value): + with expected_protocol( + AQ6370D, + comm_pairs, + ) as inst: + assert inst.sweep_speed == value + + +def test_wavelength_center_getter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:WAVelength:CENTer?", b"+8.50000000E-007\n")], + ) as inst: + assert inst.wavelength_center == 8.5e-07 + + +def test_wavelength_span_setter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:WAVelength:SPAN 1e-08", None)], + ) as inst: + inst.wavelength_span = 1e-08 + + +def test_wavelength_span_getter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:WAVelength:SPAN?", b"+1.00000000E-007\n")], + ) as inst: + assert inst.wavelength_span == 1e-07 + + +def test_wavelength_start_setter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:WAVelength:STARt 8e-07", None)], + ) as inst: + inst.wavelength_start = 8e-07 + + +def test_wavelength_stop_setter(): + with expected_protocol( + AQ6370D, + [(b":SENSe:WAVelength:STOP 9e-07", None)], + ) as inst: + inst.wavelength_stop = 9e-07 + + +def test_delete_trace(): + with expected_protocol( + AQ6370D, + [(b":TRACe:DELete TRA", None)], + ) as inst: + assert ( + inst.delete_trace( + *("A",), + ) + is None + ) + + +def test_execute_analysis(): + with expected_protocol( + AQ6370D, + [(b":CALCulate", None)], + ) as inst: + assert inst.execute_analysis() is None + + +def test_get_analysis(): + with expected_protocol( + AQ6370D, + [(b":CALCulate:DATA?", None)], + ) as inst: + assert inst.get_analysis() is None + + +def test_get_xdata(): + with expected_protocol( + AQ6370D, + [ + ( + b":TRACe:X? TRA", + b"""+8.45000000E-007,+8.45100000E-007,+8.45200000E-007,+8.45300000E-007, + +8.45400000E-007,+8.45500000E-007,+8.45600000E-007,+8.45700000E-007, + +8.45800000E-007,+8.45900000E-007,+8.46000000E-007,+8.46100000E-007, + +8.46200000E-007,+8.46300000E-007,+8.46400000E-007,+8.46500000E-007, + +8.46600000E-007,+8.46700000E-007,+8.46800000E-007,+8.46900000E-007, + +8.47000000E-007,+8.47100000E-007,+8.47200000E-007,+8.47300000E-007, + +8.47400000E-007,+8.47500000E-007,+8.47600000E-007,+8.47700000E-007, + +8.47800000E-007,+8.47900000E-007,+8.48000000E-007,+8.48100000E-007, + +8.48200000E-007,+8.48300000E-007,+8.48400000E-007,+8.48500000E-007, + +8.48600000E-007,+8.48700000E-007,+8.48800000E-007,+8.48900000E-007, + +8.49000000E-007,+8.49100000E-007,+8.49200000E-007,+8.49300000E-007, + +8.49400000E-007,+8.49500000E-007,+8.49600000E-007,+8.49700000E-007, + +8.49800000E-007,+8.49900000E-007,+8.50000000E-007,+8.50100000E-007, + +8.50200000E-007,+8.50300000E-007,+8.50400000E-007,+8.50500000E-007, + +8.50600000E-007,+8.50700000E-007,+8.50800000E-007,+8.50900000E-007, + +8.51000000E-007,+8.51100000E-007,+8.51200000E-007,+8.51300000E-007, + +8.51400000E-007,+8.51500000E-007,+8.51600000E-007,+8.51700000E-007, + +8.51800000E-007,+8.51900000E-007,+8.52000000E-007,+8.52100000E-007, + +8.52200000E-007,+8.52300000E-007,+8.52400000E-007,+8.52500000E-007, + +8.52600000E-007,+8.52700000E-007,+8.52800000E-007,+8.52900000E-007, + +8.53000000E-007,+8.53100000E-007,+8.53200000E-007,+8.53300000E-007, + +8.53400000E-007,+8.53500000E-007,+8.53600000E-007,+8.53700000E-007, + +8.53800000E-007,+8.53900000E-007,+8.54000000E-007,+8.54100000E-007, + +8.54200000E-007,+8.54300000E-007,+8.54400000E-007,+8.54500000E-007, + +8.54600000E-007,+8.54700000E-007,+8.54800000E-007,+8.54900000E-007, + +8.55000000E-007\n""", + ) + ], + ) as inst: + assert inst.get_xdata( + *("TRA",), + ) == [ + 8.45e-07, + 8.451e-07, + 8.452e-07, + 8.453e-07, + 8.454e-07, + 8.455e-07, + 8.456e-07, + 8.457e-07, + 8.458e-07, + 8.459e-07, + 8.46e-07, + 8.461e-07, + 8.462e-07, + 8.463e-07, + 8.464e-07, + 8.465e-07, + 8.466e-07, + 8.467e-07, + 8.468e-07, + 8.469e-07, + 8.47e-07, + 8.471e-07, + 8.472e-07, + 8.473e-07, + 8.474e-07, + 8.475e-07, + 8.476e-07, + 8.477e-07, + 8.478e-07, + 8.479e-07, + 8.48e-07, + 8.481e-07, + 8.482e-07, + 8.483e-07, + 8.484e-07, + 8.485e-07, + 8.486e-07, + 8.487e-07, + 8.488e-07, + 8.489e-07, + 8.49e-07, + 8.491e-07, + 8.492e-07, + 8.493e-07, + 8.494e-07, + 8.495e-07, + 8.496e-07, + 8.497e-07, + 8.498e-07, + 8.499e-07, + 8.5e-07, + 8.501e-07, + 8.502e-07, + 8.503e-07, + 8.504e-07, + 8.505e-07, + 8.506e-07, + 8.507e-07, + 8.508e-07, + 8.509e-07, + 8.51e-07, + 8.511e-07, + 8.512e-07, + 8.513e-07, + 8.514e-07, + 8.515e-07, + 8.516e-07, + 8.517e-07, + 8.518e-07, + 8.519e-07, + 8.52e-07, + 8.521e-07, + 8.522e-07, + 8.523e-07, + 8.524e-07, + 8.525e-07, + 8.526e-07, + 8.527e-07, + 8.528e-07, + 8.529e-07, + 8.53e-07, + 8.531e-07, + 8.532e-07, + 8.533e-07, + 8.534e-07, + 8.535e-07, + 8.536e-07, + 8.537e-07, + 8.538e-07, + 8.539e-07, + 8.54e-07, + 8.541e-07, + 8.542e-07, + 8.543e-07, + 8.544e-07, + 8.545e-07, + 8.546e-07, + 8.547e-07, + 8.548e-07, + 8.549e-07, + 8.55e-07, + ] + + +def test_get_ydata(): + with expected_protocol( + AQ6370D, + [ + ( + b":TRACe:Y? TRA", + b"""-5.80586383E+001,-4.82452558E+001,-2.10000000E+002,-5.00581515E+001, + -2.10000000E+002,-2.10000000E+002,-2.10000000E+002,-5.39718179E+001, + -2.10000000E+002,-4.83172555E+001,-2.10000000E+002,-5.40078179E+001, + -9.28245811E+001,-4.71722994E+001,-2.10000000E+002,-5.20649217E+001, + -2.10000000E+002,-2.10000000E+002,-4.58900745E+001,-4.76780332E+001, + -4.88929655E+001,-4.59160745E+001,-5.89591856E+001,-6.04801307E+001, + -4.56972546E+001,-5.37341531E+001,-4.68775174E+001,-4.63697811E+001, + -2.10000000E+002,-4.58639117E+001,-2.10000000E+002,-5.04835809E+001, + -2.10000000E+002,-4.55457108E+001,-2.10000000E+002,-2.10000000E+002, + -5.25281289E+001,-5.25381299E+001,-6.83873225E+001,-4.97480469E+001, + -5.03931511E+001,-4.94802372E+001,-4.99253305E+001,-5.57736026E+001, + -4.43726648E+001,-2.10000000E+002,-2.10000000E+002,-2.10000000E+002, + -2.10000000E+002,-5.35840271E+001,-2.10000000E+002,-2.10000000E+002, + -4.61320419E+001,-2.10000000E+002,-4.82889238E+001,-4.89605452E+001, + -4.80090332E+001,-2.10000000E+002,-5.24489220E+001,-5.59126029E+001, + -4.93780799E+001,-2.10000000E+002,-5.22268893E+001,-4.99580469E+001, + -2.10000000E+002,-5.37240266E+001,-6.86322957E+001,-2.10000000E+002, + -2.10000000E+002,-2.10000000E+002,-4.70394921E+001,-4.96060890E+001, + -2.10000000E+002,-4.75372963E+001,-2.10000000E+002,-2.10000000E+002, + -2.10000000E+002,-2.10000000E+002,-5.42071536E+001,-4.70477584E+001, + -2.10000000E+002,-2.10000000E+002,-2.10000000E+002,-2.10000000E+002, + -5.67689941E+001,-4.81764150E+001,-2.10000000E+002,-4.87957619E+001, + -2.10000000E+002,-2.10000000E+002,-5.51568122E+001,-2.10000000E+002, + -5.09619656E+001,-2.10000000E+002,-5.43551546E+001,-4.71242549E+001, + -5.27949223E+001,-5.43831531E+001,-2.10000000E+002,-4.94815222E+001, + -6.89463024E+001\n""", + ) + ], + ) as inst: + assert inst.get_ydata( + *("TRA",), + ) == [ + -58.0586383, + -48.2452558, + -210.0, + -50.0581515, + -210.0, + -210.0, + -210.0, + -53.9718179, + -210.0, + -48.3172555, + -210.0, + -54.0078179, + -92.8245811, + -47.1722994, + -210.0, + -52.0649217, + -210.0, + -210.0, + -45.8900745, + -47.6780332, + -48.8929655, + -45.9160745, + -58.9591856, + -60.4801307, + -45.6972546, + -53.7341531, + -46.8775174, + -46.3697811, + -210.0, + -45.8639117, + -210.0, + -50.4835809, + -210.0, + -45.5457108, + -210.0, + -210.0, + -52.5281289, + -52.5381299, + -68.3873225, + -49.7480469, + -50.3931511, + -49.4802372, + -49.9253305, + -55.7736026, + -44.3726648, + -210.0, + -210.0, + -210.0, + -210.0, + -53.5840271, + -210.0, + -210.0, + -46.1320419, + -210.0, + -48.2889238, + -48.9605452, + -48.0090332, + -210.0, + -52.448922, + -55.9126029, + -49.3780799, + -210.0, + -52.2268893, + -49.9580469, + -210.0, + -53.7240266, + -68.6322957, + -210.0, + -210.0, + -210.0, + -47.0394921, + -49.606089, + -210.0, + -47.5372963, + -210.0, + -210.0, + -210.0, + -210.0, + -54.2071536, + -47.0477584, + -210.0, + -210.0, + -210.0, + -210.0, + -56.7689941, + -48.176415, + -210.0, + -48.7957619, + -210.0, + -210.0, + -55.1568122, + -210.0, + -50.9619656, + -210.0, + -54.3551546, + -47.1242549, + -52.7949223, + -54.3831531, + -210.0, + -49.4815222, + -68.9463024, + ] + + +def test_initiate_sweep(): + with expected_protocol( + AQ6370D, + [(b":INITiate:IMMediate", None)], + ) as inst: + assert inst.initiate_sweep() is None + + +def test_reset(): + with expected_protocol( + AQ6370D, + [(b"*RST", None)], + ) as inst: + assert inst.reset() is None + + +def test_set_level_position_to_max(): + with expected_protocol( + AQ6370D, + [(b":CALCulate:MARKer:MAXimum:SRLevel", None)], + ) as inst: + assert inst.set_level_position_to_max() is None From 8642e086082ed5ca4859e096bd6bd0d2183fa368 Mon Sep 17 00:00:00 2001 From: Douwe den Blanken Date: Tue, 19 Mar 2024 17:14:29 +0100 Subject: [PATCH 052/181] Fix small typo in Keithley DMM6500 docstring --- pymeasure/instruments/keithley/keithleyDMM6500.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymeasure/instruments/keithley/keithleyDMM6500.py b/pymeasure/instruments/keithley/keithleyDMM6500.py index b9795fef6e..e5252831ce 100644 --- a/pymeasure/instruments/keithley/keithleyDMM6500.py +++ b/pymeasure/instruments/keithley/keithleyDMM6500.py @@ -1229,7 +1229,7 @@ def scanned_data(self, start_idx=None, end_idx=None, raw=False): to accept new commands :param end_idx: An alternative way to set :attr:`scan_count` :param raw: An alternative way to set :attr:`scan_interval` in second - :return: A list of scan channels' measuredh + :return: A list of scan channels' measured :rtype: A list of channels' list """ self.write(":FORM:DATA ASCII") From 328eac567e7be959308e26411b2fbe8cf79aa158 Mon Sep 17 00:00:00 2001 From: Douwe den Blanken Date: Tue, 19 Mar 2024 17:19:07 +0100 Subject: [PATCH 053/181] Fix more typos --- .../instruments/keithley/keithleyDMM6500.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDMM6500.py b/pymeasure/instruments/keithley/keithleyDMM6500.py index e5252831ce..5bdca4c201 100644 --- a/pymeasure/instruments/keithley/keithleyDMM6500.py +++ b/pymeasure/instruments/keithley/keithleyDMM6500.py @@ -361,7 +361,7 @@ def close(self): ":DISP:{function}:DIG?", ":DISP:{function}:DIG %d", """ Control the displaying number of digits for currently active :attr:`mode`. - Available values are from 3 to 6 representing dispaly digits from 3.5 to 6.5.""", + Available values are from 3 to 6 representing display digits from 3.5 to 6.5.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, @@ -713,7 +713,7 @@ def measure_voltage(self, max_voltage=1, ac=False): resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", - """ Control the 2-wire reistance full-scale measure range in Ohms. + """ Control the 2-wire resistance full-scale measure range in Ohms. Available ranges are: 10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, and 100e6. Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, @@ -756,7 +756,7 @@ def measure_voltage(self, max_voltage=1, ac=False): resistance_4W_range = Instrument.control( ":SENS:FRES:RANG?", ":SENS:FRES:RANG:AUTO 0;:SENS:FRES:RANG %g", - """ Control the 4-wire reistance full-scale measure range in Ohms. + """ Control the 4-wire resistance full-scale measure range in Ohms. Available ranges are: 1, 10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, and 100e6. Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, @@ -815,7 +815,7 @@ def measure_resistance(self, max_resistance=10e6, wires=2): self.mode = "resistance 4W" self.resistance_4W_range = max_resistance else: - raise ValueError("Keithley DMM6500 only supports 2 or 4 wire" "resistance meaurements.") + raise ValueError("Keithley DMM6500 only supports 2 or 4 wire" "resistance measurements.") ################## # Frequency (Hz) # @@ -1092,14 +1092,14 @@ def measure_continuity(self): ########## # Buffer # ########## - # Main buffer fuctions are inherited from `KeithleyBuffer` class + # Main buffer functions are inherited from `KeithleyBuffer` class buffer_points = buffer_size = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ Control the number of buffer points. This does not represent actual points in the buffer, but the configuration - value instead. `0` means the largest buffer possibe based on the available + value instead. `0` means the largest buffer possible based on the available memory when the bufer is created.""", validator=truncated_range, values=[0, 6_000_000], @@ -1255,14 +1255,14 @@ def scan_modes(self): @scan_modes.setter def scan_modes(self, new_mode): - """Set all channles to the new mode. Ex: ``scan_modes = "voltage"``""" + """Set all channels to the new mode. Ex: ``scan_modes = "voltage"``""" self.write(f':SENS:FUNC "{self._mode_command(new_mode)}", (@1:10)') @property def scan_iscomplete(self): """Get Event Status Register (ESR) bit 0 to determine if previous works were completed. - This properity is used while running time-consuming scanning operation.""" + This property is used while running time-consuming scanning operation.""" res = int(self.ask("*ESR?")) & 1 if res == 1: return True @@ -1274,7 +1274,7 @@ def scan_start(self, block_communication=True, count=None, interval=None): and to do measurements. If :attr:`scan_count` is larger than 1, the next scanning will start again - after :attr:`scan_interval` seceond. Running large counts or long interval scanning + after :attr:`scan_interval` second. Running large counts or long interval scanning is a time-consuming operation. It's better to set ``block_communication=False`` and use :attr:`scan_iscomplete` to check if the measurement is completed. From 6a884e91be024c8ff7b1a94ae9594bf7b6d562eb Mon Sep 17 00:00:00 2001 From: Douwe den Blanken Date: Tue, 19 Mar 2024 17:22:46 +0100 Subject: [PATCH 054/181] Fix (I think) all typos in Keithley instrument files --- pymeasure/instruments/keithley/keithley2000.py | 4 ++-- pymeasure/instruments/keithley/keithley6517b.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/keithley/keithley2000.py b/pymeasure/instruments/keithley/keithley2000.py index f467f6e78e..81e1b64929 100644 --- a/pymeasure/instruments/keithley/keithley2000.py +++ b/pymeasure/instruments/keithley/keithley2000.py @@ -487,7 +487,7 @@ def measure_resistance(self, max_resistance=10e6, wires=2): self.resistance_4W_range = max_resistance else: raise ValueError("Keithley 2000 only supports 2 or 4 wire" - "resistance meaurements.") + "resistance measurements.") def measure_period(self): """ Configures the instrument to measure the period. """ @@ -573,7 +573,7 @@ def local(self): def remote(self): """ Places the instrument in the remote state, which is - does not need to be explicity called in general. """ + does not need to be explicitly called in general. """ self.write(":SYST:REM") def remote_lock(self): diff --git a/pymeasure/instruments/keithley/keithley6517b.py b/pymeasure/instruments/keithley/keithley6517b.py index 5beae98395..5b164de1cc 100644 --- a/pymeasure/instruments/keithley/keithley6517b.py +++ b/pymeasure/instruments/keithley/keithley6517b.py @@ -217,7 +217,7 @@ def measure_resistance(self, nplc=1, resistance=2.1e5, auto_range=True): :param resistance: Upper limit of resistance in Ohms, from -210 POhms to 210 POhms :param auto_range: Enables auto_range if True, else uses the - resistance_range attribut + resistance_range attribute """ log.info("%s is measuring resistance.", self.name) self.write(":SENS:FUNC 'RES';" @@ -234,7 +234,7 @@ def measure_voltage(self, nplc=1, voltage=21.0, auto_range=True): :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param voltage: Upper limit of voltage in Volts, from -1000 V to 1000 V :param auto_range: Enables auto_range if True, else uses the - voltage_range attribut + voltage_range attribute """ log.info("%s is measuring voltage.", self.name) self.write(":SENS:FUNC 'VOLT';" @@ -251,7 +251,7 @@ def measure_current(self, nplc=1, current=1.05e-4, auto_range=True): :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param current: Upper limit of current in Amps, from -21 mA to 21 mA :param auto_range: Enables auto_range if True, else uses the - current_range attribut + current_range attribute """ log.info("%s is measuring current.", self.name) self.write(":SENS:FUNC 'CURR';" From 96f58b1c182d6bb91faf73f80efa07756141ee3a Mon Sep 17 00:00:00 2001 From: Douwe den Blanken Date: Tue, 19 Mar 2024 21:55:34 +0100 Subject: [PATCH 055/181] Add reviewer comments for string usage Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> --- pymeasure/instruments/keithley/keithleyDMM6500.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymeasure/instruments/keithley/keithleyDMM6500.py b/pymeasure/instruments/keithley/keithleyDMM6500.py index 5bdca4c201..ba11852f56 100644 --- a/pymeasure/instruments/keithley/keithleyDMM6500.py +++ b/pymeasure/instruments/keithley/keithleyDMM6500.py @@ -815,7 +815,7 @@ def measure_resistance(self, max_resistance=10e6, wires=2): self.mode = "resistance 4W" self.resistance_4W_range = max_resistance else: - raise ValueError("Keithley DMM6500 only supports 2 or 4 wire" "resistance measurements.") + raise ValueError("Keithley DMM6500 only supports 2 or 4 wire resistance measurements.") ################## # Frequency (Hz) # From 1e1cfee23ddbdb06c8482f970a19d8b530046d30 Mon Sep 17 00:00:00 2001 From: Douwe den Blanken Date: Wed, 20 Mar 2024 10:39:45 +0100 Subject: [PATCH 056/181] Update AUTHORS.txt with 'Douwe den Blanken' --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index fc3ec44127..e53d53cbd6 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -76,4 +76,5 @@ Matthew Zenaldin Canyon Clark Connor Carr Jannis Kleine-Schönepauck +Douwe den Blanken From 2d1e964fa352a9513e1be3155ed98c1c4beca138 Mon Sep 17 00:00:00 2001 From: pyZerrenner <47388301+pyZerrenner@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:42:44 +0100 Subject: [PATCH 057/181] Implemented locale compatible `ScientificInput` - added `toDouble` and `toString` methods to `ScientificInput` for handling locale-sensitive conversion between numbers and strings through the validator's `locale()` - have the `valueFromText` and `textFromValue` use the above methods for conversion - move text processing in `valueFromText` outside the `try` statement --- pymeasure/display/inputs.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pymeasure/display/inputs.py b/pymeasure/display/inputs.py index bcc3b7823b..97dcb60a08 100644 --- a/pymeasure/display/inputs.py +++ b/pymeasure/display/inputs.py @@ -240,17 +240,27 @@ def validate(self, text, pos): def fixCase(self, text): self.lineEdit().setText(text.toLower()) + def toDouble(self, string): + value, success = self.validator.locale().toDouble(string) + if not success: + raise ValueError('String could not be converted to a double') + else: + return value + + def toString(self, value, format='g', precision=6): + return self.validator.locale().toString(value, format, precision) + def valueFromText(self, text): + text = str(text) + if self._parameter.units: + text = text[:-(len(self._parameter.units) + 1)] try: - if self._parameter.units: - return float(str(text)[:-(len(self._parameter.units) + 1)]) - else: - return float(str(text)) + return self.toDouble(text) except ValueError: return self._parameter.default def textFromValue(self, value): - string = f"{value:g}".replace("e+", "e") + string = self.toString(value).replace("e+", "e") string = re.sub(r"e(-?)0*(\d+)", r"e\1\2", string) return string From f203fd4988192b5b148edb1c5d15e9a629a1f372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Z=C3=BCrner?= <47388301+pyZerrenner@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:53:27 +0100 Subject: [PATCH 058/181] Update AUTHORS.txt Added new contributor --- AUTHORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index e53d53cbd6..f194175d30 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -77,4 +77,4 @@ Canyon Clark Connor Carr Jannis Kleine-Schönepauck Douwe den Blanken - +Till Zürner From bcb5281100ddaf54109596afb44e2ddcc6851088 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Sun, 24 Mar 2024 16:20:58 +0100 Subject: [PATCH 059/181] Add test for correct handling of decimal separators for different locales --- tests/display/test_inputs.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/display/test_inputs.py b/tests/display/test_inputs.py index 31992ef4ca..1e1827453d 100644 --- a/tests/display/test_inputs.py +++ b/tests/display/test_inputs.py @@ -25,6 +25,7 @@ import pytest from unittest import mock +from pymeasure.display.Qt import QtCore from pymeasure.display.inputs import ScientificInput, BooleanInput, ListInput from pymeasure.experiment.parameters import BooleanParameter, ListParameter, FloatParameter @@ -238,3 +239,27 @@ def test_setValue_should_update_param(self, qtbot): sci_input.setValue(5.0) sci_input.parameter # lazy update p.assert_called_once_with(5.0) + + @pytest.mark.parametrize("locale, decimalSep", [ + [QtCore.QLocale(31, 7, 224), "."], # UK locale for period + [QtCore.QLocale(30, 7, 151), ","], # NL locale for comma + ]) + def test_locale_settings(self, qtbot, locale, decimalSep): + assert locale.decimalPoint() == decimalSep + QtCore.QLocale.setDefault(locale) + + float_param = FloatParameter('potato', + minimum=-1000, maximum=1000, default=1.3) + sci_input = ScientificInput(float_param) + + # Check if the modified locale is set + assert sci_input.locale().decimalPoint() == decimalSep + assert sci_input.validator.locale().decimalPoint() == decimalSep + + # Check if conversion from double to text works correctly + assert sci_input.valueFromText(f"2{decimalSep}6") == 2.6 + # Check if conversion from text to double works correctly + assert sci_input.textFromValue(2.6) == f"2{decimalSep}6" + + # Reset the locale settings + QtCore.QLocale.setDefault(QtCore.QLocale.system()) From 53be70f7391a986079651c979a9819034123da8a Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Sun, 24 Mar 2024 23:35:11 +0100 Subject: [PATCH 060/181] remove return statement from try-except --- pymeasure/display/inputs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pymeasure/display/inputs.py b/pymeasure/display/inputs.py index 97dcb60a08..54403eb9ba 100644 --- a/pymeasure/display/inputs.py +++ b/pymeasure/display/inputs.py @@ -255,9 +255,10 @@ def valueFromText(self, text): if self._parameter.units: text = text[:-(len(self._parameter.units) + 1)] try: - return self.toDouble(text) + val = self.toDouble(text) except ValueError: - return self._parameter.default + val = self._parameter.default + return val def textFromValue(self, value): string = self.toString(value).replace("e+", "e") From 2f8cb759bc8ca57a5ab9ebe9f87bb0aac68c4bd6 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:28:23 +0100 Subject: [PATCH 061/181] Adjust SCPI status according to review --- pymeasure/instruments/keithley/keithley2700.py | 4 ++-- pymeasure/instruments/keithley/keithley2750.py | 4 ++-- pymeasure/instruments/lakeshore/lakeshore421.py | 5 +++-- pymeasure/instruments/lakeshore/lakeshore425.py | 7 ++++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pymeasure/instruments/keithley/keithley2700.py b/pymeasure/instruments/keithley/keithley2700.py index 02f5803cbf..97d2772fd4 100644 --- a/pymeasure/instruments/keithley/keithley2700.py +++ b/pymeasure/instruments/keithley/keithley2700.py @@ -25,7 +25,7 @@ import logging from warnings import warn -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin from .buffer import KeithleyBuffer @@ -87,7 +87,7 @@ def text_length_validator(value, values): return value[:values] -class Keithley2700(KeithleyBuffer, SCPIUnknownMixin, Instrument): +class Keithley2700(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 2700 Multimeter/Switch System and provides a high-level interface for interacting with the instrument. diff --git a/pymeasure/instruments/keithley/keithley2750.py b/pymeasure/instruments/keithley/keithley2750.py index 27e15eefb9..ca7ea8dc72 100644 --- a/pymeasure/instruments/keithley/keithley2750.py +++ b/pymeasure/instruments/keithley/keithley2750.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument, SCPIMixin def clean_closed_channels(output): @@ -50,7 +50,7 @@ def clean_closed_channels(output): raise ValueError("`output` must be a string or list.") -class Keithley2750(SCPIUnknownMixin, Instrument): +class Keithley2750(SCPIMixin, Instrument): """ Represents the Keithley2750 multimeter/switch system and provides a high-level interface for interacting with the instrument. """ diff --git a/pymeasure/instruments/lakeshore/lakeshore421.py b/pymeasure/instruments/lakeshore/lakeshore421.py index ddd4a9075e..5a0d1d453d 100644 --- a/pymeasure/instruments/lakeshore/lakeshore421.py +++ b/pymeasure/instruments/lakeshore/lakeshore421.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set @@ -30,7 +30,7 @@ from time import time, sleep -class LakeShore421(SCPIUnknownMixin, Instrument): +class LakeShore421(Instrument): """ Represents the Lake Shore 421 Gaussmeter and provides a high-level interface for interacting with the instrument. @@ -64,6 +64,7 @@ def __init__(self, adapter, name="Lake Shore 421 Gaussmeter", baud_rate=9600, ** asrl={'baud_rate': baud_rate, 'data_bits': 7, 'stop_bits': 10, 'parity': 1}, read_termination='\r', write_termination='\n', + includeSCPI=False, **kwargs ) self.last_write_time = time() diff --git a/pymeasure/instruments/lakeshore/lakeshore425.py b/pymeasure/instruments/lakeshore/lakeshore425.py index 3138084ecd..a84da28650 100644 --- a/pymeasure/instruments/lakeshore/lakeshore425.py +++ b/pymeasure/instruments/lakeshore/lakeshore425.py @@ -22,14 +22,14 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, truncated_discrete_set from time import sleep import numpy as np -class LakeShore425(SCPIUnknownMixin, Instrument): +class LakeShore425(Instrument): """ Represents the LakeShore 425 Gaussmeter and provides a high-level interface for interacting with the instrument @@ -84,6 +84,7 @@ def __init__(self, adapter, name="LakeShore 425 Gaussmeter", **kwargs): 'parity': 1, # odd 'data_bits': 7 }, + includeSCPI=False, **kwargs ) @@ -96,7 +97,7 @@ def dc_mode(self, wideband=True): if wideband: self.mode = (1, 0, 1) else: - self.mode(1, 0, 2) + self.mode = (1, 0, 2) def ac_mode(self, wideband=True): """ Sets up a measurement of an oscillating (AC) field """ From b9c8a0770ef7cf61b1d0a3388c51cd3ba7559bd3 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:28:51 +0100 Subject: [PATCH 062/181] Add implemented SCPI methods to sr830 and remove SCPI status. --- pymeasure/instruments/srs/sr830.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pymeasure/instruments/srs/sr830.py b/pymeasure/instruments/srs/sr830.py index 46de1bdb79..7ae7144e67 100644 --- a/pymeasure/instruments/srs/sr830.py +++ b/pymeasure/instruments/srs/sr830.py @@ -26,7 +26,7 @@ import time import numpy as np from enum import IntFlag -from pymeasure.instruments import Instrument, SCPIUnknownMixin +from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range, discreteTruncate @@ -57,7 +57,7 @@ class ERRStatus(IntFlag): MATH_ERR = 128 -class SR830(SCPIUnknownMixin, Instrument): +class SR830(Instrument): SAMPLE_FREQUENCIES = [ 62.5e-3, 125e-3, 250e-3, 500e-3, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 @@ -88,6 +88,27 @@ class SR830(SCPIUnknownMixin, Instrument): REFERENCE_SOURCE_TRIGGER = ['SINE', 'POS EDGE', 'NEG EDGE'] INPUT_FILTER = ['Off', 'On'] + status = Instrument.measurement( + "*STB?", + """Get the status byte and Master Summary Status bit.""", + cast=str, + ) + + id = Instrument.measurement( + "*IDN?", + """Get the identification of the instrument.""", + cast=str, + maxsplit=0, + ) + + def clear(self): + """Clear the instrument status byte.""" + self.write("*CLS") + + def reset(self): + """Reset the instrument.""" + self.write("*RST") + sine_voltage = Instrument.control( "SLVL?", "SLVL%0.3f", """ A floating point property that represents the reference sine-wave @@ -370,6 +391,7 @@ def __init__(self, adapter, name="Stanford Research Systems SR830 Lock-in amplif super().__init__( adapter, name, + includeSCPI=False, **kwargs ) @@ -390,7 +412,7 @@ def auto_offset(self, channel): self.write("AOFF %d" % channel) def get_scaling(self, channel): - """ Returns the offset precent and the exapnsion term + """ Returns the offset percent and the expansion term that are used to scale the channel in question """ if channel not in self.CHANNELS: @@ -401,7 +423,7 @@ def get_scaling(self, channel): def set_scaling(self, channel, precent, expand=0): """ Sets the offset of a channel (X=1, Y=2, R=3) to a - certain precent (-105% to 105%) of the signal, with + certain percent (-105% to 105%) of the signal, with an optional expansion term (0, 10=1, 100=2) """ if channel not in self.CHANNELS: From 87960583efb40a2af177acd80ef22866a53f8323 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:29:02 +0100 Subject: [PATCH 063/181] Fix typo. --- pymeasure/instruments/generic_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymeasure/instruments/generic_types.py b/pymeasure/instruments/generic_types.py index 117af7deee..d150c63c76 100644 --- a/pymeasure/instruments/generic_types.py +++ b/pymeasure/instruments/generic_types.py @@ -102,11 +102,11 @@ def check_errors(self): class SCPIUnknownMixin(SCPIMixin): - """Mixin which adds SCPI commands to an instrument from which it is not known, whether it + """Mixin which adds SCPI commands to an instrument from which it is not known whether it supports SCPI commands or not. """ def __init__(self, *args, **kwargs): - warn("It is not known, whether this device support SCPI commands or not. Please inform " - "the pymeasure maintainers, if you know the answer.", FutureWarning) + warn("It is not known whether this device support SCPI commands or not. Please inform " + "the pymeasure maintainers if you know the answer.", FutureWarning) super().__init__(*args, **kwargs) From 99075b5bc25f4f218bfa837922a71b9b5ac208be Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:38:40 +0100 Subject: [PATCH 064/181] Make None the default wait time. --- pymeasure/instruments/channel.py | 4 ++-- pymeasure/instruments/common_base.py | 6 +++--- pymeasure/instruments/instrument.py | 4 ++-- tests/instruments/test_instrument.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pymeasure/instruments/channel.py b/pymeasure/instruments/channel.py index dc58cb4a9b..0c074cc849 100644 --- a/pymeasure/instruments/channel.py +++ b/pymeasure/instruments/channel.py @@ -132,9 +132,9 @@ def check_set_errors(self): return self.parent.check_set_errors() # Communication functions - def wait_for(self, query_delay=0): + def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. - :param query_delay: Delay between writing and reading in seconds. + :param query_delay: Delay between writing and reading in seconds. None is default delay. """ self.parent.wait_for(query_delay) diff --git a/pymeasure/instruments/common_base.py b/pymeasure/instruments/common_base.py index ba48573629..04f489e294 100644 --- a/pymeasure/instruments/common_base.py +++ b/pymeasure/instruments/common_base.py @@ -366,16 +366,16 @@ def remove_child(self, child): delattr(self, child._name) # Communication functions - def wait_for(self, query_delay=0): + def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. Implement in subclass! - :param query_delay: Delay between writing and reading in seconds. + :param query_delay: Delay between writing and reading in seconds. None is default delay. """ raise NotImplementedError("Implement in subclass!") - def ask(self, command, query_delay=0): + def ask(self, command, query_delay=None): """Write a command to the instrument and return the read response. :param command: Command string to be sent to the instrument. diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py index 50284e4072..37fcf78f2f 100644 --- a/pymeasure/instruments/instrument.py +++ b/pymeasure/instruments/instrument.py @@ -196,10 +196,10 @@ def read_binary_values(self, **kwargs): return self.adapter.read_binary_values(**kwargs) # Communication functions - def wait_for(self, query_delay=0): + def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. - :param query_delay: Delay between writing and reading in seconds. + :param query_delay: Delay between writing and reading in seconds. None is default delay. """ if query_delay: time.sleep(query_delay) diff --git a/tests/instruments/test_instrument.py b/tests/instruments/test_instrument.py index 5663e7726f..d26b79ac05 100644 --- a/tests/instruments/test_instrument.py +++ b/tests/instruments/test_instrument.py @@ -195,7 +195,7 @@ class TestWaiting: @pytest.fixture() def instr(self): class Faked(Instrument): - def wait_for(self, query_delay=0): + def wait_for(self, query_delay=None): self.waited = query_delay return Faked(ProtocolAdapter(), name="faked") @@ -208,7 +208,7 @@ def test_waiting(self): def test_ask_calls_wait(self, instr): instr.adapter.comm_pairs = [("abc", "resp")] instr.ask("abc") - assert instr.waited == 0 + assert instr.waited is None def test_ask_calls_wait_with_delay(self, instr): instr.adapter.comm_pairs = [("abc", "resp")] From 58af0c213e00dec7653142a625775942229c9244 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:39:35 +0100 Subject: [PATCH 065/181] Fix Temptronic. --- pymeasure/instruments/attocube/anc300.py | 14 ++++---- .../instruments/temptronic/temptronic_base.py | 11 +++--- .../temptronic/test_temptronic_base.py | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 tests/instruments/temptronic/test_temptronic_base.py diff --git a/pymeasure/instruments/attocube/anc300.py b/pymeasure/instruments/attocube/anc300.py index eecad75f80..2bdbd210eb 100644 --- a/pymeasure/instruments/attocube/anc300.py +++ b/pymeasure/instruments/attocube/anc300.py @@ -138,7 +138,7 @@ class Axis(Channel): stepu = Instrument.setting( "stepu %d", """Set the steps upwards for N steps. Mode must be 'stp' and N must be - positive. 0 causes a continous movement until stop is called. + positive. 0 causes a continuous movement until stop is called. .. deprecated:: 0.13.0 Use meth:`move_raw` instead. """, @@ -150,7 +150,7 @@ class Axis(Channel): stepd = Instrument.setting( "stepd %d", """Set the steps downwards for N steps. Mode must be 'stp' and N must be - positive. 0 causes a continous movement until stop is called. + positive. 0 causes a continuous movement until stop is called. .. deprecated:: 0.13.0 Use meth:`move_raw` instead. """, @@ -217,7 +217,7 @@ def move(self, steps, gnd=True): # perform the movement self.move_raw(steps) # wait for the move to finish - self.parent.wait_for(abs(steps) / self.frequency) + self.wait_for(abs(steps) / self.frequency) # ask if movement finished self.ask('stepw') if gnd: @@ -231,7 +231,7 @@ def measure_capacity(self): """ self.mode = 'cap' # wait for the measurement to finish - self.parent.wait_for(1) + self.wait_for(1) # ask if really finished self.ask('capw') return self.capacity @@ -408,9 +408,9 @@ def read(self): f"command with message {msg}") return self._extract_value(msg) - def wait_for(self, query_delay=0): + def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. - :param query_delay: Delay between writing and reading in seconds. + :param query_delay: Delay between writing and reading in seconds. None is default delay. """ - super().wait_for(query_delay or self.query_delay) + super().wait_for(self.query_delay if query_delay is None else query_delay) diff --git a/pymeasure/instruments/temptronic/temptronic_base.py b/pymeasure/instruments/temptronic/temptronic_base.py index 87aeeb9360..f7ab392ba3 100644 --- a/pymeasure/instruments/temptronic/temptronic_base.py +++ b/pymeasure/instruments/temptronic/temptronic_base.py @@ -125,6 +125,12 @@ class ATSBase(Instrument): """The base class for Temptronic ATSXXX instruments. """ + def __init__(self, adapter, name="ATSBase", **kwargs): + super().__init__(adapter, name=name, **kwargs) + + def wait_for(self, query_delay=None): + super().wait_for(0.05 if query_delay is None else query_delay) + remote_mode = Instrument.setting( "%s", """``True`` disables TS GUI but displays a “Return to local" switch.""", @@ -375,7 +381,7 @@ class ATSBase(Instrument): 0 None ====== ====== - Refere to chapter 4 in the manual + Refer to chapter 4 in the manual """, ) @@ -549,9 +555,6 @@ class ATSBase(Instrument): dynamic=True ) - def __init__(self, adapter, name="ATSBase", **kwargs): - super().__init__(adapter, name=name, query_delay=0.05, **kwargs) - def reset(self): """Reset (force) the System to the Operator screen. diff --git a/tests/instruments/temptronic/test_temptronic_base.py b/tests/instruments/temptronic/test_temptronic_base.py new file mode 100644 index 0000000000..26f581cd7a --- /dev/null +++ b/tests/instruments/temptronic/test_temptronic_base.py @@ -0,0 +1,36 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from time import perf_counter + +from pymeasure.test import expected_protocol +from pymeasure.instruments.temptronic.temptronic_base import ATSBase + + +def test_check_query_delay(): + with expected_protocol(ATSBase, [("TTIM?", "7")]) as inst: + start = perf_counter() + assert inst.maximum_test_time == 7 + delay = perf_counter() - start + assert delay > 0.05 From aa6aa791ee6462489615e20d26f51d516e9ed13f Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:52:13 +0100 Subject: [PATCH 066/181] Adjust more places of query_delay. --- pymeasure/instruments/aja/dcxs.py | 4 ++-- pymeasure/instruments/ametek/ametek7270.py | 2 +- pymeasure/instruments/common_base.py | 2 +- pymeasure/instruments/thyracont/smartline_v2.py | 6 +++--- tests/instruments/test_instrument.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pymeasure/instruments/aja/dcxs.py b/pymeasure/instruments/aja/dcxs.py index 5a3ea6c396..bdd5443bbd 100644 --- a/pymeasure/instruments/aja/dcxs.py +++ b/pymeasure/instruments/aja/dcxs.py @@ -31,7 +31,7 @@ class DCXS(Instrument): Connection to the device is made through an RS232 serial connection. The communication settings are fixed in the device at 38400, one stopbit, no parity. - The communication protocol of the device uses single character commands and fixed length replys, + The device's communication protocol uses single character commands and fixed length replies, both without any terminator. :param adapter: pyvisa resource name of the instrument or adapter instance @@ -53,7 +53,7 @@ def __init__(self, adapter, name="AJA DCXS sputtering power supply", **kwargs): # characters. self.adapter.flush_read_buffer() - def ask(self, command, query_delay=0, **kwargs): + def ask(self, command, query_delay=None, **kwargs): """Write a command to the instrument and return the read response. :param command: Command string to be sent to the instrument. diff --git a/pymeasure/instruments/ametek/ametek7270.py b/pymeasure/instruments/ametek/ametek7270.py index e2b4bc8684..fae795641a 100644 --- a/pymeasure/instruments/ametek/ametek7270.py +++ b/pymeasure/instruments/ametek/ametek7270.py @@ -248,7 +248,7 @@ def check_set_errors(self): else: return ['Incorrect return from previously set property'] - def ask(self, command, query_delay=0): + def ask(self, command, query_delay=None): """Send a command and read the response, stripping white spaces. Usually the properties use the diff --git a/pymeasure/instruments/common_base.py b/pymeasure/instruments/common_base.py index 04f489e294..a08c50aa0f 100644 --- a/pymeasure/instruments/common_base.py +++ b/pymeasure/instruments/common_base.py @@ -421,7 +421,7 @@ def values(self, command, separator=',', cast=float, preprocess_reply=None, maxs pass # Keep as string return results - def binary_values(self, command, query_delay=0, **kwargs): + def binary_values(self, command, query_delay=None, **kwargs): """ Write a command to the instrument and return a numpy array of the binary data. :param command: Command to be sent to the instrument. diff --git a/pymeasure/instruments/thyracont/smartline_v2.py b/pymeasure/instruments/thyracont/smartline_v2.py index a450541b43..2d39fb5956 100644 --- a/pymeasure/instruments/thyracont/smartline_v2.py +++ b/pymeasure/instruments/thyracont/smartline_v2.py @@ -266,7 +266,7 @@ def write_composition(self, accessCode, command, data=""): """ self.write(f"{accessCode}{command}{compose_data(data)}") - def ask(self, command_message, query_delay=0): + def ask(self, command_message, query_delay=None): """Ask for some value and check that the response matches the original command. :param str command_message: Access code, command, length, and content. @@ -276,14 +276,14 @@ def ask(self, command_message, query_delay=0): self.wait_for(query_delay) return self.read(command_message[1:3]) - def ask_manually(self, accessCode, command, data="", query_delay=0): + def ask_manually(self, accessCode, command, data="", query_delay=None): """ Send a message to the transmitter and return its answer. :param accessCode: How to access the device. :param command: Command to send to the device. :param data: Data for the command. - :param int query_delay: Time to wait between writing and reading. + :param float query_delay: Time to wait between writing and reading in seconds. :return str: Response from the device after error checking. """ self.write(f"{accessCode}{command}{compose_data(data)}") diff --git a/tests/instruments/test_instrument.py b/tests/instruments/test_instrument.py index d26b79ac05..ea796cc671 100644 --- a/tests/instruments/test_instrument.py +++ b/tests/instruments/test_instrument.py @@ -218,7 +218,7 @@ def test_ask_calls_wait_with_delay(self, instr): def test_binary_values_calls_wait(self, instr): instr.adapter.comm_pairs = [("abc", "abcdefgh")] instr.binary_values("abc") - assert instr.waited == 0 + assert instr.waited is None @pytest.mark.parametrize("method, write, reply", (("id", "*IDN?", "xyz"), From be3356744014dc8c28b66cb56341774b8ba910e5 Mon Sep 17 00:00:00 2001 From: Connor Carr Date: Thu, 28 Mar 2024 14:58:22 -0400 Subject: [PATCH 067/181] driver tested with instrument --- pymeasure/instruments/agilent/agilent4284A.py | 531 ++++++++++-------- .../instruments/agilent/test_agilent4284A.py | 37 +- 2 files changed, 312 insertions(+), 256 deletions(-) diff --git a/pymeasure/instruments/agilent/agilent4284A.py b/pymeasure/instruments/agilent/agilent4284A.py index 2cb9782042..f5d6146902 100644 --- a/pymeasure/instruments/agilent/agilent4284A.py +++ b/pymeasure/instruments/agilent/agilent4284A.py @@ -1,243 +1,288 @@ -# -# This file is part of the PyMeasure package. -# -# Copyright (c) 2013-2024 PyMeasure Developers -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# - -import logging -from time import sleep - -from pymeasure.instruments import Instrument, SCPIMixin -from pymeasure.instruments.validators import strict_discrete_set, strict_range - -log = logging.getLogger(__name__) -log.addHandler(logging.NullHandler()) - -IMPEDANCE_MODES = ( - "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", - "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR" -) - - -class Agilent4284A(SCPIMixin, Instrument): - """Represents the Agilent 4284A precision LCR meter. - - Most attributes are inherited from the Agilent E4980; a couple are overwritten - to accomodate slight differences between the instruments. - - .. code-block:: python - - agilent = Agilent4284A("GPIB::1") - - agilent.reset() # Return instrument settings to default - values - agilent.frequency = 10e3 # Set frequency to 10 kHz - agilent.voltage = 0.02 # Set AC voltage to 20 mV - agilent.mode = 'ZTR' # Set impedance mode to measure - impedance magnitude [Ohm] and phase - [rad] - agilent.trigger_on_bus() - print(agilent.trigger()) # Trigger and print a measurement - agilent.enable_high_power() # Enable upper current, voltage, and - bias limits, if properly configured. - - """ - - def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): - super().__init__(adapter, name, **kwargs) - - frequency = Instrument.control( - "FREQ?", "FREQ %g", - """Control AC frequency in Hertz, from 20 Hz to 1 MHz.""", - validator=strict_range, - values=(20, 1e6), - ) - - ac_current = Instrument.control( - "CURR:LEV?", "CURR:LEV %g", - """"Control AC current level in Amps. Valid range is 50 uA to 20 mA for default, - 50 uA to 200 mA in high-power mode.""", - validator=strict_range, - values=(50e-6, 0.02), - dynamic=True - ) - - ac_voltage = Instrument.control( - "VOLT:LEV?", "VOLT:LEV %g", - """"Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to - 20 V in high-power mode.""", - validator=strict_range, - values=(0.005, 2), - dynamic=True - ) - - high_power_enabled = Instrument.measurement( - "OUTP:HPOW?", - """Get whether the high-power mode is enabled. - High power requires Option 001 (power amplifier / DC bias) is installed.""", - values={False: 0, True: 1}, - map_values=True - ) - - bias_enabled = Instrument.control( - "BIAS:STAT?", "BIAS:STAT %d", - """Control whether DC bias is enabled.""", - validator=strict_discrete_set, - values={False: 0, True: 1}, - map_values=True - ) - - bias_voltage = Instrument.control( - "BIAS:VOLT?", "BIAS:VOLT %g", - """Control the DC bias voltage in Volts. - Maximum is 2 V by default, 40 V in high-power mode.""", - validator=strict_range, - values=(0, 2), - dynamic=True - ) - - bias_current = Instrument.control( - "BIAS:CURR?", "BIAS:CURR %g", - """Control the DC bias current in Amps. - Requires Option 001 (power amplifier / DC bias) is installed. - Maximum is 100 mA.""", - validator=strict_range, - values=(0, 0), - dynamic=True - ) - - impedance_mode = Instrument.control( - "FUNC:IMP?", "FUNC:IMP %s", - """Control impedance measurement function. - - * CPD: Parallel capacitance [F] and dissipation factor [number] - * CPQ: Parallel capacitance [F] and quality factor [number] - * CPG: Parallel capacitance [F] and parallel conductance [S] - * CPRP: Parallel capacitance [F] and parallel resistance [Ohm] - - * CSD: Series capacitance [F] and dissipation factor [number] - * CSQ: Series capacitance [F] and quality factor [number] - * CSRS: Series capacitance [F] and series resistance [Ohm] - - * LPQ: Parallel inductance [H] and quality factor [number] - * LPD: Parallel inductance [H] and dissipation factor [number] - * LPG: Parallel inductance [H] and parallel conductance [S] - * LPRP: Parallel inductance [H] and parallel resistance [Ohm] - - * LSD: Series inductance [H] and dissipation factor [number] - * LSQ: Seriesinductance [H] and quality factor [number] - * LSRS: Series inductance [H] and series resistance [Ohm] - - * RX: Resistance [Ohm] and reactance [Ohm] - * ZTD: Impedance, magnitude [Ohm] and phase [deg] - * ZTR: Impedance, magnitude [Ohm] and phase [rad] - * GB: Conductance [S] and susceptance [S] - * YTD: Admittance, magnitude [Ohm] and phase [deg] - * YTR: Admittance magnitude [Ohm] and phase [rad]""", - validator=strict_discrete_set, - values=IMPEDANCE_MODES - ) - - impedance_range = Instrument.control( - "FUNC:IMP:RANG?", "FUNC:IMP:RANG %g", - """Control the impedance measurement range. The 4284A will select an appropriate - measurement range for the setting value.""" - ) - - auto_range_enabled = Instrument.control( - "FUNC:IMP:RANG:AUTO?", "FUNC:IMP:RANG:AUTO %d", - """Control whether the impedance auto range is enabled.""", - validator=strict_discrete_set, - values={False: 0, True: 1} - ) - - def enable_high_power(self): - """Enable high power mode. - - Requires option 001 (power amplifier / DC bias) is installed. - """ - if self.options[0] == 0: - log.warning("Agilent 4284A power amplifier is not installed.") - return - self.ac_current_values = (50e-6, 0.2) - self.ac_voltage_values = (0.005, 20) - self.bias_voltage_values = (0, 40) - self.bias_current_values = (0, 0.1) - self.write("OUTP:HPOW 1") - - def disable_high_power(self): - """Disable high power mode.""" - self.ac_current_values = (50e-6, 0.02) - self.ac_voltage_values = (0.005, 2) - self.bias_voltage_values = (0, 2) - self.bias_current_values = (0, 0) - self.write("OUTP:HPOW 0") - - def frequency_sweep(self, freq_list, return_freq=True): - """Run frequency list sweep using sequential trigger. - - :param freq_list: list of frequencies - :param return_freq: if True, returns the frequencies read from the instrument - - Returns values as configured with :attr:`~.Agilent4284A.impedance_mode` and list - of frequencies in format ([val A], [val B], [frequency]) - """ - if min(freq_list) < 20 or max(freq_list) > 1e6: - log.warning("Agilent 4284A valid frequency range is 20 Hz to 1 MHz.") - return - freq_str = ",".join(['%g' % f for f in freq_list]) - self.reset() - self.clear() - self.write(f"TRIG:SOUR BUS;FORM ASC;LIST:MODE SEQ;LIST:FREQ {freq_str}") - self.write("INIT:CONT ON") - self.write("TRIG:IMM") - # TODO: see if this while block is necessary - # while True: - # if (self.status & 8) == 8: # bit 3 is list sweep measurement complete bit - # break - # else: - # sleep(1) - # continue - measured = self.values("FETCH?") - # at the end return to manual trigger - self.write(":TRIG:SOUR HOLD") - # gets 4-ples of numbers, first two are data A and B - a_data = [measured[_] for _ in range(0, 4 * len(freq_list), 4)] - b_data = [measured[_] for _ in range(1, 4 * len(freq_list), 4)] - if return_freq: - read_freqs = self.values("LIST:FREQ?") - self.check_errors() - return a_data, b_data, read_freqs - else: - self.check_errors() - return a_data, b_data, freq_list - - def trigger(self): - """Execute a bus trigger, which can be used when :meth:`~.trigger_on_bus` - is configured. Returns result of triggered measurement. - """ - return self.write("*TRG") - - def trigger_on_bus(self): - """Configure the trigger to detect events based on the bus trigger, which can be - activated by :meth:`~.trigger`. - """ - self.write("TRIG:SOUR BUS") +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging +from time import sleep + +from pymeasure.instruments import Instrument, SCPIMixin +from pymeasure.instruments.validators import strict_discrete_set, strict_range + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +IMPEDANCE_MODES = ( + "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", + "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR" +) + + +class Agilent4284A(SCPIMixin, Instrument): + """Represents the Agilent 4284A precision LCR meter. + + .. code-block:: python + + agilent = Agilent4284A("GPIB::1::INSTR") + + agilent.reset() # Return instrument settings to default + values + agilent.frequency = 10e3 # Set frequency to 10 kHz + agilent.voltage = 0.02 # Set AC voltage to 20 mV + agilent.mode = 'ZTR' # Set impedance mode to measure + impedance magnitude [Ohm] and phase + [rad] + agilent.sweep_measurement( + 'frequency', [1e4, 1e3, 100] # Perform frequency sweep measurement + ) # at 10 kHz, 1 kHz, and 100 Hz + agilent.enable_high_power() # Enable upper current, voltage, and + bias limits, if properly configured. + + """ + + def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): + kwargs.setdefault("read_termination", '\n') + kwargs.setdefault("write_termination", '\n') + kwargs.setdefault("timeout", 10000) + super().__init__(adapter, name, **kwargs) + self._set_ranges(0) + + frequency = Instrument.control( + "FREQ?", "FREQ %g", + """Control AC frequency in Hertz, from 20 Hz to 1 MHz.""", + validator=strict_range, + values=(20, 1e6), + ) + + ac_current = Instrument.control( + "CURR:LEV?", "CURR:LEV %g", + """"Control AC current level in Amps. Valid range is 50 uA to 20 mA for default, + 50 uA to 200 mA in high-power mode.""", + validator=strict_range, + values=(50e-6, 0.02), + dynamic=True + ) + + ac_voltage = Instrument.control( + "VOLT:LEV?", "VOLT:LEV %g", + """"Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to + 20 V in high-power mode.""", + validator=strict_range, + values=(0.005, 2), + dynamic=True + ) + + high_power_enabled = Instrument.measurement( + "OUTP:HPOW?", + """Get whether the high-power mode is enabled. + High power requires Option 001 (power amplifier / DC bias) is installed.""", + values={False: 0, True: 1}, + map_values=True + ) + + bias_enabled = Instrument.control( + "BIAS:STAT?", "BIAS:STAT %d", + """Control whether DC bias is enabled.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True + ) + + bias_voltage = Instrument.control( + "BIAS:VOLT?", "BIAS:VOLT %g", + """Control the DC bias voltage in Volts. + Maximum is 2 V by default, 40 V in high-power mode.""", + validator=strict_range, + values=(0, 2), + dynamic=True + ) + + bias_current = Instrument.control( + "BIAS:CURR?", "BIAS:CURR %g", + """Control the DC bias current in Amps. + Requires Option 001 (power amplifier / DC bias) is installed. + Maximum is 100 mA.""", + validator=strict_range, + values=(0, 0), + dynamic=True + ) + + impedance_mode = Instrument.control( + "FUNC:IMP?", "FUNC:IMP %s", + """Control impedance measurement function. + + * CPD: Parallel capacitance [F] and dissipation factor [number] + * CPQ: Parallel capacitance [F] and quality factor [number] + * CPG: Parallel capacitance [F] and parallel conductance [S] + * CPRP: Parallel capacitance [F] and parallel resistance [Ohm] + + * CSD: Series capacitance [F] and dissipation factor [number] + * CSQ: Series capacitance [F] and quality factor [number] + * CSRS: Series capacitance [F] and series resistance [Ohm] + + * LPQ: Parallel inductance [H] and quality factor [number] + * LPD: Parallel inductance [H] and dissipation factor [number] + * LPG: Parallel inductance [H] and parallel conductance [S] + * LPRP: Parallel inductance [H] and parallel resistance [Ohm] + + * LSD: Series inductance [H] and dissipation factor [number] + * LSQ: Seriesinductance [H] and quality factor [number] + * LSRS: Series inductance [H] and series resistance [Ohm] + + * RX: Resistance [Ohm] and reactance [Ohm] + * ZTD: Impedance, magnitude [Ohm] and phase [deg] + * ZTR: Impedance, magnitude [Ohm] and phase [rad] + * GB: Conductance [S] and susceptance [S] + * YTD: Admittance, magnitude [Ohm] and phase [deg] + * YTR: Admittance magnitude [Ohm] and phase [rad]""", + validator=strict_discrete_set, + values=IMPEDANCE_MODES + ) + + impedance_range = Instrument.control( + "FUNC:IMP:RANG?", "FUNC:IMP:RANG %g", + """Control the impedance measurement range. The 4284A will select an appropriate + measurement range for the setting value.""" + ) + + auto_range_enabled = Instrument.control( + "FUNC:IMP:RANG:AUTO?", "FUNC:IMP:RANG:AUTO %d", + """Control whether the impedance auto range is enabled.""", + validator=strict_discrete_set, + values={False: 0, True: 1} + ) + + def _set_ranges(self, condition): + """Copy dynamic property values for sweep_measurement method to reference.""" + if condition == 0: + self.ac_current_values = (50e-6, 0.02) + self.ac_voltage_values = (0.005, 2) + self.bias_voltage_values = (0, 2) + self.bias_current_values = (0, 0) + self._ac_current_values = (50e-6, 0.02) + self._ac_voltage_values = (0.005, 2) + self._bias_voltage_values = (0, 2) + self._bias_current_values = (0, 0) + elif condition == 1: + self.ac_current_values = (50e-6, 0.2) + self.ac_voltage_values = (0.005, 20) + self.bias_voltage_values = (0, 40) + self.bias_current_values = (0, 0.1) + self._ac_current_values = (50e-6, 0.2) + self._ac_voltage_values = (0.005, 20) + self._bias_voltage_values = (0, 40) + self._bias_current_values = (0, 0.1) + + def enable_high_power(self): + """Enable high power mode. + + Requires option 001 (power amplifier / DC bias) is installed. + """ + if self.options[0] == '0': + log.warning("Agilent 4284A power amplifier is not installed.") + return + self._set_ranges(1) + self.write("OUTP:HPOW 1") + + def disable_high_power(self): + """Disable high power mode.""" + self._set_ranges(0) + self.write("OUTP:HPOW 0") + + def sweep_measurement(self, sweep_mode, sweep_values): + """Run list sweep measurement using sequential trigger. + + :param sweep_mode (str): parameter to sweep across. Must be one of `frequency`, + `voltage`, `current`, `bias_voltage`, or `bias_current`. + :param sweep_values: list of parameter values to sweep across. + + Returns values as configured with :attr:`~.Agilent4284A.impedance_mode` and list + of sweep parameters in format ([val A], [val B], [sweep_values]) + """ + param_dict = { + "frequency": ("FREQ", (20, 1e6)), + "voltage": ("VOLT", self._ac_voltage_values), + "current": ("CURR", self._ac_current_values), + "bias_voltage": ("BIAS:VOLT", self._bias_voltage_values), + "bias_current": ("BIAS:CURR", self._bias_current_values) + } + + if sweep_mode not in param_dict: + log.warning( + "Sweep mode but be one of %s, not '%s'.", list(param_dict.keys()), sweep_mode + ) + return + + low_limit = param_dict[sweep_mode][1][0] + high_limit = param_dict[sweep_mode][1][1] + if (min(sweep_values) < low_limit or max(sweep_values) > high_limit): + log.warning( + "%s values are outside valid Agilent 4284A range of %g and %g " + "and will be truncated.", sweep_mode, low_limit, high_limit + ) + sweep_truncated = [] + for val in sweep_values: + if val <= high_limit and val >= low_limit: + sweep_truncated.append(val) + sweep_values = sweep_truncated + + loops = (len(sweep_values) - 1) // 10 # 4284A sweeps 10 points at a time + param_div = [] + for i in range(loops): + param_div.append(sweep_values[10*i:10*(i+1)]) + param_div.append(sweep_values[loops*10:]) + + self.clear() + self.write("TRIG:SOUR BUS;:DISP:PAGE LIST;:FORM ASC;:LIST:MODE SEQ;:INIT:CONT ON") + + a_data = [] + b_data = [] + sweep_return = [] + for i in range(loops + 1): + param_str = ",".join(['%g' % p for p in param_div[i]]) + self.write(f"LIST:{param_dict[sweep_mode][0]} {param_str};:TRIG:IMM") + status_event_register = int(self.ask("STAT:OPER?")) + while (status_event_register & 8) != 8: # Sweep bit no. 3 + sleep(0.1) + status_event_register = int(self.ask("STAT:OPER?")) + measured = self.values("FETCH?") + # gets 4-ples of numbers, first two are data A and B + a_data += [measured[_] for _ in range(0, 4 * len(param_div[i]), 4)] + b_data += [measured[_] for _ in range(1, 4 * len(param_div[i]), 4)] + sweep_return += self.values(f"LIST:{param_dict[sweep_mode][0]}?") + + # Return to manual trigger and reset display + self.write(":TRIG:SOUR HOLD;:DISP:PAGE MEAS") + self.check_errors() + return a_data, b_data, sweep_return + + def trigger(self): + """Execute a bus trigger, which can be used when :meth:`~.trigger_on_bus` + is configured. Returns result of triggered measurement. + """ + return self.values("*TRG") + + def trigger_on_bus(self): + """Configure the trigger to detect events based on the bus trigger, which can be + activated by :meth:`~.trigger`. + """ + self.write("TRIG:SOUR BUS") diff --git a/tests/instruments/agilent/test_agilent4284A.py b/tests/instruments/agilent/test_agilent4284A.py index 1a0c3b1f10..807f89241b 100644 --- a/tests/instruments/agilent/test_agilent4284A.py +++ b/tests/instruments/agilent/test_agilent4284A.py @@ -91,20 +91,31 @@ def test_disable_high_power(): inst.ac_voltage = 5 -def test_frequency_sweep(): +@pytest.fixture +def param_list(): + freq_input = [1e7, 5e6, 1e6, 5e5, 1e5, 5e4, 1e4, 5e3, 1e3, 500, 100, 50, 20, 10] + freq_str = "1000000,500000,100000,50000,10000,5000,1000,500,100,50,20" + freq_output = [1e6, 5e5, 1e5, 5e4, 1e4, 5e3, 1e3, 500, 100, 50, 20] + measured_str = "0.5,-0.785,+0,+0," + return freq_input, freq_str, freq_output, measured_str + + +def sweep_measurement(param_list): with expected_protocol( Agilent4284A, - [("*RST", None), - ("*CLS", None), - ("TRIG:SOUR BUS;FORM ASC;LIST:MODE SEQ;LIST:FREQ 50,10000,500000", None), - ("INIT:CONT ON", None), - ("TRIG:IMM", None), - ("FETCH?", "0.5,-0.785,+0,+0,1.5,-0.785,+0,+0,2.5,-0.785,+0,+0"), - (":TRIG:SOUR HOLD", None), - ("LIST:FREQ?", "48,1.01e4,4.89e5"), + [("*CLS", None), + ("TRIG:SOUR BUS;:DISP:PAGE LIST;:FORM ASC;:LIST:MODE SEQ;:INIT:CONT ON", None), + (f"LIST:FREQ {param_list[1][:-3]};:TRIG:IMM", None), + ("STAT:OPER?", "+8"), + ("FETCH?", f"{param_list[3]*10}"), + ("LIST:FREQ?", f"{param_list[1][:-3]}"), + ("LIST:FREQ 20;:TRIG:IMM", None), + ("STAT:OPER?", "+8"), + ("FETCH?", f"{param_list[3]}"), + ("LIST:FREQ?", "20"), ("SYST:ERR?", '0,"No error"')], ) as inst: - results = inst.frequency_sweep([50, 1e4, 5e5], return_freq=True) - assert results[0] == [0.5, 1.5, 2.5] - assert results[1] == [-0.785, -0.785, -0.785] - assert results[2] == [48, 1.01e4, 4.89e5] + results = inst.sweep_measurement('frequency', param_list[0]) + assert results[0] == [0.5,] * 11 + assert results[1] == [-0.785,] * 11 + assert results[2] == param_list[2] From 08da17fe0cded8ac0809fdcf61c0776863f21ec9 Mon Sep 17 00:00:00 2001 From: Connor Carr Date: Thu, 28 Mar 2024 15:05:35 -0400 Subject: [PATCH 068/181] proof-reading --- pymeasure/instruments/agilent/agilent4284A.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/agilent/agilent4284A.py b/pymeasure/instruments/agilent/agilent4284A.py index f5d6146902..34d065576a 100644 --- a/pymeasure/instruments/agilent/agilent4284A.py +++ b/pymeasure/instruments/agilent/agilent4284A.py @@ -56,7 +56,6 @@ class Agilent4284A(SCPIMixin, Instrument): ) # at 10 kHz, 1 kHz, and 100 Hz agilent.enable_high_power() # Enable upper current, voltage, and bias limits, if properly configured. - """ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): @@ -153,7 +152,8 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): * ZTR: Impedance, magnitude [Ohm] and phase [rad] * GB: Conductance [S] and susceptance [S] * YTD: Admittance, magnitude [Ohm] and phase [deg] - * YTR: Admittance magnitude [Ohm] and phase [rad]""", + * YTR: Admittance magnitude [Ohm] and phase [rad] + """, validator=strict_discrete_set, values=IMPEDANCE_MODES ) @@ -172,7 +172,7 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): ) def _set_ranges(self, condition): - """Copy dynamic property values for sweep_measurement method to reference.""" + """Set dynamic property values and make copies for sweep_measurement to reference.""" if condition == 0: self.ac_current_values = (50e-6, 0.02) self.ac_voltage_values = (0.005, 2) @@ -215,8 +215,8 @@ def sweep_measurement(self, sweep_mode, sweep_values): `voltage`, `current`, `bias_voltage`, or `bias_current`. :param sweep_values: list of parameter values to sweep across. - Returns values as configured with :attr:`~.Agilent4284A.impedance_mode` and list - of sweep parameters in format ([val A], [val B], [sweep_values]) + :returns: values as configured with :attr:`~.Agilent4284A.impedance_mode` and + list of sweep parameters in format ([val A], [val B], [sweep_values]) """ param_dict = { "frequency": ("FREQ", (20, 1e6)), From abb0f477e677637d3c5878a95983b2f7eda5c7c9 Mon Sep 17 00:00:00 2001 From: Connor Carr Date: Thu, 28 Mar 2024 15:11:33 -0400 Subject: [PATCH 069/181] fix more typos --- pymeasure/instruments/agilent/agilent4284A.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/agilent/agilent4284A.py b/pymeasure/instruments/agilent/agilent4284A.py index 34d065576a..4f70fdf801 100644 --- a/pymeasure/instruments/agilent/agilent4284A.py +++ b/pymeasure/instruments/agilent/agilent4284A.py @@ -74,7 +74,7 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): ac_current = Instrument.control( "CURR:LEV?", "CURR:LEV %g", - """"Control AC current level in Amps. Valid range is 50 uA to 20 mA for default, + """Control AC current level in Amps. Valid range is 50 uA to 20 mA for default, 50 uA to 200 mA in high-power mode.""", validator=strict_range, values=(50e-6, 0.02), @@ -83,7 +83,7 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): ac_voltage = Instrument.control( "VOLT:LEV?", "VOLT:LEV %g", - """"Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to + """Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to 20 V in high-power mode.""", validator=strict_range, values=(0.005, 2), From f20a753b50954f3168bd9d93f405292b835519e4 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:23:20 +0100 Subject: [PATCH 070/181] Add Kepco BOP to instruments and documentation Initial commit including minimum necessary preliminary information for Kepco BOP bipolar power supply (with BIT 4886 digital interface card). The power supplies come in many variants, at 100 W, 200 W or 400 W power rating, and different combinations of maximum output currents and voltages, e.g. for 400 W, the BOP 36-12 can source/sink 12 A at +/- 36 V, while the BOP 50-8 can source/sink 8 A at +/- 50 V. The current generation of power supplies are fitted (or can be later retrofitted) with a BIT 4886 digital interface card, that can communicate via GPIB or RS-232. Since the card is shared among all BOP variants, my intention is to write a complete class representing the BOP 36-12, from which the other variants can inherit the bulk of the methods and with only the need to change a few properties, e.g. maximum rated voltage and current. --- AUTHORS.txt | 1 + docs/api/instruments/index.rst | 1 + docs/api/instruments/kepco/bop.rst | 7 +++++ docs/api/instruments/kepco/index.rst | 12 ++++++++ pymeasure/instruments/kepco/__init__.py | 25 ++++++++++++++++ pymeasure/instruments/kepco/kepcobop.py | 40 +++++++++++++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 docs/api/instruments/kepco/bop.rst create mode 100644 docs/api/instruments/kepco/index.rst create mode 100644 pymeasure/instruments/kepco/__init__.py create mode 100644 pymeasure/instruments/kepco/kepcobop.py diff --git a/AUTHORS.txt b/AUTHORS.txt index f194175d30..82d8ffc2dd 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -78,3 +78,4 @@ Connor Carr Jannis Kleine-Schönepauck Douwe den Blanken Till Zürner +J. A. Wilcox diff --git a/docs/api/instruments/index.rst b/docs/api/instruments/index.rst index a391002254..a6ab33bd9f 100644 --- a/docs/api/instruments/index.rst +++ b/docs/api/instruments/index.rst @@ -45,6 +45,7 @@ Instruments by manufacturer: inficon/index ipgphotonics/index keithley/index + kepco/index keysight/index lakeshore/index lecroy/index diff --git a/docs/api/instruments/kepco/bop.rst b/docs/api/instruments/kepco/bop.rst new file mode 100644 index 0000000000..a1c8d5c79e --- /dev/null +++ b/docs/api/instruments/kepco/bop.rst @@ -0,0 +1,7 @@ +####################### +BOP Bipolar Power Supply with BIT 4886 Digital Interface Card +####################### + +.. autoclass:: pymeasure.instruments.kepco.kepcobop + :members: + :show-inheritance: diff --git a/docs/api/instruments/kepco/index.rst b/docs/api/instruments/kepco/index.rst new file mode 100644 index 0000000000..5096a855bc --- /dev/null +++ b/docs/api/instruments/kepco/index.rst @@ -0,0 +1,12 @@ +.. module:: pymeasure.instruments.kepco + +######################### +KEPCO INC. +######################### + +This section contains specific dpcumentation on the Kepco Inc. power supplies instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. + +.. toctree:: + :maxdepth: 2 + + bop diff --git a/pymeasure/instruments/kepco/__init__.py b/pymeasure/instruments/kepco/__init__.py new file mode 100644 index 0000000000..725a329a18 --- /dev/null +++ b/pymeasure/instruments/kepco/__init__.py @@ -0,0 +1,25 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from .kepcobop import KepcoBOP3612 diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py new file mode 100644 index 0000000000..e137007f40 --- /dev/null +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -0,0 +1,40 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.instruments import Instrument + + +class KepcoBOP3612(Instrument): + """ Represents the Kepco BOP 36-12 (M or D) 400 W bipolar power supply + (fitted with BIT 4886 digital interface card) + and provides a high-level interface for interacting + with the instrument. + """ + + def __init__(self, adapter, name="Kepco BOP", **kwargs): + super().__init__( + adapter, + name, + **kwargs + ) From 0f8ef4dc70dfe85d198d652c1ad526047529d973 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:15:01 +0100 Subject: [PATCH 071/181] Added basic commands to KepcoBOP6312 Added in a minimal set of commands to the KepcoBOP3612 to do some simple measurements. In particular, setting of output mode, voltage and current setpoints and measurement routines. There are numerous other commands detailed in the manual; however, these are for more sophisticated control and not needed for the moment. --- .gitignore | 1 + pymeasure/instruments/kepco/kepcobop.py | 152 ++++++++++++++++++++++-- 2 files changed, 146 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c2f56e2669..005955daa3 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ settings.json # Vim *.swp +pymeasure.code-workspace diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index e137007f40..709f7829b1 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -22,19 +22,157 @@ # THE SOFTWARE. # -from pymeasure.instruments import Instrument +# List of Kepco BOP's (Vmax,Imax) [single channel output] +# 100 W: +# 5-20 +# 20-5 +# 50-2 +# 100-1 +# 200 W: +# 5-30 +# 20-10 +# 36-6 +# 50-4 +# 72-3 +# 100-2 +# 200-1 -class KepcoBOP3612(Instrument): - """ Represents the Kepco BOP 36-12 (M or D) 400 W bipolar power supply - (fitted with BIT 4886 digital interface card) - and provides a high-level interface for interacting - with the instrument. +# 400 W: +# 20-20 +# 36-12 +# 50-8 +# 72-6 +# 100-4 + +from pymeasure.instruments import Instrument, SCPIMixin +from pymeasure.instruments.validators import strict_discrete_set, \ + truncated_range + +OPERATING_MODES = ['VOLT', 'CURR'] + + +class KepcoBOP3612(SCPIMixin, Instrument): """ + Represents the Kepco BOP 36-12 (M or D) 400 W bipolar power supply + fitted with BIT 4886 digital interface card (minimal implementation) + and provides a high-level interface for interacting with the instrument. + """ + _Vmax = 36 + _Imax = 12 - def __init__(self, adapter, name="Kepco BOP", **kwargs): + def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", + read_termination="\n", write_termination="\n", **kwargs): super().__init__( adapter, name, + read_termination=read_termination, + write_termination=write_termination, **kwargs ) + + output_enabled = Instrument.control( + "OUTPut?", + "OUTPut %d", + """ + Control whether the source is enabled, takes values True or False (bool) + """, + validator=strict_discrete_set, + values={True: 1, False: 0}, + map_values=True, + ) + + def beep(self): + """Causes the unit to emit a brief audible tone.""" + self.write("SYSTem:BEEP") + + confidence_test = Instrument.measurement( + "*TST?", + """ + Power supply interface self-test procedure. + Returns 0 if all tests passed, + otherwise corresponding error code. + """, + cast=int + ) + + bop_test = Instrument.measurement( + "DIAG:TST?", + """ + Power supply self-test (includes interface plus BOP operation). + Caution: Output will switch on and swing to maximum values. + Disconnect any load before testing. + Reutnrs 0 if all tests passed, + otherwise corresponding error code. + """, + cast=int + ) + + def wait_to_continue(self): + """ Causes the power supply to wait until all previously issued + commands and queries are complete before executing subsequent + commands or queries. """ + self.write("*WAI") + + voltage_measure = Instrument.measurement( + "MEASure:VOLTage?", + """ + Measures actual voltage that is across the output terminals. + """, + cast=float + ) + + current_measure = Instrument.measurement( + "MEASure:CURRent?", + """ + Measures the actual current through the output terminals. + """, + cast=float + ) + + operating_mode = Instrument.control( + "FUNCtion:MODE?", "FUNCtion:MODE %s", + """ + A string property that controls the operating mode of the BOP. + As a command, a string, VOLT or CURR, is sent. + As a query, a 0 or 1 is returned, corresponding to (VOLT, CURR) respectively. + This is mapped to corresponding string. + """, + validator=strict_discrete_set, + values=OPERATING_MODES, + get_process=lambda x: OPERATING_MODES[int(x)] + ) + + current = Instrument.control( + "CURRent?", "CURRent %g", + """ + Sets the output current setpoint, depending on the operating mode. + If power supply in current mode, this sets the output current, + depending on the voltage compliance and load conditions. + If power supply in voltage mode, this sets the compliance current + for the corresponding voltage set point. + Query returns corresponding programmed value, meaning of which + is dependent on power supply operating context (see: `operating_mode`). + Set current not same as actual current (see: `current_measure`). + Output must be enabled separately (see: `output_enabled`) + """, + validator=truncated_range, + values=[-1*_Imax, _Imax] + ) + + voltage = Instrument.control( + "VOLTage?", "VOLTage %g", + """ + Sets the output voltage setpoint, depending on the operating mode. + If power supply in voltage mode, this sets the output voltage, + depending on the current compliance and load conditions. + If power supply in current mode, this sets the compliance voltage + for the corresponding current set point. + Query returns corresponding programmed value, meaning of which + is dependent on power supply operating context (see: `operating_mode`). + Set voltage not same as actual voltage (see: `voltage_measure`). + Output must be enabled separately (see: `output_enabled`) + """, + validator=truncated_range, + values=[-1*_Vmax, _Vmax] + ) From bd6459b96b36f4ddcea74fd707db540300d01c37 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:22:23 +0100 Subject: [PATCH 072/181] Rename methods Rename a few methods in keeping with recommendations of documentation (e.g. voltage_measure => voltage). Additionally, specify default baud_rate expected for these devices. --- pymeasure/instruments/kepco/kepcobop.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index 709f7829b1..f8aea6ebbd 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -62,10 +62,12 @@ class KepcoBOP3612(SCPIMixin, Instrument): _Imax = 12 def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", - read_termination="\n", write_termination="\n", **kwargs): + baud_rate=9600, read_termination="\n", write_termination="\n", + **kwargs): super().__init__( adapter, name, + baud_rate=baud_rate, read_termination=read_termination, write_termination=write_termination, **kwargs @@ -114,7 +116,7 @@ def wait_to_continue(self): commands or queries. """ self.write("*WAI") - voltage_measure = Instrument.measurement( + voltage = Instrument.measurement( "MEASure:VOLTage?", """ Measures actual voltage that is across the output terminals. @@ -122,7 +124,7 @@ def wait_to_continue(self): cast=float ) - current_measure = Instrument.measurement( + current = Instrument.measurement( "MEASure:CURRent?", """ Measures the actual current through the output terminals. @@ -143,7 +145,7 @@ def wait_to_continue(self): get_process=lambda x: OPERATING_MODES[int(x)] ) - current = Instrument.control( + current_setpoint = Instrument.control( "CURRent?", "CURRent %g", """ Sets the output current setpoint, depending on the operating mode. @@ -160,7 +162,7 @@ def wait_to_continue(self): values=[-1*_Imax, _Imax] ) - voltage = Instrument.control( + voltage_setpoint = Instrument.control( "VOLTage?", "VOLTage %g", """ Sets the output voltage setpoint, depending on the operating mode. From 2f16f9e27ab4efe66f51a7bfdf01fb3afdf6a2ab Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:30:32 +0100 Subject: [PATCH 073/181] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 005955daa3..c2f56e2669 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,3 @@ settings.json # Vim *.swp -pymeasure.code-workspace From ba6f47a1291fb499f299262cce04db4c5b892bd2 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:47:02 +0100 Subject: [PATCH 074/181] Update Docs Adjust overline length for titles --- docs/api/instruments/kepco/bop.rst | 4 ++-- docs/api/instruments/kepco/index.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/instruments/kepco/bop.rst b/docs/api/instruments/kepco/bop.rst index a1c8d5c79e..37b120e669 100644 --- a/docs/api/instruments/kepco/bop.rst +++ b/docs/api/instruments/kepco/bop.rst @@ -1,6 +1,6 @@ -####################### +############################################################# BOP Bipolar Power Supply with BIT 4886 Digital Interface Card -####################### +############################################################# .. autoclass:: pymeasure.instruments.kepco.kepcobop :members: diff --git a/docs/api/instruments/kepco/index.rst b/docs/api/instruments/kepco/index.rst index 5096a855bc..ffe0687b6a 100644 --- a/docs/api/instruments/kepco/index.rst +++ b/docs/api/instruments/kepco/index.rst @@ -1,8 +1,8 @@ .. module:: pymeasure.instruments.kepco -######################### +########## KEPCO INC. -######################### +########## This section contains specific dpcumentation on the Kepco Inc. power supplies instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. From 6e401c495e58d33f62a4bb618799419804f428e6 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:50:32 +0100 Subject: [PATCH 075/181] Hard code baud_rate in super().__init__() The communication protocol of the interface is pre-programmed and cannot be changed. Change the `baud_rate`, `read_termination` and `write_termination` to hard-coded values. --- pymeasure/instruments/kepco/kepcobop.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index f8aea6ebbd..a2cf0c4737 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -62,14 +62,13 @@ class KepcoBOP3612(SCPIMixin, Instrument): _Imax = 12 def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", - baud_rate=9600, read_termination="\n", write_termination="\n", **kwargs): super().__init__( adapter, name, - baud_rate=baud_rate, - read_termination=read_termination, - write_termination=write_termination, + baud_rate=9600, + read_termination="\n", + write_termination="\n", **kwargs ) From 91d544503a7a29e8f8092bcdc3b633dada7a90be Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:18:32 +0100 Subject: [PATCH 076/181] Corrected language in docstrings Corrected the language in docstrings to include imperative voice and also keywords (`Get, Control, Measure, Set`) for corresponding `Instrument` methods. --- pymeasure/instruments/kepco/kepcobop.py | 60 +++++++++++++++---------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index a2cf0c4737..4eb775a73b 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -83,16 +83,20 @@ def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", map_values=True, ) + # TODO + # Return list of errors based on return number. + def beep(self): - """Causes the unit to emit a brief audible tone.""" + """Cause the unit to emit a brief audible tone.""" self.write("SYSTem:BEEP") confidence_test = Instrument.measurement( "*TST?", """ - Power supply interface self-test procedure. - Returns 0 if all tests passed, - otherwise corresponding error code. + Get error code after performing interface self-test procedure. + + Returns 0 if all tests passed, otherwise corresponding error code + as detailed in manual. """, cast=int ) @@ -100,17 +104,18 @@ def beep(self): bop_test = Instrument.measurement( "DIAG:TST?", """ - Power supply self-test (includes interface plus BOP operation). + Get error code after performing full power supply self-test. + + Returns 0 if all tests passed, otherwise corresponding error code + as detailed in manual. Caution: Output will switch on and swing to maximum values. Disconnect any load before testing. - Reutnrs 0 if all tests passed, - otherwise corresponding error code. """, cast=int ) def wait_to_continue(self): - """ Causes the power supply to wait until all previously issued + """ Cause the power supply to wait until all previously issued commands and queries are complete before executing subsequent commands or queries. """ self.write("*WAI") @@ -118,7 +123,7 @@ def wait_to_continue(self): voltage = Instrument.measurement( "MEASure:VOLTage?", """ - Measures actual voltage that is across the output terminals. + Measure voltage present across the output terminals in Volts. """, cast=float ) @@ -126,7 +131,7 @@ def wait_to_continue(self): current = Instrument.measurement( "MEASure:CURRent?", """ - Measures the actual current through the output terminals. + Measure current through the output terminals in Amps. """, cast=float ) @@ -134,9 +139,10 @@ def wait_to_continue(self): operating_mode = Instrument.control( "FUNCtion:MODE?", "FUNCtion:MODE %s", """ - A string property that controls the operating mode of the BOP. + Control the operating mode of the BOP. + As a command, a string, VOLT or CURR, is sent. - As a query, a 0 or 1 is returned, corresponding to (VOLT, CURR) respectively. + As a query, a 0 or 1 is returned, corresponding to VOLT or CURR respectively. This is mapped to corresponding string. """, validator=strict_discrete_set, @@ -147,14 +153,17 @@ def wait_to_continue(self): current_setpoint = Instrument.control( "CURRent?", "CURRent %g", """ - Sets the output current setpoint, depending on the operating mode. - If power supply in current mode, this sets the output current, - depending on the voltage compliance and load conditions. + Control the output current setpoint. + + Functionality depends on the operating mode. + If power supply in current mode, this sets the output current setpoint. + The current achieved depends on the voltage compliance and load conditions + (see: `current`). If power supply in voltage mode, this sets the compliance current for the corresponding voltage set point. - Query returns corresponding programmed value, meaning of which - is dependent on power supply operating context (see: `operating_mode`). - Set current not same as actual current (see: `current_measure`). + Query returns programmed value, meaning of which is dependent on + power supply operating context (see: `operating_mode`). + Output must be enabled separately (see: `output_enabled`) """, validator=truncated_range, @@ -164,14 +173,17 @@ def wait_to_continue(self): voltage_setpoint = Instrument.control( "VOLTage?", "VOLTage %g", """ - Sets the output voltage setpoint, depending on the operating mode. - If power supply in voltage mode, this sets the output voltage, - depending on the current compliance and load conditions. + Control the output voltage setpoint. + + Functionality depends on the operating mode. + If power supply in voltage mode, this sets the output voltage setpoint. + The voltage achieved depends on the current compliance and load conditions + (see: `voltage`). If power supply in current mode, this sets the compliance voltage for the corresponding current set point. - Query returns corresponding programmed value, meaning of which - is dependent on power supply operating context (see: `operating_mode`). - Set voltage not same as actual voltage (see: `voltage_measure`). + Query returns programmed value, meaning of which is dependent on + power supply operating context (see: `operating_mode`). + Output must be enabled separately (see: `output_enabled`) """, validator=truncated_range, From da4c711a4fa66ec4e15c1f4c8d8f3160bba149de Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:21:47 +0100 Subject: [PATCH 077/181] Linting Removed trailing whitespace. --- pymeasure/instruments/kepco/kepcobop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index 4eb775a73b..f2d27511f1 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -83,7 +83,7 @@ def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", map_values=True, ) - # TODO + # TODO # Return list of errors based on return number. def beep(self): @@ -161,7 +161,7 @@ def wait_to_continue(self): (see: `current`). If power supply in voltage mode, this sets the compliance current for the corresponding voltage set point. - Query returns programmed value, meaning of which is dependent on + Query returns programmed value, meaning of which is dependent on power supply operating context (see: `operating_mode`). Output must be enabled separately (see: `output_enabled`) @@ -181,7 +181,7 @@ def wait_to_continue(self): (see: `voltage`). If power supply in current mode, this sets the compliance voltage for the corresponding current set point. - Query returns programmed value, meaning of which is dependent on + Query returns programmed value, meaning of which is dependent on power supply operating context (see: `operating_mode`). Output must be enabled separately (see: `output_enabled`) From 72b59706f51cecae3ad84e503c53675a83bf9d52 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:54:29 +0100 Subject: [PATCH 078/181] Add error codes to kepcobop Use `IntFlag` to determine errors based on returned error code from `confidence_test` and `bop_test`. --- pymeasure/instruments/kepco/kepcobop.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index f2d27511f1..2e08326079 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -45,6 +45,7 @@ # 72-6 # 100-4 +from enum import IntFlag from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, \ truncated_range @@ -52,6 +53,20 @@ OPERATING_MODES = ['VOLT', 'CURR'] +class TestErrorCode(IntFlag): + QUARTER_SCALE_VOLTAGE_READBACK = 512 + QUARTER_SCALE_VOLTAGE = 256 + MIN_VOLTAGE_OUTPUT = 128 + MAX_VOLTAGE_OUTPUT = 64 + LOOP_BACK_TEST = 32 + DIGITAL_POT = 16 + OPTICAL_BUFFER = 8 + FLASH = 4 + RAM = 2 + ROM = 1 + OK = 0 + + class KepcoBOP3612(SCPIMixin, Instrument): """ Represents the Kepco BOP 36-12 (M or D) 400 W bipolar power supply @@ -83,9 +98,6 @@ def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", map_values=True, ) - # TODO - # Return list of errors based on return number. - def beep(self): """Cause the unit to emit a brief audible tone.""" self.write("SYSTem:BEEP") @@ -98,7 +110,7 @@ def beep(self): Returns 0 if all tests passed, otherwise corresponding error code as detailed in manual. """, - cast=int + get_process=lambda v: TestErrorCode(v), ) bop_test = Instrument.measurement( @@ -111,7 +123,7 @@ def beep(self): Caution: Output will switch on and swing to maximum values. Disconnect any load before testing. """, - cast=int + get_process=lambda v: TestErrorCode(v), ) def wait_to_continue(self): From 53bd5e9a62f477bed203357c3083c9312967f6e2 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:39:11 +0100 Subject: [PATCH 079/181] Update hardcoded GPIB configuration `baud_rate` is not a valid keyword for GPIBInstrument (pyvisa). Since actual `baud_rate` is same as default, don't specify any. Unsure how you would change the `baud_rate` of a GPIB instrument... --- pymeasure/instruments/kepco/kepcobop.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pymeasure/instruments/kepco/kepcobop.py b/pymeasure/instruments/kepco/kepcobop.py index 2e08326079..270c301986 100644 --- a/pymeasure/instruments/kepco/kepcobop.py +++ b/pymeasure/instruments/kepco/kepcobop.py @@ -79,9 +79,8 @@ class KepcoBOP3612(SCPIMixin, Instrument): def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", **kwargs): super().__init__( - adapter, - name, - baud_rate=9600, + adapter=adapter, + name=name, read_termination="\n", write_termination="\n", **kwargs From 70f131aacabecc7c6bd044f800ddb93d7ff42e22 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:08:53 +0200 Subject: [PATCH 080/181] change empty default dictionaries. --- pymeasure/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymeasure/test.py b/pymeasure/test.py index 5b5df68c58..0ab639b88c 100644 --- a/pymeasure/test.py +++ b/pymeasure/test.py @@ -29,7 +29,7 @@ @contextmanager def expected_protocol(instrument_cls, comm_pairs, - connection_attributes={}, connection_methods={}, + connection_attributes=None, connection_methods=None, **kwargs): """Context manager that checks sent/received instrument commands without a device connected. From 14a9393be4b1c149ccb346a2d0458ac9a6238217 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:09:03 +0200 Subject: [PATCH 081/181] Fix documentation typos. --- pymeasure/instruments/agilent/agilentB1500.py | 2 +- pymeasure/instruments/hcp/tc038.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/agilent/agilentB1500.py b/pymeasure/instruments/agilent/agilentB1500.py index 8afff56c3e..35af9b642a 100644 --- a/pymeasure/instruments/agilent/agilentB1500.py +++ b/pymeasure/instruments/agilent/agilentB1500.py @@ -277,7 +277,7 @@ class _data_formatting_generic(): smu_status = { 1: 'A/D converter overflowed.', 2: 'Oscillation of force or saturation current.', - 4: 'Antoher unit reached its compliance setting.', + 4: 'Another unit reached its compliance setting.', 8: 'This unit reached its compliance setting.', 16: 'Target value was not found within the search range.', 32: 'Search measurement was automatically stopped.', diff --git a/pymeasure/instruments/hcp/tc038.py b/pymeasure/instruments/hcp/tc038.py index 13773ac918..4d6958b30d 100644 --- a/pymeasure/instruments/hcp/tc038.py +++ b/pymeasure/instruments/hcp/tc038.py @@ -161,6 +161,6 @@ def set_monitored_quantity(self, quantity='temperature'): information = Instrument.measurement( "INF6", - """Get the information about the device and its capabilites.""", + """Get the information about the device and its capabilities.""", get_process=lambda got: got[7:-1], ) From 9935ecba028d291db00469c49fd6ffc28bc21010 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:26:55 +0200 Subject: [PATCH 082/181] Fix hp8116a to use staticmethods. Fixes #1006 --- pymeasure/instruments/hp/hp8116a.py | 2 ++ tests/instruments/hp/test_hp8116a.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py index a0c140c7b9..b25ef4c1c1 100644 --- a/pymeasure/instruments/hp/hp8116a.py +++ b/pymeasure/instruments/hp/hp8116a.py @@ -197,6 +197,7 @@ def _parse_value_with_unit(value_str, units): return value + @staticmethod def _generate_1_2_5_sequence(min, max): """ Generate a list of a 1-2-5 sequence between min and max. """ exp_min = int(np.log10(min)) @@ -209,6 +210,7 @@ def _generate_1_2_5_sequence(min, max): return list(sequence) + @staticmethod def _boolean_control(identifier, state_index, docs, inverted=False, **kwargs): return Instrument.control( 'CST', identifier + '%d', docs, diff --git a/tests/instruments/hp/test_hp8116a.py b/tests/instruments/hp/test_hp8116a.py index a4e2310679..33c0b4b397 100644 --- a/tests/instruments/hp/test_hp8116a.py +++ b/tests/instruments/hp/test_hp8116a.py @@ -31,6 +31,9 @@ HP8116A.status = property(fget=lambda self: Status(5)) +init_comm = [(b"CST", b"x" * 87 + b' ,\r\n')] # communication during init + + def test_init(): with expected_protocol( HP8116A, @@ -53,3 +56,14 @@ def test_duty_cycle_setter(): [(b"CST", b"x" * 87 + b' ,\r\n'), (b"DTY 34.5 %", None)], ) as instr: instr.duty_cycle = 34.5 + + +def test_sweep_time(): + with expected_protocol(HP8116A, init_comm + [("SWT 5 S", None)]) as inst: + # This test tests also the generate_1_2_5_sequence method and truncation. + inst.sweep_time = 3 + + +def test_limit_enabled(): + with expected_protocol(HP8116A, init_comm + [("L1", None)]) as inst: + inst.limit_enabled = True From 8037517907754b415f293e505d66a39a32786031 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:41:02 +0200 Subject: [PATCH 083/181] Fix hp8116a docstrings --- pymeasure/instruments/hp/hp8116a.py | 62 +++++++++++------------ tests/instruments/test_all_instruments.py | 1 - 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py index b25ef4c1c1..f31df1227f 100644 --- a/pymeasure/instruments/hp/hp8116a.py +++ b/pymeasure/instruments/hp/hp8116a.py @@ -256,7 +256,7 @@ def ask(self, command, num_bytes=None): operating_mode = Instrument.control( 'CST', '%s', - """ A string property that controls the operating mode of the instrument. + """Control the operating mode of the instrument. Possible values (without Option 001) are: 'normal', 'triggered', 'gate', 'external_width'. With Option 001, 'internal_sweep', 'external_sweep', 'external_width', 'external_pulse' are also available. @@ -269,7 +269,7 @@ def ask(self, command, num_bytes=None): control_mode = Instrument.control( 'CST', '%s', - """ A string property that controls the control mode of the instrument. + """Control the control mode of the instrument. Possible values are 'off', 'FM', 'AM', 'PWM', 'VCO'. """, validator=strict_discrete_set, @@ -280,7 +280,7 @@ def ask(self, command, num_bytes=None): trigger_slope = Instrument.control( 'CST', '%s', - """ A string property that controls the slope the trigger triggers on. + """Control the slope the trigger triggers on. Possible values are: 'off', 'positive', 'negative'. """, validator=strict_discrete_set, @@ -291,7 +291,7 @@ def ask(self, command, num_bytes=None): shape = Instrument.control( 'CST', '%s', - """ A string property that controls the shape of the output waveform. + """Control the shape of the output waveform. Possible values are: 'dc', 'sine', 'triangle', 'square', 'pulse'. """, validator=strict_discrete_set, @@ -302,39 +302,36 @@ def ask(self, command, num_bytes=None): haversine_enabled = _boolean_control( 'H', 4, - """ A boolean property that controls whether a haversine/havertriangle signal + """Control whether a haversine/havertriangle signal is generated when in 'triggered', 'internal_burst' or 'external_burst' operating mode. """, ) autovernier_enabled = _boolean_control( 'A', 5, - """ A boolean property that controls whether the autovernier is enabled. """, + """Control whether the autovernier is enabled (bool).""", check_set_errors=True ) limit_enabled = _boolean_control( 'L', 6, - """ A boolean property that controls whether parameter limiting is enabled. """, + """Control whether parameter limiting is enabled (bool).""", ) complement_enabled = _boolean_control( 'C', 7, - """ A boolean property that controls whether the complement - of the signal is generated. - """, + """Control whether the complement of the signal is generated (bool).""", ) output_enabled = _boolean_control( 'D', 8, - """ A boolean property that controls whether the output is enabled. """, + """Control whether the output is enabled (bool).""", inverted=True, # The actual command is "Disable output"... ) frequency = Instrument.control( 'IFRQ', 'FRQ %s', - """ A floating point value that controls the frequency of the - output in Hz. The allowed frequency range is 1 mHz to 52.5 MHz. + """Control the frequency of the output in Hz. (strict float from 1 mHz to 52.5 MHz). """, validator=strict_range, values=[1e-3, 52.5001e6], @@ -344,7 +341,7 @@ def ask(self, command, num_bytes=None): duty_cycle = Instrument.control( 'IDTY', 'DTY %s %%', - """ An integer value that controls the duty cycle of the output in percent. + """Control the duty cycle of the output in percent (float). The allowed range generally is 10 % to 90 %, but it also depends on the current frequency. It is valid for all shapes except 'pulse', where :py:attr:`pulse_width` is used instead. """, @@ -356,7 +353,7 @@ def ask(self, command, num_bytes=None): pulse_width = Instrument.control( 'IWID', 'WID %s', - """ A floating point value that controls the pulse width. + """Control the pulse width (float). The allowed pulse width range is 8 ns to 999 ms. The pulse width may not be larger than the period. """, @@ -368,8 +365,8 @@ def ask(self, command, num_bytes=None): amplitude = Instrument.control( 'IAMP', 'AMP %s', - """ A floating point value that controls the amplitude of the - output in V. The allowed amplitude range generally is 10 mV to 16 V, + """Control the amplitude of the output in V (float). + The allowed amplitude range generally is 10 mV to 16 V, but it is also limited by the current offset. """, validator=strict_range, @@ -380,8 +377,8 @@ def ask(self, command, num_bytes=None): offset = Instrument.control( 'IOFS', 'OFS %s', - """ A floating point value that controls the offset of the - output in V. The allowed offset range generally is -7.95 V to 7.95 V, + """Control the offset of the output in V (float). + The allowed offset range generally is -7.95 V to 7.95 V, but it is also limited by the amplitude. """, validator=strict_range, @@ -392,8 +389,8 @@ def ask(self, command, num_bytes=None): high_level = Instrument.control( 'IHIL', 'HIL %s', - """ A floating point value that controls the high level of the - output in V. The allowed high level range generally is -7.9 V to 8 V, + """Control the high level of the output in V (float). + The allowed high level range generally is -7.9 V to 8 V, but it must be at least 10 mV greater than the low level. """, validator=strict_range, @@ -404,8 +401,8 @@ def ask(self, command, num_bytes=None): low_level = Instrument.control( 'ILOL', 'LOL %s', - """ A floating point value that controls the low level of the - output in V. The allowed low level range generally is -8 V to 7.9 V, + """Control the low level of the output in V (float). + The allowed low level range generally is -8 V to 7.9 V, but it must be at least 10 mV less than the high level. """, validator=strict_range, @@ -416,7 +413,7 @@ def ask(self, command, num_bytes=None): burst_number = Instrument.control( 'IBUR', 'BUR %s #', - """ An integer value that controls the number of periods generated in a burst. + """Control the number of periods generated in a burst (int). The allowed range is 1 to 1999. It is only valid for units with Option 001 in one of the burst modes. """, @@ -427,8 +424,8 @@ def ask(self, command, num_bytes=None): repetition_rate = Instrument.control( 'IRPT', 'RPT %s', - """ A floating point value that controls the repetition rate (= the time between bursts) - in 'internal_burst' mode. The allowed range is 20 ns to 999 ms. + """Control the repetition rate (= the time between bursts) + in 'internal_burst' mode (strict float from 20 ns to 999 ms). """, validator=strict_range, values=[20e-9, 999.001e-3], @@ -438,7 +435,7 @@ def ask(self, command, num_bytes=None): sweep_start = Instrument.control( 'ISTA', 'STA %s', - """ A floating point value that controls the start frequency in both sweep modes. + """Control the start frequency in both sweep modes (float). The allowed range is 1 mHz to 52.5 MHz. """, validator=strict_range, @@ -449,7 +446,7 @@ def ask(self, command, num_bytes=None): sweep_stop = Instrument.control( 'ISTP', 'STP %s', - """ A floating point value that controls the stop frequency in both sweep modes. + """Control the stop frequency in both sweep modes (float). The allowed range is 1 mHz to 52.5 MHz. """, validator=strict_range, @@ -460,7 +457,7 @@ def ask(self, command, num_bytes=None): sweep_marker_frequency = Instrument.control( 'IMRK', 'MRK %s', - """ A floating point value that controls the frequency marker in both sweep modes. + """Control the frequency marker in both sweep modes (float). At this frequency, the marker output switches from low to high. The allowed range is 1 mHz to 52.5 MHz. """, @@ -472,7 +469,7 @@ def ask(self, command, num_bytes=None): sweep_time = Instrument.control( 'ISWT', 'SWT %s', - """ A floating point value that controls the sweep time per decade in both sweep modes. + """Control the sweep time per decade in both sweep modes (float). The sweep time is selectable in a 1-2-5 sequence between 10 ms and 500 s. """, validator=truncated_discrete_set, @@ -483,16 +480,17 @@ def ask(self, command, num_bytes=None): @property def status(self): - """ Returns the status byte of the 8116A as an IntFlag-type enum. """ + """Get the status byte of the 8116A as a :class:`Status` IntFlag-type enum.""" return Status(self.adapter.connection.read_stb()) @property def complete(self): + """Get whether the measurement is complete (bool).""" return not (self.status & Status.buffer_not_empty) @property def options(self): - """ Return the device options installed. The only possible option is 001. """ + """Get the device options installed. The only possible option is 001.""" if self.has_option_001: return ['001'] else: diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py index f09ac2b8c4..55ac486ef0 100644 --- a/tests/instruments/test_all_instruments.py +++ b/tests/instruments/test_all_instruments.py @@ -156,7 +156,6 @@ def find_devices_in_module(module): "HP6632A", "HP6633A", "HP6634A", - "HP8116A", "Keithley2000", "Keithley2306", "Keithley2306Channel", From 7a256cba267f34a0c4503bbbb250c2302a7ffc34 Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:56:10 +0100 Subject: [PATCH 084/181] Create test_kepcobop.py Create test unit for KepcoBOP3612 using generator. --- tests/instruments/kepco/test_kepcobop.py | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/instruments/kepco/test_kepcobop.py diff --git a/tests/instruments/kepco/test_kepcobop.py b/tests/instruments/kepco/test_kepcobop.py new file mode 100644 index 0000000000..f488717483 --- /dev/null +++ b/tests/instruments/kepco/test_kepcobop.py @@ -0,0 +1,123 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import pytest + +from pymeasure.test import expected_protocol +from pymeasure.instruments.kepco import KepcoBOP3612 + + +def test_init(): + with expected_protocol( + KepcoBOP3612, + [], + ): + pass # Verify the expected communication. + + +def test_bop_test_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'DIAG:TST?', b'0')], + ) as inst: + assert inst.bop_test == 0 + + +def test_confidence_test_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'*TST?', b'0')], + ) as inst: + assert inst.confidence_test == 0 + + +def test_current_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'MEASure:CURRent?', b'4.99E-3')], + ) as inst: + assert inst.current == 0.00499 + + +def test_current_setpoint_setter(): + with expected_protocol( + KepcoBOP3612, + [(b'CURRent 0.1', None)], + ) as inst: + inst.current_setpoint = 0.1 + + +def test_current_setpoint_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'CURRent?', b'9.989E-2')], + ) as inst: + assert inst.current_setpoint == 0.09989 + + +def test_id_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'*IDN?', b'KEPCO,BIT 4886 36-12 08-04-2023,H249977,4.04-1.82')], + ) as inst: + assert inst.id == 'KEPCO,BIT 4886 36-12 08-04-2023,H249977,4.04-1.82' + + +def test_output_enabled_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'OUTPut?', b'0')], + ) as inst: + assert inst.output_enabled is False + + +def test_voltage_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'MEASure:VOLTage?', b'8.0E-3')], + ) as inst: + assert inst.voltage == 0.008 + + +def test_voltage_setpoint_setter(): + with expected_protocol( + KepcoBOP3612, + [(b'VOLTage 0.1', None)], + ) as inst: + inst.voltage_setpoint = 0.1 + + +def test_voltage_setpoint_getter(): + with expected_protocol( + KepcoBOP3612, + [(b'VOLTage?', b'1.000E-1')], + ) as inst: + assert inst.voltage_setpoint == 0.1 + + +def test_beep(): + with expected_protocol( + KepcoBOP3612, + [(b'SYSTem:BEEP', None)], + ) as inst: + assert inst.beep() is None From e15c0c18bccc682b73428ba47e74ad3724f9402a Mon Sep 17 00:00:00 2001 From: Joe Wilcox <31365972+JAW90@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:58:37 +0100 Subject: [PATCH 085/181] Update test_kepcobop.py Remove unused import --- tests/instruments/kepco/test_kepcobop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/instruments/kepco/test_kepcobop.py b/tests/instruments/kepco/test_kepcobop.py index f488717483..8283dca63c 100644 --- a/tests/instruments/kepco/test_kepcobop.py +++ b/tests/instruments/kepco/test_kepcobop.py @@ -21,7 +21,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.kepco import KepcoBOP3612 From a1fc5b6936215c8c6ef9dcf9e893ebae906ac4f4 Mon Sep 17 00:00:00 2001 From: mcdo0486 Date: Thu, 11 Apr 2024 12:46:24 -0500 Subject: [PATCH 086/181] Add citation file for PyMeasure repository --- CITATION.cff | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..502ac4a822 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,9 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- name: PyMeasure Developers +title: "PyMeasure" +version: 0.13.1 +repository-code: https://github.com/pymeasure/pymeasure +commit: a064eef80cb072d1c2e905e2f64f0a0455060446 + From a897a5eb1df93b0a28f36019feca600fab2b3e26 Mon Sep 17 00:00:00 2001 From: mcdo0486 Date: Fri, 26 Apr 2024 14:15:58 -0500 Subject: [PATCH 087/181] Trying old ZENODO DOI to all software collections --- CITATION.cff | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CITATION.cff b/CITATION.cff index 502ac4a822..35cd9e93e7 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,6 +4,9 @@ authors: - name: PyMeasure Developers title: "PyMeasure" version: 0.13.1 +identifiers: + - type: doi + value: 10.5281/zenodo.595633 repository-code: https://github.com/pymeasure/pymeasure commit: a064eef80cb072d1c2e905e2f64f0a0455060446 From c636137c357e6ed9b495548dc52faab1957d629a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 29 Apr 2024 09:30:14 +0200 Subject: [PATCH 088/181] Use permanent Zenodo DOI in readme.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index aa2c99d705..4dd743cfae 100644 --- a/README.rst +++ b/README.rst @@ -19,8 +19,8 @@ PyMeasure runs on Python 3.8-3.11, and is tested with continuous-integration on :target: http://pymeasure.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3732545.svg - :target: https://doi.org/10.5281/zenodo.3732545 +.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.595633.svg + :target: https://doi.org/10.5281/zenodo.595633 .. image:: https://anaconda.org/conda-forge/pymeasure/badges/version.svg :target: https://anaconda.org/conda-forge/pymeasure From 82d8f646b44bdd334cdb4bf3a8aaabb1bd83a0dd Mon Sep 17 00:00:00 2001 From: mcdo0486 Date: Mon, 29 Apr 2024 09:02:34 -0500 Subject: [PATCH 089/181] Move DOI from `identifiers` to `doi`. Remove commit hash. --- CITATION.cff | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 35cd9e93e7..67b80cb52a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,9 +4,5 @@ authors: - name: PyMeasure Developers title: "PyMeasure" version: 0.13.1 -identifiers: - - type: doi - value: 10.5281/zenodo.595633 +doi: 10.5281/zenodo.595633 repository-code: https://github.com/pymeasure/pymeasure -commit: a064eef80cb072d1c2e905e2f64f0a0455060446 - From 7acb0a650ea64ba1e1c6c070ccf5d479ab3a96e4 Mon Sep 17 00:00:00 2001 From: mcdo0486 Date: Mon, 29 Apr 2024 11:43:49 -0500 Subject: [PATCH 090/181] Add Zenodo publisher to CITATION.cff --- CITATION.cff | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CITATION.cff b/CITATION.cff index 67b80cb52a..e175a69cb5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,4 +5,6 @@ authors: title: "PyMeasure" version: 0.13.1 doi: 10.5281/zenodo.595633 +publisher: +- name: Zenodo repository-code: https://github.com/pymeasure/pymeasure From 74e4ef6d3e536f4e4194e09ff926acde7a7cc7d0 Mon Sep 17 00:00:00 2001 From: mcdo0486 Date: Mon, 29 Apr 2024 14:38:16 -0500 Subject: [PATCH 091/181] Up pyvisa to 1.13.0 for capatibility for macos-latest runners with M1 CPU --- .github/pymeasure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pymeasure.yml b/.github/pymeasure.yml index df091d75e2..2f1c8af39e 100644 --- a/.github/pymeasure.yml +++ b/.github/pymeasure.yml @@ -9,7 +9,7 @@ dependencies: - pyqt=5.15.7 - pyqtgraph=0.12.4 - pyserial=3.4 - - pyvisa=1.12.0 + - pyvisa=1.13.0 - pyzmq=24.0.1 - qt=5.15.6 # Development dependencies below From 7ed3f495fa17a20da0cc4d5576f5b88d200aa817 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:56:45 +0200 Subject: [PATCH 092/181] Bump codecov action version. --- .github/workflows/pymeasure_CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index 7046d2e974..7a8e246b1b 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -76,7 +76,7 @@ jobs: echo "::add-matcher::.github/pytest.json" xvfb-run -a pytest --cov=pymeasure --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./coverage/reports/ From 95c723103fbf6651a75f78fce5cea1b68a342458 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:19:59 +0200 Subject: [PATCH 093/181] Test different configuration. --- .github/workflows/pymeasure_CI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index 7a8e246b1b..021b120913 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -74,14 +74,14 @@ jobs: # xvfb for graphical interface run: | echo "::add-matcher::.github/pytest.json" - xvfb-run -a pytest --cov=pymeasure --cov-report=xml + xvfb-run -a pytest --cov=pymeasure - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage/reports/ + # directory: ./coverage/reports/ fail_ci_if_error: true - files: ./coverage.xml,!./cache + # files: ./coverage.xml,!./cache flags: unittests name: codecov-umbrella verbose: true From 874b97327a4bec831c852946597c236f064439b8 Mon Sep 17 00:00:00 2001 From: Benedikt Moneke <67148916+bmoneke@users.noreply.github.com> Date: Tue, 30 Apr 2024 02:06:47 +0200 Subject: [PATCH 094/181] Fix coverage report --- .github/workflows/pymeasure_CI.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index 021b120913..15a7294bb6 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -74,14 +74,13 @@ jobs: # xvfb for graphical interface run: | echo "::add-matcher::.github/pytest.json" - xvfb-run -a pytest --cov=pymeasure + xvfb-run -a pytest --cov=pymeasure --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - # directory: ./coverage/reports/ fail_ci_if_error: true - # files: ./coverage.xml,!./cache + files: ./coverage.xml,!./cache flags: unittests name: codecov-umbrella verbose: true From 959470c47e1e4d3561ed12d842e62dd9a3d7ec3d Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:56:24 +0200 Subject: [PATCH 095/181] Small change --- pymeasure/instruments/hp/hp8116a.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py index f31df1227f..98654653e4 100644 --- a/pymeasure/instruments/hp/hp8116a.py +++ b/pymeasure/instruments/hp/hp8116a.py @@ -533,7 +533,7 @@ def reset(self): def shutdown(self): """ Gracefully close the connection to the 8116A. """ self.adapter.connection.clear() - self.adapter.connection.close() + self.adapter.close() super().shutdown() def check_errors(self): From 364efa1c7f73ae562d696e30fef0fff82245081a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:08:13 +0200 Subject: [PATCH 096/181] Move static methods outside the main class. --- pymeasure/instruments/hp/hp8116a.py | 48 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py index 98654653e4..8604cbd672 100644 --- a/pymeasure/instruments/hp/hp8116a.py +++ b/pymeasure/instruments/hp/hp8116a.py @@ -35,6 +35,30 @@ log.addHandler(logging.NullHandler()) +def _generate_1_2_5_sequence(min, max): + """ Generate a list of a 1-2-5 sequence between min and max. """ + exp_min = int(np.log10(min)) + exp_max = int(np.log10(max)) + + seq_1_2_5 = np.array([1, 2, 5]) + sequence = np.array([seq_1_2_5 * (10 ** exp) for exp in range(exp_min - 1, exp_max + 1)]) + sequence = sequence.flatten() + sequence = sequence[(sequence >= min) & (sequence <= max)] + + return list(sequence) + + +def _boolean_control(identifier, state_index, docs, inverted=False, **kwargs): + return Instrument.control( + 'CST', identifier + '%d', docs, + validator=strict_discrete_set, + values=[True, False], + get_process=lambda x: inverted ^ bool(int(x[state_index][1])), + set_process=lambda x: int(inverted ^ x), + **kwargs + ) + + class Status(IntFlag): """ IntFlag type for the GPIB status byte which is returned by the :py:attr:`status` property. When the timing_error or programming_error flag is set, a more detailed error description @@ -197,30 +221,6 @@ def _parse_value_with_unit(value_str, units): return value - @staticmethod - def _generate_1_2_5_sequence(min, max): - """ Generate a list of a 1-2-5 sequence between min and max. """ - exp_min = int(np.log10(min)) - exp_max = int(np.log10(max)) - - seq_1_2_5 = np.array([1, 2, 5]) - sequence = np.array([seq_1_2_5 * (10 ** exp) for exp in range(exp_min - 1, exp_max + 1)]) - sequence = sequence.flatten() - sequence = sequence[(sequence >= min) & (sequence <= max)] - - return list(sequence) - - @staticmethod - def _boolean_control(identifier, state_index, docs, inverted=False, **kwargs): - return Instrument.control( - 'CST', identifier + '%d', docs, - validator=strict_discrete_set, - values=[True, False], - get_process=lambda x: inverted ^ bool(int(x[state_index][1])), - set_process=lambda x: int(inverted ^ x), - **kwargs - ) - # Instrument communication # def write(self, command): From f1c643c64d66d640381241746c1ecce8e3a0267a Mon Sep 17 00:00:00 2001 From: mcdo0486 Date: Tue, 30 Apr 2024 09:19:23 -0500 Subject: [PATCH 097/181] Add step to RELEASE.md for updating CITATION.cff --- RELEASE.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index eb94b194d2..f107572dd1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,19 +11,21 @@ * Adapt the format and structure to the previous release message: * Divide the entries into categories and try to begin entries with "New", "Add", "Fix" or "Remove" as appropriate. (This could also be automated by the above generator with some labeling effort on our part) * We also remove the PR URLs as they clutter the log and condense the new contributors list. -4. Push the changes up as a PR -5. Verify that the builds complete -6. Merge the PR -7. Fetch `master`, build and check the source packages +4. Update the version number in CITATION.cff + * On the line starting with `version: `, replace the current version number with the new version number +5. Push the changes up as a PR +6. Verify that the builds complete +7. Merge the PR +8. Fetch `master`, build and check the source packages - `python -m pip install --upgrade build twine` - `python -m build` - Check the distributions (`twine check dist/*`, version will not yet be correct) -8. Create a git tag in the format "vX.Y.Z" -9. Build final packages and confirm the correct version number is being used - - `python -m build` - - Check the distributions (`twine check dist/*`) -10. Push the Git tag -11. Create a tagged [release on GitHub](https://github.com/pymeasure/pymeasure/releases). You'll have to paste in the changelog entry and probably edit it a bit as that form expects Markdown, not ReST (probably just removing `:code:` tags will be sufficient). +9. Create a git tag in the format "vX.Y.Z" +10. Build final packages and confirm the correct version number is being used + - `python -m build` + - Check the distributions (`twine check dist/*`) +11. Push the Git tag +12. Create a tagged [release on GitHub](https://github.com/pymeasure/pymeasure/releases). You'll have to paste in the changelog entry and probably edit it a bit as that form expects Markdown, not ReST (probably just removing `:code:` tags will be sufficient). ## PyPI release From 040e0a65c470287e770a1d02c9e877ddfebb00e2 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Sat, 13 Jan 2024 18:24:49 +0100 Subject: [PATCH 098/181] replace NaN with nan --- pymeasure/display/widgets/table_widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymeasure/display/widgets/table_widget.py b/pymeasure/display/widgets/table_widget.py index 1e46263228..65a0b90496 100644 --- a/pymeasure/display/widgets/table_widget.py +++ b/pymeasure/display/widgets/table_widget.py @@ -23,7 +23,7 @@ # import logging -from numpy import float64, NaN +from numpy import float64, nan from functools import partial import pyqtgraph as pg import pandas as pd @@ -178,7 +178,7 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): # Cast to column type value_render = column_type.type(value) except (IndexError, ValueError, TypeError): - value = NaN + value = nan value_render = "" if isinstance(value_render, float64): # limit maximum number of decimal digits displayed @@ -297,7 +297,7 @@ def export_df(self): df = None else: # Concatenate pandas data frames - df = pd.concat(df_list, axis=self.concat_axis).replace(to_replace=NaN, value="") + df = pd.concat(df_list, axis=self.concat_axis).replace(to_replace=nan, value="") return df def set_index(self, index): From 3816f992b5c15a3f8da7a2cdc2e9e1e9906049fe Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Sat, 13 Jan 2024 18:30:47 +0100 Subject: [PATCH 099/181] Make numpy import a bit more standardized --- pymeasure/display/widgets/table_widget.py | 9 +-- pymeasure/experiment/sequencer.py | 63 ++++++++++--------- .../instruments/deltaelektronika/sm7045d.py | 5 +- pymeasure/instruments/fwbell/fwbell5080.py | 5 +- pymeasure/instruments/hp/hp856Xx.py | 27 ++++---- .../instruments/oxfordinstruments/itc503.py | 35 ++++++----- 6 files changed, 75 insertions(+), 69 deletions(-) diff --git a/pymeasure/display/widgets/table_widget.py b/pymeasure/display/widgets/table_widget.py index 65a0b90496..25f1c07436 100644 --- a/pymeasure/display/widgets/table_widget.py +++ b/pymeasure/display/widgets/table_widget.py @@ -23,8 +23,9 @@ # import logging -from numpy import float64, nan from functools import partial + +import numpy as np import pyqtgraph as pg import pandas as pd @@ -178,9 +179,9 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): # Cast to column type value_render = column_type.type(value) except (IndexError, ValueError, TypeError): - value = nan + value = np.nan value_render = "" - if isinstance(value_render, float64): + if isinstance(value_render, np.float64): # limit maximum number of decimal digits displayed value_render = f"{value_render:.{self.float_digits:d}g}" @@ -297,7 +298,7 @@ def export_df(self): df = None else: # Concatenate pandas data frames - df = pd.concat(df_list, axis=self.concat_axis).replace(to_replace=nan, value="") + df = pd.concat(df_list, axis=self.concat_axis).replace(to_replace=np.nan, value="") return df def set_index(self, index): diff --git a/pymeasure/experiment/sequencer.py b/pymeasure/experiment/sequencer.py index 9874c07714..019063c8ed 100644 --- a/pymeasure/experiment/sequencer.py +++ b/pymeasure/experiment/sequencer.py @@ -24,9 +24,10 @@ import logging import re -import numpy from itertools import product +import numpy as np + log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -101,35 +102,35 @@ class SequenceHandler: 'range': range, 'sorted': sorted, 'list': list, - 'arange': numpy.arange, - 'linspace': numpy.linspace, - 'arccos': numpy.arccos, - 'arcsin': numpy.arcsin, - 'arctan': numpy.arctan, - 'arctan2': numpy.arctan2, - 'ceil': numpy.ceil, - 'cos': numpy.cos, - 'cosh': numpy.cosh, - 'degrees': numpy.degrees, - 'e': numpy.e, - 'exp': numpy.exp, - 'fabs': numpy.fabs, - 'floor': numpy.floor, - 'fmod': numpy.fmod, - 'frexp': numpy.frexp, - 'hypot': numpy.hypot, - 'ldexp': numpy.ldexp, - 'log': numpy.log, - 'log10': numpy.log10, - 'modf': numpy.modf, - 'pi': numpy.pi, - 'power': numpy.power, - 'radians': numpy.radians, - 'sin': numpy.sin, - 'sinh': numpy.sinh, - 'sqrt': numpy.sqrt, - 'tan': numpy.tan, - 'tanh': numpy.tanh, + 'arange': np.arange, + 'linspace': np.linspace, + 'arccos': np.arccos, + 'arcsin': np.arcsin, + 'arctan': np.arctan, + 'arctan2': np.arctan2, + 'ceil': np.ceil, + 'cos': np.cos, + 'cosh': np.cosh, + 'degrees': np.degrees, + 'e': np.e, + 'exp': np.exp, + 'fabs': np.fabs, + 'floor': np.floor, + 'fmod': np.fmod, + 'frexp': np.frexp, + 'hypot': np.hypot, + 'ldexp': np.ldexp, + 'log': np.log, + 'log10': np.log10, + 'modf': np.modf, + 'pi': np.pi, + 'power': np.power, + 'radians': np.radians, + 'sin': np.sin, + 'sinh': np.sinh, + 'sqrt': np.sqrt, + 'tan': np.tan, + 'tanh': np.tanh, } def __init__(self, valid_inputs=(), file_obj=None): @@ -185,7 +186,7 @@ def eval_string(string, name=None, depth=None, log_enabled=True): "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationError("No sequence entered") - evaluated_string = numpy.array(evaluated_string) + evaluated_string = np.array(evaluated_string) return evaluated_string def _get_idx(self, seq_item): diff --git a/pymeasure/instruments/deltaelektronika/sm7045d.py b/pymeasure/instruments/deltaelektronika/sm7045d.py index 55b501b819..270e54248a 100644 --- a/pymeasure/instruments/deltaelektronika/sm7045d.py +++ b/pymeasure/instruments/deltaelektronika/sm7045d.py @@ -26,7 +26,8 @@ from pymeasure.instruments.validators import strict_range from time import sleep -from numpy import linspace + +import numpy as np class SM7045D(SCPIUnknownMixin, Instrument): @@ -124,7 +125,7 @@ def ramp_to_current(self, target_current, current_step=0.1): curr = self.current n = round(abs(curr - target_current) / current_step) + 1 - for i in linspace(curr, target_current, n): + for i in np.linspace(curr, target_current, n): self.current = i sleep(0.1) diff --git a/pymeasure/instruments/fwbell/fwbell5080.py b/pymeasure/instruments/fwbell/fwbell5080.py index 81d4763af8..87f7c9fa5e 100644 --- a/pymeasure/instruments/fwbell/fwbell5080.py +++ b/pymeasure/instruments/fwbell/fwbell5080.py @@ -24,9 +24,10 @@ from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set -from numpy import array, float64 from time import sleep +import numpy as np + class FWBell5080(SCPIMixin, Instrument): """ Represents the F.W. Bell 5080 Handheld Gaussmeter and @@ -127,7 +128,7 @@ def fields(self, samples=1): raise Exception("F.W. Bell 5080 does not support samples less than 1.") else: data = [self.field for i in range(int(samples))] - return array(data, dtype=float64) + return np.array(data, dtype=np.float64) def auto_range(self): """ Enables the auto range functionality. """ diff --git a/pymeasure/instruments/hp/hp856Xx.py b/pymeasure/instruments/hp/hp856Xx.py index 2292a35fbd..72d2ea19d2 100644 --- a/pymeasure/instruments/hp/hp856Xx.py +++ b/pymeasure/instruments/hp/hp856Xx.py @@ -25,9 +25,10 @@ import logging from math import log10 from enum import Enum, IntFlag -from numpy import arange from datetime import datetime +import numpy as np + from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, truncated_discrete_set, \ joined_validators, strict_range @@ -725,7 +726,7 @@ def set_crt_adjustment_pattern(self): """, validator=joined_validators(strict_discrete_set, truncated_discrete_set), - values=[["AUTO", "MAN"], arange(10, 80, 10)], + values=[["AUTO", "MAN"], np.arange(10, 80, 10)], cast=int, ) @@ -2035,7 +2036,7 @@ def get_power_bandwidth(self, trace, percent): pbw = instr.power_bandwidth(Trace.A, 99.0) print("The power bandwidth at 99 percent is %f kHz" % (pbw / 1e3)) """ - ran = arange(0, 100, 0.1) + ran = np.arange(0, 100, 0.1) if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) @@ -2064,7 +2065,7 @@ def get_power_bandwidth(self, trace, percent): Type: :code:`str, dec` """, validator=joined_validators(strict_discrete_set, truncated_discrete_set), - values=[["AUTO", "MAN"], arange(10, 2e6)], + values=[["AUTO", "MAN"], np.arange(10, 2e6)], set_process=lambda v: v if isinstance(v, str) else f"{int(v)} Hz", get_process=lambda v: v if isinstance(v, str) else int(v) ) @@ -2078,7 +2079,7 @@ def get_power_bandwidth(self, trace, percent): parameters adjust the ratio in a 1, 2, 5 sequence. The default ratio is 0.011. """, validator=strict_range, - values=arange(0.002, 0.10, 0.001) + values=np.arange(0.002, 0.10, 0.001) ) def recall_open_short_average(self): @@ -2450,7 +2451,7 @@ def request_service(self, input): cannot be adjusted. """, validator=joined_validators(strict_discrete_set, strict_range), - values=[["AUTO", "MAN"], arange(50E-6, 100)], + values=[["AUTO", "MAN"], np.arange(50E-6, 100)], set_process=lambda v: v if isinstance(v, str) else ("%.3f S" % v) ) @@ -2563,7 +2564,7 @@ def store_thru(self): trace data, the data below the threshold will be permanently lost. """, validator=strict_discrete_set, - values=arange(-200, 30), + values=np.arange(-200, 30), ) threshold_enabled = Instrument.setting( @@ -2745,7 +2746,7 @@ def create_fft_trace_window(self, trace, window_mode): Type: :code:`str, int` """, validator=strict_range, - values=arange(1, 999), + values=np.arange(1, 999), cast=int ) @@ -2779,7 +2780,7 @@ def create_fft_trace_window(self, trace, window_mode): Type: :code:`int` """, validator=joined_validators(strict_discrete_set, strict_range), - values=[["AUTO", "MAN"], arange(1, 3e6)], + values=[["AUTO", "MAN"], np.arange(1, 3e6)], cast=int, set_process=lambda v: v if isinstance(v, str) else f"{v} Hz" ) @@ -2794,7 +2795,7 @@ def create_fft_trace_window(self, trace, window_mode): new ratio—the resolution bandwidth does not change value. """, validator=strict_range, - values=arange(0.002, 0.10, 0.001) + values=np.arange(0.002, 0.10, 0.001) ) def view_trace(self, trace): @@ -2962,7 +2963,7 @@ def __init__(self, adapter, name="Hewlett-Packard HP8560A", **kwargs): Only available with an HP 8560A Option 002. """, validator=strict_range, - values=arange(0.1, 12.75, 0.05) + values=np.arange(0.1, 12.75, 0.05) ) source_power_sweep = Instrument.control( @@ -2979,7 +2980,7 @@ def __init__(self, adapter, name="Hewlett-Packard HP8560A", **kwargs): Only available with an HP 8560A Option 002. """, validator=truncated_discrete_set, - values=arange(0.1, 12.75, 0.05), + values=np.arange(0.1, 12.75, 0.05), ) source_power_sweep_enabled = Instrument.setting( @@ -3003,7 +3004,7 @@ def __init__(self, adapter, name="Hewlett-Packard HP8560A", **kwargs): Only available with an HP 8560A Option 002. """, validator=joined_validators(strict_discrete_set, truncated_discrete_set), - values=[["OFF", "ON"], arange(-10, 2.8, 0.05)], + values=[["OFF", "ON"], np.arange(-10, 2.8, 0.05)], set_process=lambda v: v if isinstance(v, str) else ("%.2f {amplitude_unit}" % v) ) diff --git a/pymeasure/instruments/oxfordinstruments/itc503.py b/pymeasure/instruments/oxfordinstruments/itc503.py index baef1687d0..39e0a85e03 100644 --- a/pymeasure/instruments/oxfordinstruments/itc503.py +++ b/pymeasure/instruments/oxfordinstruments/itc503.py @@ -25,9 +25,10 @@ import logging from time import sleep, time -import numpy from enum import IntFlag +import numpy as np + from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, \ truncated_range, strict_range @@ -514,34 +515,34 @@ def program_sweep(self, temperatures, sweep_time, hold_time, steps=None): self.sweep_status = 0 # Convert input np.ndarrays - temperatures = numpy.array(temperatures, ndmin=1) - sweep_time = numpy.array(sweep_time, ndmin=1) - hold_time = numpy.array(hold_time, ndmin=1) + temperatures = np.array(temperatures, ndmin=1) + sweep_time = np.array(sweep_time, ndmin=1) + hold_time = np.array(hold_time, ndmin=1) # Make steps array if steps is None: steps = temperatures.size - steps = numpy.linspace(1, steps, steps) + steps = np.linspace(1, steps, steps) # Create interpolated arrays - interpolator = numpy.round( - numpy.linspace(1, steps.size, temperatures.size)) - temperatures = numpy.interp(steps, interpolator, temperatures) + interpolator = np.round( + np.linspace(1, steps.size, temperatures.size)) + temperatures = np.interp(steps, interpolator, temperatures) - interpolator = numpy.round( - numpy.linspace(1, steps.size, sweep_time.size)) - sweep_time = numpy.interp(steps, interpolator, sweep_time) + interpolator = np.round( + np.linspace(1, steps.size, sweep_time.size)) + sweep_time = np.interp(steps, interpolator, sweep_time) - interpolator = numpy.round( - numpy.linspace(1, steps.size, hold_time.size)) - hold_time = numpy.interp(steps, interpolator, hold_time) + interpolator = np.round( + np.linspace(1, steps.size, hold_time.size)) + hold_time = np.interp(steps, interpolator, hold_time) # Pad with zeros to wipe unused steps (total 16) of the sweep program padding = 16 - temperatures.size - temperatures = numpy.pad(temperatures, (0, padding), 'constant', + temperatures = np.pad(temperatures, (0, padding), 'constant', constant_values=temperatures[-1]) - sweep_time = numpy.pad(sweep_time, (0, padding), 'constant') - hold_time = numpy.pad(hold_time, (0, padding), 'constant') + sweep_time = np.pad(sweep_time, (0, padding), 'constant') + hold_time = np.pad(hold_time, (0, padding), 'constant') # Setting the arrays to the controller for line, (setpoint, sweep, hold) in \ From 99562495746e69b226e6fcfefb8f99b0951db813 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Sat, 13 Jan 2024 18:47:33 +0100 Subject: [PATCH 100/181] Linting --- pymeasure/instruments/oxfordinstruments/itc503.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pymeasure/instruments/oxfordinstruments/itc503.py b/pymeasure/instruments/oxfordinstruments/itc503.py index 39e0a85e03..b0274616cd 100644 --- a/pymeasure/instruments/oxfordinstruments/itc503.py +++ b/pymeasure/instruments/oxfordinstruments/itc503.py @@ -35,7 +35,6 @@ from .base import OxfordInstrumentsBase - # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -540,7 +539,7 @@ def program_sweep(self, temperatures, sweep_time, hold_time, steps=None): # Pad with zeros to wipe unused steps (total 16) of the sweep program padding = 16 - temperatures.size temperatures = np.pad(temperatures, (0, padding), 'constant', - constant_values=temperatures[-1]) + constant_values=temperatures[-1]) sweep_time = np.pad(sweep_time, (0, padding), 'constant') hold_time = np.pad(hold_time, (0, padding), 'constant') From 82a4fac1bf8b04b8052e48271c57cd3a5b34395f Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 15 Apr 2024 23:19:52 +0200 Subject: [PATCH 101/181] Limit numpy version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3b135f1f2f..83851f0666 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ classifiers = packages = find: python_requires = >=3.8 install_requires = - numpy >= 1.6.1 + numpy >= 1.6.1, < 3 pandas >= 0.14 pint pyvisa >= 1.8 From 80e650b46542a1d8468c984da52bbded84edccd5 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 22 Apr 2024 22:41:09 +0200 Subject: [PATCH 102/181] Update pandas minimum version number --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 83851f0666..8e749ff3ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ packages = find: python_requires = >=3.8 install_requires = numpy >= 1.6.1, < 3 - pandas >= 0.14 + pandas >= 0.25.3, < 3 pint pyvisa >= 1.8 pyserial >= 2.7 From b07961eed0c0ad7fade185f12d44c1366b5b61c1 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 22 Apr 2024 22:50:50 +0200 Subject: [PATCH 103/181] Update changes.rst --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5ab48d922..825f662068 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,7 @@ Upcoming version ================ +Main items of this new release: +- Added support for numpy 2.0; limited the allowed numpy version to <3. Added features - SCPI instruments have :code:`next_error` property giving the next error. @@ -25,6 +27,7 @@ Dropped Support --------------- - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. + Version 0.13.1 (2023-10-05) =========================== New release to fix ineffective python version restriction in the project metadata (only affected Python<=3.7 environments installing via pip). From ad3945f46e096e15e358c40593a2c1e8cc29a76b Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 29 Apr 2024 21:42:58 +0200 Subject: [PATCH 104/181] Add test for numpy2 --- .github/pymeasure_np2.yml | 28 ++++++++++++++++++++++++++++ .github/workflows/pymeasure_CI.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/pymeasure_np2.yml diff --git a/.github/pymeasure_np2.yml b/.github/pymeasure_np2.yml new file mode 100644 index 0000000000..8c55bdc2b7 --- /dev/null +++ b/.github/pymeasure_np2.yml @@ -0,0 +1,28 @@ +name: pymeasure_np2 +channels: + - conda-forge +dependencies: + - cloudpickle=1.6.0 + - pandas>=2.2.2 +# - pint=0.18 a pint version that has not yet been released (>0.23) is required to work with numpy2 + - pyqt=5.15.7 + - pyqtgraph=0.12.4 + - pyserial=3.4 + - pyvisa=1.12.0 + - pyzmq=24.0.1 + - qt=5.15.6 +# Development dependencies below + - pytest-qt=4.2.0 + - pytest-runner=5.2 + - pytest=7.2.0 + - pytest-cov=4.1.0 + - pyvisa-sim==0.5.1 + - flake8=6.0.0 + - setuptools_scm # don't pin, to get newest features + - sphinx=5.3.0 + - sphinx_rtd_theme=1.2.2 +# pip is currently not needed, but the recommended tool when packages that are unavailable on conda are required + - pip # don't pin, to gain newest conda compatibility fixes + - pip: + - numpy==2.0.0rc1 + - -e git+https://github.com/hgrecco/pint.git#egg=pint diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index 15a7294bb6..645d4b4b44 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -124,3 +124,33 @@ jobs: run: | echo "::add-matcher::.github/pytest.json" pytest + + + test_numpy2: + name: Python ${{ matrix.python-version }} with numpy>=2 + runs-on: "ubuntu-latest" + defaults: + run: + shell: bash -l {0} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install pymeasure requirements + uses: mamba-org/setup-micromamba@v1 + with: + environment-file: .github/pymeasure_np2.yml + create-args: python=3.11 + cache-environment-key: py3.11-ubuntu-latest-mamba-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} + cache-downloads: false + - name: Python version + run: python --version + - name: Install Pymeasure + # If the pytest problem matcher stops working because of bad paths, do an editable install + run: pip install .[tests] + - name: Pymeasure version + run: python -c "import pymeasure;print(pymeasure.__version__)" + - name: Run pytest with xvfb + run: | + echo "::add-matcher::.github/pytest.json" + xvfb-run -a pytest From 625447447c445a68682b3222fe85926f5866fe1e Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 29 Apr 2024 21:46:48 +0200 Subject: [PATCH 105/181] revert pandas minimum version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 8e749ff3ea..10441d0499 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ packages = find: python_requires = >=3.8 install_requires = numpy >= 1.6.1, < 3 - pandas >= 0.25.3, < 3 + pandas >= 0.14, < 3 pint pyvisa >= 1.8 pyserial >= 2.7 From d7034fb01733fb201097f1f59c05cd8381058b7f Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 29 Apr 2024 21:59:37 +0200 Subject: [PATCH 106/181] Add remark on installation instructions --- docs/quick_start.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/quick_start.rst b/docs/quick_start.rst index c1f0886483..ce85cbadad 100644 --- a/docs/quick_start.rst +++ b/docs/quick_start.rst @@ -49,6 +49,7 @@ However, this needs a VISA implementation installed to handle device communicati If you do not already know what this means, install the pure-Python :code:`pyvisa-py` package (using the same installation you used above). If you want to know more, consult `the PyVISA documentation `__. + Checking the version -------------------- @@ -61,3 +62,9 @@ Execute the following Python code. pymeasure.__version__ You should see the version of PyMeasure printed out. At this point you have PyMeasure installed, and you are ready to start using it! Are you ready to :doc:`connect to an instrument <./tutorial/connecting>`? + +.. warning:: + The release of numpy version 2 caused some compatibility issues. + PyMeasure is compatible with version 2 as of PyMeasure version 0.14. + To use PyMeasure with numpy>=2, you also ensure the dependencies are compatible. + This means installing pandas>=2.2.2 and pint>0.23 (at the time of writing, pint needs to be installed directly from the github repository, as no numpy-2 compatible version has been released). From 507ba10f8dc786d02e6654e4ac66834d3f2a3cf2 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 29 Apr 2024 22:39:20 +0200 Subject: [PATCH 107/181] Fix the requirements yml file for pandas --- .github/pymeasure_np2.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/pymeasure_np2.yml b/.github/pymeasure_np2.yml index 8c55bdc2b7..d1c8ce13b6 100644 --- a/.github/pymeasure_np2.yml +++ b/.github/pymeasure_np2.yml @@ -3,8 +3,8 @@ channels: - conda-forge dependencies: - cloudpickle=1.6.0 - - pandas>=2.2.2 -# - pint=0.18 a pint version that has not yet been released (>0.23) is required to work with numpy2 +# - pandas>=2.2.2 # The version on conda-forge seems not be built with numpy==2, but the pip version has been +# - pint=0.18 # A pint version that has not yet been released (>0.23) is required to work with numpy2 - pyqt=5.15.7 - pyqtgraph=0.12.4 - pyserial=3.4 @@ -25,4 +25,5 @@ dependencies: - pip # don't pin, to gain newest conda compatibility fixes - pip: - numpy==2.0.0rc1 + - pandas==2.2.2 - -e git+https://github.com/hgrecco/pint.git#egg=pint From a82cbf48b2447b47e844ad3166dae7bed741dca0 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 30 Apr 2024 21:46:08 +0200 Subject: [PATCH 108/181] Adjust requirements yml file --- .github/{pymeasure_np2.yml => pymeasure_numpy2.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/{pymeasure_np2.yml => pymeasure_numpy2.yml} (91%) diff --git a/.github/pymeasure_np2.yml b/.github/pymeasure_numpy2.yml similarity index 91% rename from .github/pymeasure_np2.yml rename to .github/pymeasure_numpy2.yml index d1c8ce13b6..8a73797f5c 100644 --- a/.github/pymeasure_np2.yml +++ b/.github/pymeasure_numpy2.yml @@ -1,4 +1,4 @@ -name: pymeasure_np2 +name: pymeasure_numpy2 channels: - conda-forge dependencies: @@ -26,4 +26,4 @@ dependencies: - pip: - numpy==2.0.0rc1 - pandas==2.2.2 - - -e git+https://github.com/hgrecco/pint.git#egg=pint + - git+https://github.com/hgrecco/pint.git#egg=pint From d66524d87b9381c1cb69ddd03b4e808b9c61aace Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 30 Apr 2024 21:46:57 +0200 Subject: [PATCH 109/181] REmove warning --- docs/quick_start.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/quick_start.rst b/docs/quick_start.rst index ce85cbadad..af2e20f270 100644 --- a/docs/quick_start.rst +++ b/docs/quick_start.rst @@ -62,9 +62,3 @@ Execute the following Python code. pymeasure.__version__ You should see the version of PyMeasure printed out. At this point you have PyMeasure installed, and you are ready to start using it! Are you ready to :doc:`connect to an instrument <./tutorial/connecting>`? - -.. warning:: - The release of numpy version 2 caused some compatibility issues. - PyMeasure is compatible with version 2 as of PyMeasure version 0.14. - To use PyMeasure with numpy>=2, you also ensure the dependencies are compatible. - This means installing pandas>=2.2.2 and pint>0.23 (at the time of writing, pint needs to be installed directly from the github repository, as no numpy-2 compatible version has been released). From d064d490da4785f745d010678bd3b9de0dee5249 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 30 Apr 2024 21:49:00 +0200 Subject: [PATCH 110/181] Adjusted changes.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 825f662068..8a94c6d184 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ Upcoming version ================ Main items of this new release: -- Added support for numpy 2.0; limited the allowed numpy version to <3. +- Added support for numpy 2.0. Added features - SCPI instruments have :code:`next_error` property giving the next error. From bde2caa2408344d123904768fd7efd78b21c7f02 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 30 Apr 2024 22:19:29 +0200 Subject: [PATCH 111/181] Fix workflow file --- .github/workflows/pymeasure_CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index 645d4b4b44..fbd246b813 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -127,7 +127,7 @@ jobs: test_numpy2: - name: Python ${{ matrix.python-version }} with numpy>=2 + name: Python 3.11, ubuntu-latest, numpy>=2 runs-on: "ubuntu-latest" defaults: run: @@ -139,7 +139,7 @@ jobs: - name: Install pymeasure requirements uses: mamba-org/setup-micromamba@v1 with: - environment-file: .github/pymeasure_np2.yml + environment-file: .github/pymeasure_numpy2.yml create-args: python=3.11 cache-environment-key: py3.11-ubuntu-latest-mamba-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} cache-downloads: false From 9750b6d285c77e41cbba3c14436b6402e1b80647 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 30 Apr 2024 23:35:01 +0200 Subject: [PATCH 112/181] Upped minimum version for pytest and pyvisa. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 10441d0499..9ea26832a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = numpy >= 1.6.1, < 3 pandas >= 0.14, < 3 pint - pyvisa >= 1.8 + pyvisa >= 1.9 pyserial >= 2.7 pyqtgraph >= 0.12 importlib-metadata; python_version<"3.8" @@ -42,7 +42,7 @@ tcp = cloudpickle>=0.3.1 python-vxi11 = python-vxi11>=0.9 tests = - pytest >= 2.9.1 + pytest >= 3.3.0 pytest-cov >= 4.1.0 pytest-qt >= 2.4.0 # install pyqt or pyside manually as desired pyvisa-sim >= 0.4.0 From 08d49826aa37ce22461d55e6e3a9f055e01eccc7 Mon Sep 17 00:00:00 2001 From: Dominik Kriegner Date: Fri, 3 May 2024 08:15:16 +0200 Subject: [PATCH 113/181] fix Stanford Research SR830 output conversion (#1069) * fix Stanford Research SR830 output conversion * remove expand from output conversion By testing with hardware I found that the expand value should not be included in the equation inside the output_conversion function. It seems the logic in the manual applies only to the analog output. The return value of the "OUTP?1" command is not effected by the expand factor! * add protocol tests for selected getters and the output_conversion * fix linter errors --- pymeasure/instruments/srs/sr830.py | 4 +- tests/instruments/srs/test_sr830.py | 95 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 tests/instruments/srs/test_sr830.py diff --git a/pymeasure/instruments/srs/sr830.py b/pymeasure/instruments/srs/sr830.py index 7ae7144e67..8ea56f9918 100644 --- a/pymeasure/instruments/srs/sr830.py +++ b/pymeasure/instruments/srs/sr830.py @@ -436,9 +436,9 @@ def output_conversion(self, channel): """ Returns a function that can be used to determine the signal from the channel output (X, Y, or R) """ - offset, expand = self.get_scaling(channel) + offset, _ = self.get_scaling(channel) sensitivity = self.sensitivity - return lambda x: (x / (10. * expand) + offset) * sensitivity + return lambda x: x + offset / 100 * sensitivity @property def sample_frequency(self): diff --git a/tests/instruments/srs/test_sr830.py b/tests/instruments/srs/test_sr830.py new file mode 100644 index 0000000000..4c5ec9d771 --- /dev/null +++ b/tests/instruments/srs/test_sr830.py @@ -0,0 +1,95 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import pytest + +from pymeasure.test import expected_protocol +from pymeasure.instruments.srs.sr830 import SR830 + + +def test_id(): + """Verify the communication of the device type.""" + with expected_protocol( + SR830, + [("*IDN?", "Stanford_Research_Systems,SR830,s/n12345,ver1.07"),], + ) as inst: + assert inst.id == "Stanford_Research_Systems,SR830,s/n12345,ver1.07" + + +@pytest.mark.parametrize("number, value", ( + ("0", 2e-9), + ("14", 100e-6), + ("25", 0.5), +)) +def test_sensitivity(number, value): + """Verify the communication of the sensitivity getter.""" + with expected_protocol( + SR830, + [("SENS?", number),], + ) as inst: + assert inst.sensitivity == pytest.approx(value) + + +def test_frequency(): + """Verify the communication of the frequency getter.""" + with expected_protocol( + SR830, + [("FREQ?", "121.98"),], + ) as inst: + assert inst.frequency == pytest.approx(121.98) + + +def test_snap(): + """Verify the communication of the measurement values.""" + with expected_protocol( + SR830, + [("SNAP? 1,2", "-4.17234e-007,-5.9605e-007"),], + ) as inst: + xy = inst.xy + assert len(xy) == 2 + assert xy[0] == pytest.approx(-4.17234e-007) + assert xy[1] == pytest.approx(-5.9605e-007) + + +def test_get_scaling(): + """Verify the communication of the X channel scaling settings.""" + with expected_protocol( + SR830, + [("OEXP? 1", "9.7,1"),], + ) as inst: + offset, expand = inst.get_scaling("X") + assert offset == pytest.approx(9.7) + assert expand == pytest.approx(10) + + +def test_output_conversion(): + """Verify the communication of the X channel value with conversion.""" + with expected_protocol( + SR830, + [("OEXP? 1", "10,1"), + ("SENS?", "19"), + ("OUTP?1", "-0.000500266"), + ], + ) as inst: + conv = inst.output_conversion("X") + assert conv(inst.x) == pytest.approx(-2.66e-7) From d63d22522b9db308b238e4d595fec0d342415278 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 6 May 2024 09:18:47 +0200 Subject: [PATCH 114/181] Refactor tests and add comment. --- pymeasure/instruments/hp/hp8116a.py | 1 + tests/instruments/hp/test_hp8116a.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py index 8604cbd672..d71e6f4082 100644 --- a/pymeasure/instruments/hp/hp8116a.py +++ b/pymeasure/instruments/hp/hp8116a.py @@ -35,6 +35,7 @@ log.addHandler(logging.NullHandler()) +# for Python>3.9, these two methods can be static methods in the class def _generate_1_2_5_sequence(min, max): """ Generate a list of a 1-2-5 sequence between min and max. """ exp_min = int(np.log10(min)) diff --git a/tests/instruments/hp/test_hp8116a.py b/tests/instruments/hp/test_hp8116a.py index 33c0b4b397..3cdf8642b8 100644 --- a/tests/instruments/hp/test_hp8116a.py +++ b/tests/instruments/hp/test_hp8116a.py @@ -37,7 +37,7 @@ def test_init(): with expected_protocol( HP8116A, - [(b"CST", b"x" * 87 + b' ,\r\n')], + init_comm, ): pass # Verify the expected communication. @@ -45,7 +45,7 @@ def test_init(): def test_duty_cycle(): with expected_protocol( HP8116A, - [(b"CST", b"x" * 87 + b' ,\r\n'), (b"IDTY", b"00000035")], + init_comm + [(b"IDTY", b"00000035")], ) as instr: assert instr.duty_cycle == 35 @@ -53,7 +53,7 @@ def test_duty_cycle(): def test_duty_cycle_setter(): with expected_protocol( HP8116A, - [(b"CST", b"x" * 87 + b' ,\r\n'), (b"DTY 34.5 %", None)], + init_comm + [(b"DTY 34.5 %", None)], ) as instr: instr.duty_cycle = 34.5 From 2cf0b8d2245afd65c05b8f97f4b2fd271aeed7f0 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 6 May 2024 09:29:59 +0200 Subject: [PATCH 115/181] Rework docstrings. --- pymeasure/instruments/hp/hp8116a.py | 47 ++++++++++++----------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py index d71e6f4082..b19e3b6682 100644 --- a/pymeasure/instruments/hp/hp8116a.py +++ b/pymeasure/instruments/hp/hp8116a.py @@ -332,7 +332,7 @@ def ask(self, command, num_bytes=None): frequency = Instrument.control( 'IFRQ', 'FRQ %s', - """Control the frequency of the output in Hz. (strict float from 1 mHz to 52.5 MHz). + """Control the frequency of the output in Hz (strict float from 1e-3 to 52.5e6). """, validator=strict_range, values=[1e-3, 52.5001e6], @@ -354,8 +354,7 @@ def ask(self, command, num_bytes=None): pulse_width = Instrument.control( 'IWID', 'WID %s', - """Control the pulse width (float). - The allowed pulse width range is 8 ns to 999 ms. + """Control the pulse width in s (strict float from 8e-9 to 999e-3). The pulse width may not be larger than the period. """, validator=strict_range, @@ -366,9 +365,8 @@ def ask(self, command, num_bytes=None): amplitude = Instrument.control( 'IAMP', 'AMP %s', - """Control the amplitude of the output in V (float). - The allowed amplitude range generally is 10 mV to 16 V, - but it is also limited by the current offset. + """Control the amplitude of the output in V (strict float from 10e-3 to 16). + The allowed amplitude range is also limited by the current offset. """, validator=strict_range, values=[10e-3, 16.001], @@ -378,9 +376,8 @@ def ask(self, command, num_bytes=None): offset = Instrument.control( 'IOFS', 'OFS %s', - """Control the offset of the output in V (float). - The allowed offset range generally is -7.95 V to 7.95 V, - but it is also limited by the amplitude. + """Control the offset of the output in V (strit float from -7.95 to 7.95). + The allowed offset range is also limited by the amplitude. """, validator=strict_range, values=[-7.95, 7.95001], @@ -390,9 +387,8 @@ def ask(self, command, num_bytes=None): high_level = Instrument.control( 'IHIL', 'HIL %s', - """Control the high level of the output in V (float). - The allowed high level range generally is -7.9 V to 8 V, - but it must be at least 10 mV greater than the low level. + """Control the high level of the output in V (strict float from -7.9 to 8). + The allowed high level range must be at least 10 mV greater than the low level. """, validator=strict_range, values=[-7.9, 8.001], @@ -402,9 +398,8 @@ def ask(self, command, num_bytes=None): low_level = Instrument.control( 'ILOL', 'LOL %s', - """Control the low level of the output in V (float). - The allowed low level range generally is -8 V to 7.9 V, - but it must be at least 10 mV less than the high level. + """Control the low level of the output in V (strict float from -8 to 7.9). + The allowed low level range must be at least 10 mV less than the high level. """, validator=strict_range, values=[-8, 7.9001], @@ -414,9 +409,8 @@ def ask(self, command, num_bytes=None): burst_number = Instrument.control( 'IBUR', 'BUR %s #', - """Control the number of periods generated in a burst (int). - The allowed range is 1 to 1999. It is only valid for units with Option 001 - in one of the burst modes. + """Control the number of periods generated in a burst (strict int from 1 to 1999). + It is only valid for units with Option 001 in one of the burst modes. """, validator=strict_range, values=[1, 1999], @@ -425,8 +419,8 @@ def ask(self, command, num_bytes=None): repetition_rate = Instrument.control( 'IRPT', 'RPT %s', - """Control the repetition rate (= the time between bursts) - in 'internal_burst' mode (strict float from 20 ns to 999 ms). + """Control the repetition rate in s (i.e. the time between bursts) + in 'internal_burst' mode (strict float from 20e-9 to 999e-3). """, validator=strict_range, values=[20e-9, 999.001e-3], @@ -436,8 +430,8 @@ def ask(self, command, num_bytes=None): sweep_start = Instrument.control( 'ISTA', 'STA %s', - """Control the start frequency in both sweep modes (float). - The allowed range is 1 mHz to 52.5 MHz. + """Control the start frequency in both sweep modes in Hz + (strict float from 1e-3 to 52.5e6). """, validator=strict_range, values=[1e-3, 52.5001e6], @@ -447,8 +441,7 @@ def ask(self, command, num_bytes=None): sweep_stop = Instrument.control( 'ISTP', 'STP %s', - """Control the stop frequency in both sweep modes (float). - The allowed range is 1 mHz to 52.5 MHz. + """Control the stop frequency in both sweep modes in Hz (strict float from 1e-3 to 52.5e6). """, validator=strict_range, values=[1e-3, 52.5001e6], @@ -458,9 +451,9 @@ def ask(self, command, num_bytes=None): sweep_marker_frequency = Instrument.control( 'IMRK', 'MRK %s', - """Control the frequency marker in both sweep modes (float). + """Control the frequency marker in both sweep modes in Hz + (strict float from 1e-3 to 52.5e6). At this frequency, the marker output switches from low to high. - The allowed range is 1 mHz to 52.5 MHz. """, validator=strict_range, values=[1e-3, 52.5001e6], @@ -470,7 +463,7 @@ def ask(self, command, num_bytes=None): sweep_time = Instrument.control( 'ISWT', 'SWT %s', - """Control the sweep time per decade in both sweep modes (float). + """Control the sweep time per decade in both sweep modes in s (float). The sweep time is selectable in a 1-2-5 sequence between 10 ms and 500 s. """, validator=truncated_discrete_set, From 0e4858460236df02759f7f7fc1e24351704cd93b Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 6 May 2024 09:37:55 +0200 Subject: [PATCH 116/181] Improve anc300 documentation --- pymeasure/instruments/attocube/anc300.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/attocube/anc300.py b/pymeasure/instruments/attocube/anc300.py index 2bdbd210eb..d65466ebcf 100644 --- a/pymeasure/instruments/attocube/anc300.py +++ b/pymeasure/instruments/attocube/anc300.py @@ -246,7 +246,7 @@ class ANC300Controller(Instrument): :param axisnames: a list of axis names which will be used to create properties with these names :param passwd: password for the attocube standard console - :param query_delay: delay between sending and reading (default 0.05 sec) + :param query_delay: default delay between sending and reading in s (default 0.05) :param host: host address of the instrument (e.g. 169.254.0.1) .. deprecated:: 0.11.2 @@ -411,6 +411,7 @@ def read(self): def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. - :param query_delay: Delay between writing and reading in seconds. None is default delay. + :param query_delay: Delay between writing and reading in seconds. + None means :attr:`query_delay`. """ super().wait_for(self.query_delay if query_delay is None else query_delay) From 5e1a5141debdb3c8802542d505283d7a82d263eb Mon Sep 17 00:00:00 2001 From: Nick James Kirkby <20824939+driftregion@users.noreply.github.com> Date: Tue, 7 May 2024 04:57:05 -0700 Subject: [PATCH 117/181] add Agilent4294A (#998) * add Agilent4294A start + stop. Image saving needs review * fix image saving routine: add correct read and write terminations * run flake8 * fix test_property_docstrings * lint * Update pymeasure/instruments/agilent/agilent4294A.py Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> * add title control * address style feedback * update AUTHORS, add doc and test, correct start and stop frequency formatting * update toctree * address feedback on test; add get_data method * Update pymeasure/instruments/agilent/agilent4294A.py Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> * Move MEASUREMENT_TYPES to module-level list. Add spacing between Instrument.control declarations. Remove unnecessary calls to print() * add SCPIMixin class. Deprecate include_scpi=True * Update copyright year in pymeasure/instruments/agilent/agilent4294A.py Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> * Update copyright year in tests/instruments/agilent/test_agilent4294A.py Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> --------- Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> --- AUTHORS.txt | 1 + docs/api/instruments/agilent/agilent4294A.rst | 7 + docs/api/instruments/agilent/index.rst | 1 + pymeasure/instruments/agilent/__init__.py | 1 + pymeasure/instruments/agilent/agilent4294A.py | 181 ++++++++++++++++++ .../instruments/agilent/test_agilent4294A.py | 55 ++++++ 6 files changed, 246 insertions(+) create mode 100644 docs/api/instruments/agilent/agilent4294A.rst create mode 100644 pymeasure/instruments/agilent/agilent4294A.py create mode 100644 tests/instruments/agilent/test_agilent4294A.py diff --git a/AUTHORS.txt b/AUTHORS.txt index 82d8ffc2dd..8181c9360d 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -79,3 +79,4 @@ Jannis Kleine-Schönepauck Douwe den Blanken Till Zürner J. A. Wilcox +Nick James Kirkby diff --git a/docs/api/instruments/agilent/agilent4294A.rst b/docs/api/instruments/agilent/agilent4294A.rst new file mode 100644 index 0000000000..7d2e6a875c --- /dev/null +++ b/docs/api/instruments/agilent/agilent4294A.rst @@ -0,0 +1,7 @@ +########################################## +Agilent 4294A Precision Impedance Analyzer +########################################## + +.. autoclass:: pymeasure.instruments.agilent.Agilent4294A + :members: + :show-inheritance: diff --git a/docs/api/instruments/agilent/index.rst b/docs/api/instruments/agilent/index.rst index 2be7342776..aa5bea0708 100644 --- a/docs/api/instruments/agilent/index.rst +++ b/docs/api/instruments/agilent/index.rst @@ -18,6 +18,7 @@ If the instrument you are looking for is not here, also check :doc:`HP<../hp/ind agilent34410A agilent34450A agilent4156 + agilent4294A agilent33220A agilent33500 agilent33521A diff --git a/pymeasure/instruments/agilent/__init__.py b/pymeasure/instruments/agilent/__init__.py index 64570bf0bd..ea6747f0c6 100644 --- a/pymeasure/instruments/agilent/__init__.py +++ b/pymeasure/instruments/agilent/__init__.py @@ -29,6 +29,7 @@ from .agilent34410A import Agilent34410A from .agilent34450A import Agilent34450A from .agilent4156 import Agilent4156 +from .agilent4294A import Agilent4294A from .agilent33220A import Agilent33220A from .agilent33500 import Agilent33500 from .agilent33521A import Agilent33521A diff --git a/pymeasure/instruments/agilent/agilent4294A.py b/pymeasure/instruments/agilent/agilent4294A.py new file mode 100644 index 0000000000..2f0f396d31 --- /dev/null +++ b/pymeasure/instruments/agilent/agilent4294A.py @@ -0,0 +1,181 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.instruments import Instrument, SCPIMixin +from pymeasure.instruments.validators import strict_range, strict_discrete_set +import pandas as pd +import numpy as np +import os + +# Set of valid arguments for the MEAS? command +MEASUREMENT_TYPES = [ + "IMPH", + "IRIM", + "LSR", + "LSQ", + "CSR", + "CSQ", + "CSD", + "AMPH", + "ARIM", + "LPG", + "LPQ", + "CPG", + "CPQ", + "CPD", + "COMP", + "IMLS", + "IMCS", + "IMLP", + "IMCP", + "IMRS", + "IMQ", + "IMD", + "LPR", + "CPR", +] + + +class Agilent4294A(SCPIMixin, Instrument): + """ Represents the Agilent 4294A Precision Impedance Analyzer """ + + def __init__(self, adapter, name="Agilent 4294A Precision Impedance Analyzer", + read_termination="\n", + write_termination="\n", + timeout=5000, + **kwargs): + + super().__init__( + adapter, + name, + read_termination=read_termination, + write_termination=write_termination, + timeout=timeout, + **kwargs + ) + + start_frequency = Instrument.control( + "STAR?", "STAR %d HZ", "Control the start frequency in Hz", + validator=strict_range, values=[40, 140E6] + ) + + stop_frequency = Instrument.control( + "STOP?", "STOP %d HZ", "Control the stop frequency in Hz", + validator=strict_range, values=[40, 140E6] + ) + + num_points = Instrument.control( + "POIN?", "POIN %d", "Control the number of points measured at each sweep", + validator=strict_discrete_set, values=range(2, 802), + cast=int, + ) + + measurement_type = Instrument.control( + "MEAS?", "MEAS %d", "Control the measurement type. See MEASUREMENT_TYPES", + validator=strict_discrete_set, values=MEASUREMENT_TYPES, + ) + + active_trace = Instrument.control( + "TRAC?", "TRAC %s", "Control the active trace", + validator=strict_discrete_set, values=["A", "B"] + ) + + title = Instrument.control( + "TITL?", 'TITL "%s"', "Control the title of the active trace" + ) + + def save_graphics(self, path=""): + """ Save graphics on the screen to a file on the local computer. + Adapted from: + https://www.keysight.com/se/en/lib/software-detail/programming-examples/4294a-data-transfer-program-excel-vba-1645196.html + """ + + self.write("STOD MEMO") # store to internal memory + self.write("PRIC VARI") # save a color image + + root, ext = os.path.splitext(path) + if ext != ".tiff": + ext = ".tiff" + if not root: + root = "graphics" + + path = root + ext + + REMOTE_FILE = "agt4294a.tiff" # Filename of the in-memory file on the device + self.write(f'SAVDTIF "{REMOTE_FILE}"') + + vErr = self.ask("OUTPERRO?").split(",") + if not int(vErr[0]) == 0: + self.write(f'PURG "{REMOTE_FILE}"') + self.write(f'SAVDTIF "{REMOTE_FILE}"') + vErr = self.ask("OUTPERRO?").split(",") + + self.write(f'ROPEN "{REMOTE_FILE}"') + lngFileSize = int(self.ask(f'FSIZE? "{REMOTE_FILE}"')) + MAX_BUFF_SIZE = 16384 + iBufCnt = lngFileSize // MAX_BUFF_SIZE + if lngFileSize % MAX_BUFF_SIZE > 0: + iBufCnt += 1 + + with open(path, 'wb') as file: + for _ in range(iBufCnt): + data = self.adapter.connection.query_binary_values("READ?", datatype='B', + container=bytes) + file.write(data) + self.write(f'PURG "{REMOTE_FILE}"') + + return path + + def get_data(self, path=None): + """ + Get the measurement data from the instrument after completion. + + :param path: Path for optional data export to CSV. + :returns: Pandas Dataframe + """ + prev_active_trace = self.active_trace + + num_points = self.num_points + freqs = np.array(self.ask("OUTPSWPRM?").split(","), dtype=float) + self.active_trace = "A" + adata = np.array(self.ask("OUTPDTRC?").split(","), dtype=float).reshape(num_points, 2) + + self.active_trace = "B" + bdata = np.array(self.ask("OUTPDTRC?").split(","), dtype=float).reshape(num_points, 2) + + # restore the previous state + self.active_trace = prev_active_trace + + df = pd.DataFrame( + np.hstack((freqs.reshape(-1, 1), adata, bdata)), + columns=["Frequency", "A Real", "A Imag", "B Real", "B Imag"] + ) + + if path is not None: + _, ext = os.path.splitext(path) + if ext != ".csv": + path = path + ".csv" + df.to_csv(path, index=False) + + return df diff --git a/tests/instruments/agilent/test_agilent4294A.py b/tests/instruments/agilent/test_agilent4294A.py new file mode 100644 index 0000000000..da7d32da7b --- /dev/null +++ b/tests/instruments/agilent/test_agilent4294A.py @@ -0,0 +1,55 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest +from pymeasure.test import expected_protocol +from pymeasure.instruments.agilent.agilent4294A import Agilent4294A + + +@pytest.mark.parametrize("freq", [40, 140E6]) +def test_set_start_freq(freq): + """ Test Agilent 4294A start frequency setter """ + with expected_protocol(Agilent4294A, [(f"STAR {freq:.0f} HZ", None), ],) as inst: + inst.start_frequency = freq + + +@pytest.mark.parametrize("freq", [40, 140E6]) +def test_get_start_freq(freq): + """ Test Agilent 4294A start frequency getter """ + with expected_protocol(Agilent4294A, [("STAR?", freq), ],) as inst: + assert freq == inst.start_frequency + + +@pytest.mark.parametrize("freq", [40, 140E6]) +def test_set_stop_freq(freq): + """ Test Agilent 4294A stop frequency setter """ + with expected_protocol(Agilent4294A, [(f"STOP {freq:.0f} HZ", None), ],) as inst: + inst.stop_frequency = freq + + +@pytest.mark.parametrize("freq", [40, 140E6]) +def test_get_stop_freq(freq): + """ Test Agilent 4294A stop frequency getter """ + with expected_protocol(Agilent4294A, [("STOP?", freq), ],) as inst: + assert freq == inst.stop_frequency From fe955f2ffbe5ed4bbb2bb2573adc2ed67fbf5a5e Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 7 May 2024 14:20:30 +0200 Subject: [PATCH 118/181] Update readme and setup.cfg --- README.rst | 5 +++-- setup.cfg | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 4dd743cfae..c498a53970 100644 --- a/README.rst +++ b/README.rst @@ -4,13 +4,14 @@ PyMeasure scientific package ############################ -PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. +PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. +Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. PyMeasure is currently under active development, so please report any issues you experience to our `Issues page`_. .. _Issues page: https://github.com/pymeasure/pymeasure/issues -PyMeasure runs on Python 3.8-3.11, and is tested with continuous-integration on Linux, macOS, and Windows. +PyMeasure runs on Python 3.8-3.12, and is tested with continuous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml/badge.svg :target: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml diff --git a/setup.cfg b/setup.cfg index 9ea26832a8..614b0165d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Scientific/Engineering [options] From be1790992b875b2995f7b16a324eea2c74c4fb69 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 7 May 2024 14:23:19 +0200 Subject: [PATCH 119/181] Add CI for python 312 --- .github/pymeasure_python312.yml | 26 +++++++++++++++++++ .github/workflows/pymeasure_CI.yml | 40 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .github/pymeasure_python312.yml diff --git a/.github/pymeasure_python312.yml b/.github/pymeasure_python312.yml new file mode 100644 index 0000000000..bffba88b52 --- /dev/null +++ b/.github/pymeasure_python312.yml @@ -0,0 +1,26 @@ +name: pymeasure_python312 +channels: + - conda-forge +dependencies: + - cloudpickle=1.6.0 + - numpy=1.26.4 + - pandas=2.2.0 + - pint=0.18 + - pyqt=5.15.9 + - pyqtgraph=0.12.4 + - pyserial=3.4 + - pyvisa=1.14.1 + - pyzmq=25.1.2 + - qt=5.15.8 +# Development dependencies below + - pytest-qt=4.2.0 + - pytest-runner=5.2 + - pytest=7.2.0 + - pytest-cov=4.1.0 + - pyvisa-sim==0.5.1 + - flake8=6.0.0 + - setuptools_scm # don't pin, to get newest features + - sphinx=5.3.0 + - sphinx_rtd_theme=1.2.2 +# pip is currently not needed, but the recommended tool when packages that are unavailable on conda are required +# - pip # don't pin, to gain newest conda compatibility fixes diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index fbd246b813..638fdd9ee8 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -154,3 +154,43 @@ jobs: run: | echo "::add-matcher::.github/pytest.json" xvfb-run -a pytest + + test_python312: + name: Python ${{ matrix.python-version }}, ${{ matrix.os }} + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -l {0} + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + python-version: ["3.12"] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install pymeasure requirements + uses: mamba-org/setup-micromamba@v1 + with: + environment-file: .github/pymeasure_python312.yml + create-args: python=${{ matrix.python-version }} + cache-environment-key: py${{ matrix.python-version }}-${{ matrix.os }}-mamba-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} + cache-downloads: false + - name: Python version + run: python --version + - name: Install Pymeasure + # If the pytest problem matcher stops working because of bad paths, do an editable install + run: pip install .[tests] + - name: Pymeasure version + run: python -c "import pymeasure;print(pymeasure.__version__)" + - name: Run pytest with xvfb + if: runner.os == 'Linux' + run: | + echo "::add-matcher::.github/pytest.json" + xvfb-run -a pytest + - name: Run pytest + if: runner.os != 'Linux' + run: | + echo "::add-matcher::.github/pytest.json" + pytest From 8f28269d40a60b5fb49eddd69738adda7eb87a9e Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 7 May 2024 14:38:30 +0200 Subject: [PATCH 120/181] Catch ni AttributeError --- tests/instruments/test_all_instruments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py index 55ac486ef0..070611ac35 100644 --- a/tests/instruments/test_all_instruments.py +++ b/tests/instruments/test_all_instruments.py @@ -66,9 +66,10 @@ def find_devices_in_module(module): # Some non-required driver dependencies may not be installed on test computer, # for example ni.VirtualBench pass - except OSError: + except (OSError, AttributeError): # On Windows instruments.ni.daqmx can raise an OSError before ModuleNotFoundError # when checking installed driver files + # it raises an AttributeError under Python 312 pass return devices, channels From 17d42546cb5870e7050c66d5e64fe211eebdecfa Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 7 May 2024 14:43:59 +0200 Subject: [PATCH 121/181] Split readme lines --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c498a53970..ecff0095c3 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,8 @@ PyMeasure scientific package ############################ -PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. +PyMeasure makes scientific measurements easy to set up and run. +The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. PyMeasure is currently under active development, so please report any issues you experience to our `Issues page`_. From 2a16a21226f467f86f3875ab99d04a42b7d1e958 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 1 Feb 2024 18:46:41 +0100 Subject: [PATCH 122/181] Add publishing CI file. --- .github/workflows/python-publish.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000000..fc371e1936 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Build and Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1.8.11 From 17b93491b5f63ca3fc5dc83d024a0b7b1bcb37f3 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:38:44 +0100 Subject: [PATCH 123/181] Bump setup-python version --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index fc371e1936..409abc9cf2 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies From 256ebc3f9e7af5410f7e56f777fb131c62e5e3fa Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:57:16 +0100 Subject: [PATCH 124/181] Modify release notes --- RELEASE.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index f107572dd1..85a455f8b4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -16,21 +16,19 @@ 5. Push the changes up as a PR 6. Verify that the builds complete 7. Merge the PR -8. Fetch `master`, build and check the source packages - - `python -m pip install --upgrade build twine` - - `python -m build` - - Check the distributions (`twine check dist/*`, version will not yet be correct) -9. Create a git tag in the format "vX.Y.Z" -10. Build final packages and confirm the correct version number is being used - - `python -m build` - - Check the distributions (`twine check dist/*`) -11. Push the Git tag -12. Create a tagged [release on GitHub](https://github.com/pymeasure/pymeasure/releases). You'll have to paste in the changelog entry and probably edit it a bit as that form expects Markdown, not ReST (probably just removing `:code:` tags will be sufficient). - -## PyPI release +8. Create a new [release on GitHub](https://github.com/pymeasure/pymeasure/releases) + * Add a tag name in the format "vX.Y.Z" and select "create tag on publish" + * You'll have to paste in the changelog entry and probably edit it a bit as that form expects Markdown, not ReST (probably just removing `:code:` tags will be sufficient). + * Publish the release +8. Approve the _build and upload_ run under Actions. + This will create the wheel and upload it to PyPI. + +## PyPI release - manually Official guide [here](https://packaging.python.org/en/latest/tutorials/packaging-projects/). +If the upload action does not work, you can create a PyPI release manually: + 1. Upload the wheel and source distributions to the test server - `python -m twine upload --repository testpypi dist/*` 2. Verify the test repository: https://test.pypi.org/project/PyMeasure From 35fc7b47dad4350f29624a1ddce24d3479cb8bab Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 8 May 2024 10:55:23 +0200 Subject: [PATCH 125/181] Use a ternary operator for the environment file. --- .github/workflows/pymeasure_CI.yml | 45 ++---------------------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/.github/workflows/pymeasure_CI.yml b/.github/workflows/pymeasure_CI.yml index 638fdd9ee8..8b604e2e23 100644 --- a/.github/workflows/pymeasure_CI.yml +++ b/.github/workflows/pymeasure_CI.yml @@ -95,7 +95,7 @@ jobs: fail-fast: true matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 with: @@ -103,7 +103,8 @@ jobs: - name: Install pymeasure requirements uses: mamba-org/setup-micromamba@v1 with: - environment-file: .github/pymeasure.yml + # ternary operator: if python-version == 3.12 use pymeasure_python312.yml else use pymeasure.yml + environment-file: ${{ matrix.python-version == '3.12' && '.github/pymeasure_python312.yml' || '.github/pymeasure.yml'}} create-args: python=${{ matrix.python-version }} cache-environment-key: py${{ matrix.python-version }}-${{ matrix.os }}-mamba-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} cache-downloads: false @@ -154,43 +155,3 @@ jobs: run: | echo "::add-matcher::.github/pytest.json" xvfb-run -a pytest - - test_python312: - name: Python ${{ matrix.python-version }}, ${{ matrix.os }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash -l {0} - strategy: - fail-fast: true - matrix: - os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: ["3.12"] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install pymeasure requirements - uses: mamba-org/setup-micromamba@v1 - with: - environment-file: .github/pymeasure_python312.yml - create-args: python=${{ matrix.python-version }} - cache-environment-key: py${{ matrix.python-version }}-${{ matrix.os }}-mamba-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }} - cache-downloads: false - - name: Python version - run: python --version - - name: Install Pymeasure - # If the pytest problem matcher stops working because of bad paths, do an editable install - run: pip install .[tests] - - name: Pymeasure version - run: python -c "import pymeasure;print(pymeasure.__version__)" - - name: Run pytest with xvfb - if: runner.os == 'Linux' - run: | - echo "::add-matcher::.github/pytest.json" - xvfb-run -a pytest - - name: Run pytest - if: runner.os != 'Linux' - run: | - echo "::add-matcher::.github/pytest.json" - pytest From 3110582e47172852d1e66c8f96b770541f2d9d14 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 8 May 2024 14:33:48 +0200 Subject: [PATCH 126/181] Add missing steps to manual pypi release. --- RELEASE.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 85a455f8b4..32004f0d7b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -29,13 +29,21 @@ Official guide [here](https://packaging.python.org/en/latest/tutorials/packaging If the upload action does not work, you can create a PyPI release manually: -1. Upload the wheel and source distributions to the test server +1. Fetch `master`, build and check the source packages + - `python -m pip install --upgrade build twine` + - `python -m build` + - Check the distributions (`twine check dist/*`, version will not yet be correct) +2. Ensure to have a git tag in the format "vX.Y.Z" +3. Build final packages and confirm the correct version number is being used + - `python -m build` + - Check the distributions (`twine check dist/*`) +4. Upload the wheel and source distributions to the test server - `python -m twine upload --repository testpypi dist/*` -2. Verify the test repository: https://test.pypi.org/project/PyMeasure -3. Confirm that the installation works (best in a separate environment) +5. Verify the test repository: https://test.pypi.org/project/PyMeasure +6. Confirm that the installation works (best in a separate environment) - `python -m pip install --index-url https://test.pypi.org/simple/ --no-deps pymeasure` -4. Upload to the real repository (`twine upload dist/PyMeasure-*`) -5. Verify that the package is updated: https://pypi.org/project/PyMeasure +7. Upload to the real repository (`twine upload dist/PyMeasure-*`) +8. Verify that the package is updated: https://pypi.org/project/PyMeasure ## conda-forge feedstock From de2ed27f63c3d622e32fd5cdfd8f19b4f42a7407 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 8 May 2024 14:40:49 +0200 Subject: [PATCH 127/181] Ease version limits on pypi-publish action. --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 409abc9cf2..f4b3cfe6fb 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -28,4 +28,4 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@release/v1 From 81bf9ca4e02daaee7a1dfce8162f76d8a322890a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 8 May 2024 11:55:02 +0200 Subject: [PATCH 128/181] Add changelog --- CHANGES.rst | 88 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8a94c6d184..83adab6e08 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,14 +1,21 @@ -Upcoming version -================ + +Version 0.14.0 (2024-mm-dd) +=========================== Main items of this new release: -- Added support for numpy 2.0. -Added features -- SCPI instruments have :code:`next_error` property giving the next error. +- Add support for numpy 2.0 +- Add support for python 3.12 +- Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` +- Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. + Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. + Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. + Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. + For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. +- 15 new instruments Deprecated features ------------------- -- Remove :code:`TelnetAdapter`, as its library is deprecated. +- Remove :code:`TelnetAdapter`, as its library is deprecated (@BenediktBurger, #1045) - Replaced :code:`directory_input` keyword-argument of :code:`ManagedWindowBase` by :code:`enable_file_input` (@CasperSchippers, #964) - Make parameter :code:`includeSCPI` obligatory for all instruments, even those which use SCPI (@BenediktBurger, #1007) - Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPIMixin` class if the device supports SCPI commands. @@ -16,16 +23,75 @@ Deprecated features - Replaced :code:`error` property of Keithley instruments by :code:`next_error`. - Replaced :code:`measurement_time` property of Pendulum CNT-91 by :code:`gate_time`. - Replaced :code:`sample_rate` keyword-argument of :code:`buffer_frequency_time_series` of Pendulum CNT-91 by :code:`gate_time`. -- The property :code:`unit` of MKS937B switched to using values defined in :code:`instruments/mksinst/mks937b/Unit`. Old string values are not supported anymore. (@dkriegner, @BenediktBurger #1034) +- Replaced MKS937B :code:`unit` to use :code:`instruments/mksinst/mks937b/Unit` instead of strings (@dkriegner, @BenediktBurger #1034) + +Instruments mechanics +--------------------- +- Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019) +- Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) +- Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) +- Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) +- Remove auto-importing all instruments in :code:`pymeasure/instruments/__init__.py`` (@mcdo0486, #919) +- Add :code:`find_serial_port` to find a serial port by providing USB information (@BenediktBurger, #982) + +Instruments +----------- +- Add Agilent4294A (@driftregion, #998) +- Add AimTTI PL series power supplies (@guuskuiper, #942) +- Add HP11713A Switch & Attenuator Driver (@neuschs, #970) +- Add HP437B power meter (@neuschs, #979) +- Add Inficon SQM160 SQM-160 multi-film rate/thickness monitor (@dkriegner, #991) +- Add Keithley 2182 (@ConnorGCarr, #1043) +- Add KeithleyDMM6500 (@fwutw, #963) +- Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) +- Add KeysightE3631A (@OptimisticBeliever, #990) +- Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) +- Add Proterial rod4 (@ConnorGCarr, #1044) +- Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) +- Add redpitaya board (@seb5g, #1010, #1035) +- Add Teledyne HDO6xxx (@RobertoRoos, #868) +- Add Yokogawa AQ6370D Optical Spectral Analyzer (@jnnskls, #1059) +- Fix property docstrings of several instruments (@BenediktBurger, #1018) +- Fix checksums of hcp TC038D tests (@BenediktBurger, #987) +- Fix Hp8116a (@BenediktBurger, #1088) +- Fix Hp856x to append amplitude units (@neuschs, #977) +- Fix Stanford Research SR830 output conversion (@dkriegner, #1069) +- Fix SR830 missing get_buffer method (@seb5g, #999) +- Fix set command of SR860 aux output (@wehlgrundspitze, #1048) +- Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) +- Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) +- Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) +- Update Agilent33500 Series from :code:`.ch[]` to :code:`.channels[]` (@AlecVercruysse, #945) +- Update AWG401x driver to use 'channels' (@mcdo0486, #944) +- Update HP33120A with new burst modulation parameters (@mzen228, #1056) +- Update HP34401A with new remote control command. (@Rybok, #992) +- Update Keithleys' next_error (@msmttchr, #1030) +- Update pendulum CNT-91 (@bleykauf, #988) GUI --- - Add a :code:`FileInputWidget` to choose if and where the experiment data is stored. (@CasperSchippers, #964) -- A default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) +- Add a default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) +- Fix :code:`ScientificInput` to be locale compatible (@pyZerrenner, #1074) +- Fix exception if loading result file with an empty parameter (@poje42, #1016) + +Miscellaneous +------------- +- Add support for python 3.12 (@BenediktBurger, #1051) +- Add support for numpy 2.0 (@CasperSchippers, #1026) +- Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) +- Add citation file for PyMeasure repository (@mcdo0486, #1092) +- Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) +- Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) +- Fix/pandas futurewarning (@CasperSchippers, #1062) +- Change copyright year. (@BenediktBurger, #1032) +- Fix typos (@afuetterer, #1003) + +New Contributors +---------------- +@guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion -Dropped Support ---------------- -- Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. +**Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 Version 0.13.1 (2023-10-05) From 15646bf1bcccac92d4cc81eaf2c25d8d05e77946 Mon Sep 17 00:00:00 2001 From: Konrad/p Date: Fri, 10 May 2024 13:28:49 +0200 Subject: [PATCH 129/181] confirmed that SCPI works with the Keysight E36312A --- pymeasure/instruments/keysight/keysightE36312A.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/keysight/keysightE36312A.py b/pymeasure/instruments/keysight/keysightE36312A.py index f9018bb0a9..fe377f699f 100644 --- a/pymeasure/instruments/keysight/keysightE36312A.py +++ b/pymeasure/instruments/keysight/keysightE36312A.py @@ -25,7 +25,7 @@ import logging -from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin +from pymeasure.instruments import Instrument, Channel, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) @@ -71,7 +71,7 @@ class VoltageChannel(Channel): ) -class KeysightE36312A(SCPIUnknownMixin, Instrument): +class KeysightE36312A(SCPIMixin, Instrument): """ Represents the Keysight E36312A Power supply interface for interacting with the instrument. From 67473a2873b79605891fa9769947d8350c2bda75 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 13 May 2024 16:46:30 +0200 Subject: [PATCH 130/181] Update year and code to current master. --- pymeasure/instruments/kuhneelectronic/__init__.py | 2 +- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 5 +++-- tests/instruments/kuhneelectronic/test_kusg245_250a.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pymeasure/instruments/kuhneelectronic/__init__.py b/pymeasure/instruments/kuhneelectronic/__init__.py index 9e66811ca7..692edb9377 100644 --- a/pymeasure/instruments/kuhneelectronic/__init__.py +++ b/pymeasure/instruments/kuhneelectronic/__init__.py @@ -1,7 +1,7 @@ # # This file is part of the PyMeasure package. # -# Copyright (c) 2013-2023 PyMeasure Developers +# Copyright (c) 2013-2024 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index d3c2dcfb83..21bb9637ff 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -1,7 +1,7 @@ # # This file is part of the PyMeasure package. # -# Copyright (c) 2013-2023 PyMeasure Developers +# Copyright (c) 2013-2024 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -83,6 +83,7 @@ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): asrl={"baud_rate": 115200, "read_termination": termination_character, "write_termination": termination_character}, + includeSCPI=False, **kwargs) self._power_limit = power_limit @@ -395,7 +396,7 @@ def turn_on(self): self.rf_enabled = True def write(self, command, **kwargs): - self.adapter.write(command, **kwargs) + super().write(command, **kwargs) if _is_expecting_acknowledgement(command): s = self.read() if s != "A": diff --git a/tests/instruments/kuhneelectronic/test_kusg245_250a.py b/tests/instruments/kuhneelectronic/test_kusg245_250a.py index e65e13d84d..247a91b613 100644 --- a/tests/instruments/kuhneelectronic/test_kusg245_250a.py +++ b/tests/instruments/kuhneelectronic/test_kusg245_250a.py @@ -1,7 +1,7 @@ # # This file is part of the PyMeasure package. # -# Copyright (c) 2013-2023 PyMeasure Developers +# Copyright (c) 2013-2024 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal From fce1d7cbbd9dd7e6c969308ff5f863d230bd69a9 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 13 May 2024 16:53:25 +0200 Subject: [PATCH 131/181] Fix docstring. --- pymeasure/instruments/kuhneelectronic/kusg245_250a.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py index 21bb9637ff..97e6356544 100644 --- a/pymeasure/instruments/kuhneelectronic/kusg245_250a.py +++ b/pymeasure/instruments/kuhneelectronic/kusg245_250a.py @@ -319,7 +319,6 @@ def phase_shift(self, value): @property def reflection_limit(self): - "B%d", """Control limit of reflection in Watts (integer in 0 - no limit, 100, 150, 180, 200, 230). From c3fcbf554cb3484a9108cce954eb80a76223f2a6 Mon Sep 17 00:00:00 2001 From: Konrad/p Date: Tue, 14 May 2024 10:27:03 +0200 Subject: [PATCH 132/181] added my name to AUTHORS.txt --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 8181c9360d..3cbbd042ee 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -80,3 +80,4 @@ Douwe den Blanken Till Zürner J. A. Wilcox Nick James Kirkby +Konrad Gralher \ No newline at end of file From 23425177ed17ea449d06746846fdbd81a988515a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 14 May 2024 11:56:18 +0200 Subject: [PATCH 133/181] Change timer to ns perf counter --- tests/instruments/temptronic/test_temptronic_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/instruments/temptronic/test_temptronic_base.py b/tests/instruments/temptronic/test_temptronic_base.py index 26f581cd7a..ff424205ca 100644 --- a/tests/instruments/temptronic/test_temptronic_base.py +++ b/tests/instruments/temptronic/test_temptronic_base.py @@ -22,7 +22,7 @@ # THE SOFTWARE. # -from time import perf_counter +from time import perf_counter_ns from pymeasure.test import expected_protocol from pymeasure.instruments.temptronic.temptronic_base import ATSBase @@ -30,7 +30,7 @@ def test_check_query_delay(): with expected_protocol(ATSBase, [("TTIM?", "7")]) as inst: - start = perf_counter() + start = perf_counter_ns() assert inst.maximum_test_time == 7 - delay = perf_counter() - start - assert delay > 0.05 + delay = perf_counter_ns() - start + assert delay > 0.05 * 1e9 From 9313be7b49c9297608646446e4b106f24e6ffb24 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 14 May 2024 15:19:02 +0200 Subject: [PATCH 134/181] Add academic references --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 83adab6e08..0e8bedd5d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 +- Improve academic quotability with an up to date Zenodo DOI and with citation information. - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. From b893d195919af14e114cfc26d362de1cdba4ce88 Mon Sep 17 00:00:00 2001 From: Connor Carr Date: Tue, 14 May 2024 09:44:12 -0400 Subject: [PATCH 135/181] clean up agilent 4284A driver --- docs/api/instruments/agilent/agilent4284A.rst | 4 +- pymeasure/instruments/agilent/agilent4284A.py | 117 +++++++++++------- .../instruments/agilent/test_agilent4284A.py | 8 +- 3 files changed, 77 insertions(+), 52 deletions(-) diff --git a/docs/api/instruments/agilent/agilent4284A.rst b/docs/api/instruments/agilent/agilent4284A.rst index d497dbf93c..ceb34eba73 100644 --- a/docs/api/instruments/agilent/agilent4284A.rst +++ b/docs/api/instruments/agilent/agilent4284A.rst @@ -1,6 +1,6 @@ -############################## +####################### Agilent 4284A LCR Meter -############################## +####################### .. autoclass:: pymeasure.instruments.agilent.Agilent4284A :members: diff --git a/pymeasure/instruments/agilent/agilent4284A.py b/pymeasure/instruments/agilent/agilent4284A.py index 4f70fdf801..b8cef8e170 100644 --- a/pymeasure/instruments/agilent/agilent4284A.py +++ b/pymeasure/instruments/agilent/agilent4284A.py @@ -90,14 +90,6 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): dynamic=True ) - high_power_enabled = Instrument.measurement( - "OUTP:HPOW?", - """Get whether the high-power mode is enabled. - High power requires Option 001 (power amplifier / DC bias) is installed.""", - values={False: 0, True: 1}, - map_values=True - ) - bias_enabled = Instrument.control( "BIAS:STAT?", "BIAS:STAT %d", """Control whether DC bias is enabled.""", @@ -118,7 +110,7 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): bias_current = Instrument.control( "BIAS:CURR?", "BIAS:CURR %g", """Control the DC bias current in Amps. - Requires Option 001 (power amplifier / DC bias) is installed. + Requires Option 001 (power amplifier / DC bias) to be installed. Maximum is 100 mA.""", validator=strict_range, values=(0, 0), @@ -168,21 +160,37 @@ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): "FUNC:IMP:RANG:AUTO?", "FUNC:IMP:RANG:AUTO %d", """Control whether the impedance auto range is enabled.""", validator=strict_discrete_set, - values={False: 0, True: 1} + values={False: 0, True: 1}, + map_values=True + ) + + trigger_source = Instrument.control( + "TRIG:SOUR?", "TRIG:SOUR %s", + """Control trigger mode. Valid options are `INT`, `EXT`, `BUS`, or `HOLD`.""", + validator=strict_discrete_set, + values=('INT', 'EXT', 'BUS', 'HOLD'), + cast=str ) - def _set_ranges(self, condition): + trigger_delay = Instrument.control( + "TRIG:DEL?", "TRIG:DEL %g", + """Control trigger delay in seconds. Valid range is 0 to 60, with 1 ms resolution.""", + validator=strict_range, + values=(0, 60) + ) + + trigger_continuous_enabled = Instrument.control( + "TRIG:CONT?", "TRIG:CONT %d", + """Control whether trigger state automatically returns to WAIT FOR TRIGGER + after measurement.""", + validator=strict_discrete_set, + values={False: 0, True: 1}, + map_values=True + ) + + def _set_ranges(self, high_power_mode): """Set dynamic property values and make copies for sweep_measurement to reference.""" - if condition == 0: - self.ac_current_values = (50e-6, 0.02) - self.ac_voltage_values = (0.005, 2) - self.bias_voltage_values = (0, 2) - self.bias_current_values = (0, 0) - self._ac_current_values = (50e-6, 0.02) - self._ac_voltage_values = (0.005, 2) - self._bias_voltage_values = (0, 2) - self._bias_current_values = (0, 0) - elif condition == 1: + if high_power_mode: self.ac_current_values = (50e-6, 0.2) self.ac_voltage_values = (0.005, 20) self.bias_voltage_values = (0, 40) @@ -191,27 +199,39 @@ def _set_ranges(self, condition): self._ac_voltage_values = (0.005, 20) self._bias_voltage_values = (0, 40) self._bias_current_values = (0, 0.1) + else: + self.ac_current_values = (50e-6, 0.02) + self.ac_voltage_values = (0.005, 2) + self.bias_voltage_values = (0, 2) + self.bias_current_values = (0, 0) + self._ac_current_values = (50e-6, 0.02) + self._ac_voltage_values = (0.005, 2) + self._bias_voltage_values = (0, 2) + self._bias_current_values = (0, 0) - def enable_high_power(self): - """Enable high power mode. - - Requires option 001 (power amplifier / DC bias) is installed. + @property + def high_power_enabled(self): + """Control whether high power mode is enabled. + Enabling requires option 001 (power amplifier / DC bias) to be installed. """ - if self.options[0] == '0': - log.warning("Agilent 4284A power amplifier is not installed.") - return - self._set_ranges(1) - self.write("OUTP:HPOW 1") - - def disable_high_power(self): - """Disable high power mode.""" - self._set_ranges(0) - self.write("OUTP:HPOW 0") + mode = self.values("OUTP:HPOW?", cast=int) + return bool(mode) + + @high_power_enabled.setter + def high_power_enabled(self, val): + if not val: + self._set_ranges(0) + self.write("OUTP:HPOW 0") + elif val and self.options[0] == '0': + raise AttributeError("Agilent 4284A power amplifier is not installed.") + else: + self._set_ranges(1) + self.write("OUTP:HPOW 1") def sweep_measurement(self, sweep_mode, sweep_values): """Run list sweep measurement using sequential trigger. - :param sweep_mode (str): parameter to sweep across. Must be one of `frequency`, + :param str sweep_mode: parameter to sweep across. Must be one of `frequency`, `voltage`, `current`, `bias_voltage`, or `bias_current`. :param sweep_values: list of parameter values to sweep across. @@ -227,10 +247,9 @@ def sweep_measurement(self, sweep_mode, sweep_values): } if sweep_mode not in param_dict: - log.warning( - "Sweep mode but be one of %s, not '%s'.", list(param_dict.keys()), sweep_mode + raise KeyError( + f"Sweep mode but be one of {list(param_dict.keys())}, not '{sweep_mode}'." ) - return low_limit = param_dict[sweep_mode][1][0] high_limit = param_dict[sweep_mode][1][1] @@ -241,7 +260,7 @@ def sweep_measurement(self, sweep_mode, sweep_values): ) sweep_truncated = [] for val in sweep_values: - if val <= high_limit and val >= low_limit: + if low_limit <= val <= high_limit: sweep_truncated.append(val) sweep_values = sweep_truncated @@ -276,13 +295,19 @@ def sweep_measurement(self, sweep_mode, sweep_values): return a_data, b_data, sweep_return def trigger(self): - """Execute a bus trigger, which can be used when :meth:`~.trigger_on_bus` - is configured. Returns result of triggered measurement. + """Execute a bus trigger, regardless of trigger state. + Can be used when :attr:`trigger_source` is set to `BUS`. + Returns result of triggered measurement. """ return self.values("*TRG") - def trigger_on_bus(self): - """Configure the trigger to detect events based on the bus trigger, which can be - activated by :meth:`~.trigger`. + def trigger_immediate(self): + """Execute a bus trigger, regardless of trigger state. + Can be used when :attr:`trigger_source` is set to `BUS`. + Measurement result must be retrieved with `FETCH?` command. """ - self.write("TRIG:SOUR BUS") + self.write("TRIG:IMM") + + def trigger_initiate(self): + """Change the trigger state from IDLE to WAIT FOR TRIGGER for one trigger sequence.""" + self.write("TRIG:INIT:IMM") diff --git a/tests/instruments/agilent/test_agilent4284A.py b/tests/instruments/agilent/test_agilent4284A.py index 807f89241b..e85db29ae6 100644 --- a/tests/instruments/agilent/test_agilent4284A.py +++ b/tests/instruments/agilent/test_agilent4284A.py @@ -47,13 +47,13 @@ def test_frequency_limit(): inst.frequency = 1 -@pytest.mark.parametrize("power_mode", [0, 1]) +@pytest.mark.parametrize("power_mode", ["0", "1"]) def test_high_power_mode(power_mode): with expected_protocol( Agilent4284A, [("OUTP:HPOW?", power_mode),], ) as inst: - assert power_mode == inst.high_power_enabled + assert bool(power_mode) == inst.high_power_enabled @pytest.mark.parametrize("impedance_mode", [ @@ -76,7 +76,7 @@ def test_enable_high_power(): ("OUTP:HPOW 1", None), ("VOLT:LEV 5", None),], ) as inst: - inst.enable_high_power() + inst.high_power_enabled = True inst.ac_voltage = 5 @@ -87,7 +87,7 @@ def test_disable_high_power(): [("OUTP:HPOW 0", None), ("VOLT:LEV 5", None)], ) as inst: - inst.disable_high_power() + inst.high_power_enabled = False inst.ac_voltage = 5 From bc56d169aa4ab19a4198e418322bfa4327089b5a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 15 May 2024 09:08:51 +0200 Subject: [PATCH 136/181] Add acceptable factor --- tests/instruments/temptronic/test_temptronic_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/instruments/temptronic/test_temptronic_base.py b/tests/instruments/temptronic/test_temptronic_base.py index ff424205ca..869c4d7f3b 100644 --- a/tests/instruments/temptronic/test_temptronic_base.py +++ b/tests/instruments/temptronic/test_temptronic_base.py @@ -33,4 +33,7 @@ def test_check_query_delay(): start = perf_counter_ns() assert inst.maximum_test_time == 7 delay = perf_counter_ns() - start - assert delay > 0.05 * 1e9 + # HACK acceptable factor is needed, as in CI under windows (Py38, Py39) the `sleep` interval + # is slightly shorter than the given argument. + acceptable_factor = 0.95 + assert delay > 0.05 * 1e9 * acceptable_factor From 11ee19d73219858f1db01e645fd0255fdcdc5f7c Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 09:40:21 +0200 Subject: [PATCH 137/181] Add newest pull requests to changelog. --- CHANGES.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0e8bedd5d6..2a86c6465b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,7 +12,7 @@ Main items of this new release: Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. -- 15 new instruments +- 17 new instruments Deprecated features ------------------- @@ -38,6 +38,7 @@ Instruments mechanics Instruments ----------- - Add Agilent4294A (@driftregion, #998) +- Add Agilent 4284A by (@ConnorGCarr #1079) - Add AimTTI PL series power supplies (@guuskuiper, #942) - Add HP11713A Switch & Attenuator Driver (@neuschs, #970) - Add HP437B power meter (@neuschs, #979) @@ -46,6 +47,7 @@ Instruments - Add KeithleyDMM6500 (@fwutw, #963) - Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) - Add KeysightE3631A (@OptimisticBeliever, #990) +- Add Kuhne Electronic KU SG 2.45 250A microwave generator (@jurajjasik, @BenediktBurger, @1108) - Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) - Add Proterial rod4 (@ConnorGCarr, #1044) - Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) @@ -56,9 +58,11 @@ Instruments - Fix checksums of hcp TC038D tests (@BenediktBurger, #987) - Fix Hp8116a (@BenediktBurger, #1088) - Fix Hp856x to append amplitude units (@neuschs, #977) +- Fix Keysight E36312A confirmed SCPI functionality (@Konradrundfunk, #1107) - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) +- Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) @@ -82,6 +86,7 @@ Miscellaneous - Add support for numpy 2.0 (@CasperSchippers, #1026) - Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) - Add citation file for PyMeasure repository (@mcdo0486, #1092) +- Add release CI (@BenediktBurger, #1039) - Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) - Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) - Fix/pandas futurewarning (@CasperSchippers, #1062) @@ -90,7 +95,7 @@ Miscellaneous New Contributors ---------------- -@guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion +@guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion, @jurajjasik, @Konradrundfunk **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 From a7a3b1610efbd73d9aa6745ab9e0a7b618dacf01 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 10:48:15 +0200 Subject: [PATCH 138/181] Bump setuptools_scm version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0bbf20173b..501e49a7b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] +requires = ["setuptools>=45", "wheel", "setuptools_scm>=8.1.0"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] From d6e7fe2fe6bb6d3da848056a37dbead1a54ecc5b Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 11:02:09 +0200 Subject: [PATCH 139/181] Add setuptools_scm explicitly to the environment. --- .github/pymeasure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pymeasure.yml b/.github/pymeasure.yml index 2f1c8af39e..7be904d260 100644 --- a/.github/pymeasure.yml +++ b/.github/pymeasure.yml @@ -19,7 +19,7 @@ dependencies: - pytest-cov=4.1.0 - pyvisa-sim==0.5.1 - flake8=6.0.0 - - setuptools_scm # don't pin, to get newest features + - setuptools_scm==8.1.0 # don't pin, to get newest features - sphinx=5.3.0 - sphinx_rtd_theme=1.2.2 # pip is currently not needed, but the recommended tool when packages that are unavailable on conda are required From 42e5c152030897a4b140888e2fb06cc04be76f45 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 11:18:44 +0200 Subject: [PATCH 140/181] Update changelog --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2a86c6465b..06e4b52ca3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,5 @@ -Version 0.14.0 (2024-mm-dd) +Version 0.14.0 (2024-05-21) =========================== Main items of this new release: @@ -62,7 +62,7 @@ Instruments - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) -- Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109) +- Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109, #1110) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) From b9e83b7e4342f453f6f54f921929e14024f82272 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 11:36:53 +0200 Subject: [PATCH 141/181] Update citation.cff. --- CITATION.cff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index e175a69cb5..f7c243f937 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -3,7 +3,7 @@ message: "If you use this software, please cite it as below." authors: - name: PyMeasure Developers title: "PyMeasure" -version: 0.13.1 +version: 0.14.0 doi: 10.5281/zenodo.595633 publisher: - name: Zenodo From 34a7f5f24eea73076169fbb19eb298bb3010b526 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 22 May 2024 09:30:50 +0200 Subject: [PATCH 142/181] Update changelog. --- CHANGES.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 06e4b52ca3..575ec65967 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,12 @@ -Version 0.14.0 (2024-05-21) +Version 0.14.0 (2024-05-22) =========================== Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 - Improve academic quotability with an up to date Zenodo DOI and with citation information. +- Add default :code:`queue` method and a :code:`FileInputWidget`, allowing to more quickly get started with the PyMeasure user interface (:code:`ManagedWindow`). - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. @@ -28,7 +29,7 @@ Deprecated features Instruments mechanics --------------------- -- Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019) +- Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019, #1047) - Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) - Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) - Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) From d4647325156efb9b42eec035d41d8396d5265e64 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 8 May 2024 11:55:02 +0200 Subject: [PATCH 143/181] Add changelog --- CHANGES.rst | 88 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8a94c6d184..83adab6e08 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,14 +1,21 @@ -Upcoming version -================ + +Version 0.14.0 (2024-mm-dd) +=========================== Main items of this new release: -- Added support for numpy 2.0. -Added features -- SCPI instruments have :code:`next_error` property giving the next error. +- Add support for numpy 2.0 +- Add support for python 3.12 +- Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` +- Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. + Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. + Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. + Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. + For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. +- 15 new instruments Deprecated features ------------------- -- Remove :code:`TelnetAdapter`, as its library is deprecated. +- Remove :code:`TelnetAdapter`, as its library is deprecated (@BenediktBurger, #1045) - Replaced :code:`directory_input` keyword-argument of :code:`ManagedWindowBase` by :code:`enable_file_input` (@CasperSchippers, #964) - Make parameter :code:`includeSCPI` obligatory for all instruments, even those which use SCPI (@BenediktBurger, #1007) - Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPIMixin` class if the device supports SCPI commands. @@ -16,16 +23,75 @@ Deprecated features - Replaced :code:`error` property of Keithley instruments by :code:`next_error`. - Replaced :code:`measurement_time` property of Pendulum CNT-91 by :code:`gate_time`. - Replaced :code:`sample_rate` keyword-argument of :code:`buffer_frequency_time_series` of Pendulum CNT-91 by :code:`gate_time`. -- The property :code:`unit` of MKS937B switched to using values defined in :code:`instruments/mksinst/mks937b/Unit`. Old string values are not supported anymore. (@dkriegner, @BenediktBurger #1034) +- Replaced MKS937B :code:`unit` to use :code:`instruments/mksinst/mks937b/Unit` instead of strings (@dkriegner, @BenediktBurger #1034) + +Instruments mechanics +--------------------- +- Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019) +- Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) +- Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) +- Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) +- Remove auto-importing all instruments in :code:`pymeasure/instruments/__init__.py`` (@mcdo0486, #919) +- Add :code:`find_serial_port` to find a serial port by providing USB information (@BenediktBurger, #982) + +Instruments +----------- +- Add Agilent4294A (@driftregion, #998) +- Add AimTTI PL series power supplies (@guuskuiper, #942) +- Add HP11713A Switch & Attenuator Driver (@neuschs, #970) +- Add HP437B power meter (@neuschs, #979) +- Add Inficon SQM160 SQM-160 multi-film rate/thickness monitor (@dkriegner, #991) +- Add Keithley 2182 (@ConnorGCarr, #1043) +- Add KeithleyDMM6500 (@fwutw, #963) +- Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) +- Add KeysightE3631A (@OptimisticBeliever, #990) +- Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) +- Add Proterial rod4 (@ConnorGCarr, #1044) +- Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) +- Add redpitaya board (@seb5g, #1010, #1035) +- Add Teledyne HDO6xxx (@RobertoRoos, #868) +- Add Yokogawa AQ6370D Optical Spectral Analyzer (@jnnskls, #1059) +- Fix property docstrings of several instruments (@BenediktBurger, #1018) +- Fix checksums of hcp TC038D tests (@BenediktBurger, #987) +- Fix Hp8116a (@BenediktBurger, #1088) +- Fix Hp856x to append amplitude units (@neuschs, #977) +- Fix Stanford Research SR830 output conversion (@dkriegner, #1069) +- Fix SR830 missing get_buffer method (@seb5g, #999) +- Fix set command of SR860 aux output (@wehlgrundspitze, #1048) +- Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) +- Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) +- Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) +- Update Agilent33500 Series from :code:`.ch[]` to :code:`.channels[]` (@AlecVercruysse, #945) +- Update AWG401x driver to use 'channels' (@mcdo0486, #944) +- Update HP33120A with new burst modulation parameters (@mzen228, #1056) +- Update HP34401A with new remote control command. (@Rybok, #992) +- Update Keithleys' next_error (@msmttchr, #1030) +- Update pendulum CNT-91 (@bleykauf, #988) GUI --- - Add a :code:`FileInputWidget` to choose if and where the experiment data is stored. (@CasperSchippers, #964) -- A default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) +- Add a default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) +- Fix :code:`ScientificInput` to be locale compatible (@pyZerrenner, #1074) +- Fix exception if loading result file with an empty parameter (@poje42, #1016) + +Miscellaneous +------------- +- Add support for python 3.12 (@BenediktBurger, #1051) +- Add support for numpy 2.0 (@CasperSchippers, #1026) +- Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) +- Add citation file for PyMeasure repository (@mcdo0486, #1092) +- Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) +- Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) +- Fix/pandas futurewarning (@CasperSchippers, #1062) +- Change copyright year. (@BenediktBurger, #1032) +- Fix typos (@afuetterer, #1003) + +New Contributors +---------------- +@guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion -Dropped Support ---------------- -- Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. +**Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 Version 0.13.1 (2023-10-05) From b987ea0201d9ef2a9ebcedb86997527f766deee7 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 14 May 2024 15:19:02 +0200 Subject: [PATCH 144/181] Add academic references --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 83adab6e08..0e8bedd5d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 +- Improve academic quotability with an up to date Zenodo DOI and with citation information. - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. From 704e0b79fa0fd15961abdcf798d9d692ee98e353 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 09:40:21 +0200 Subject: [PATCH 145/181] Add newest pull requests to changelog. --- CHANGES.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0e8bedd5d6..2a86c6465b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,7 +12,7 @@ Main items of this new release: Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. -- 15 new instruments +- 17 new instruments Deprecated features ------------------- @@ -38,6 +38,7 @@ Instruments mechanics Instruments ----------- - Add Agilent4294A (@driftregion, #998) +- Add Agilent 4284A by (@ConnorGCarr #1079) - Add AimTTI PL series power supplies (@guuskuiper, #942) - Add HP11713A Switch & Attenuator Driver (@neuschs, #970) - Add HP437B power meter (@neuschs, #979) @@ -46,6 +47,7 @@ Instruments - Add KeithleyDMM6500 (@fwutw, #963) - Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) - Add KeysightE3631A (@OptimisticBeliever, #990) +- Add Kuhne Electronic KU SG 2.45 250A microwave generator (@jurajjasik, @BenediktBurger, @1108) - Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) - Add Proterial rod4 (@ConnorGCarr, #1044) - Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) @@ -56,9 +58,11 @@ Instruments - Fix checksums of hcp TC038D tests (@BenediktBurger, #987) - Fix Hp8116a (@BenediktBurger, #1088) - Fix Hp856x to append amplitude units (@neuschs, #977) +- Fix Keysight E36312A confirmed SCPI functionality (@Konradrundfunk, #1107) - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) +- Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) @@ -82,6 +86,7 @@ Miscellaneous - Add support for numpy 2.0 (@CasperSchippers, #1026) - Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) - Add citation file for PyMeasure repository (@mcdo0486, #1092) +- Add release CI (@BenediktBurger, #1039) - Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) - Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) - Fix/pandas futurewarning (@CasperSchippers, #1062) @@ -90,7 +95,7 @@ Miscellaneous New Contributors ---------------- -@guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion +@guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion, @jurajjasik, @Konradrundfunk **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 From 093108d4861a59cbc29258b1296b6452f0bbfb2a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 10:48:15 +0200 Subject: [PATCH 146/181] Bump setuptools_scm version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0bbf20173b..501e49a7b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] +requires = ["setuptools>=45", "wheel", "setuptools_scm>=8.1.0"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] From 014106390fca01b39e87e64fec5cce63187c3863 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 11:02:09 +0200 Subject: [PATCH 147/181] Add setuptools_scm explicitly to the environment. --- .github/pymeasure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pymeasure.yml b/.github/pymeasure.yml index 2f1c8af39e..7be904d260 100644 --- a/.github/pymeasure.yml +++ b/.github/pymeasure.yml @@ -19,7 +19,7 @@ dependencies: - pytest-cov=4.1.0 - pyvisa-sim==0.5.1 - flake8=6.0.0 - - setuptools_scm # don't pin, to get newest features + - setuptools_scm==8.1.0 # don't pin, to get newest features - sphinx=5.3.0 - sphinx_rtd_theme=1.2.2 # pip is currently not needed, but the recommended tool when packages that are unavailable on conda are required From d9095e8b9caf2afb3f958af75e5eebfd3372d06d Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 11:18:44 +0200 Subject: [PATCH 148/181] Update changelog --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2a86c6465b..06e4b52ca3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,5 @@ -Version 0.14.0 (2024-mm-dd) +Version 0.14.0 (2024-05-21) =========================== Main items of this new release: @@ -62,7 +62,7 @@ Instruments - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) -- Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109) +- Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109, #1110) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) From 8b6d4e848120e07c956a589d4403cab0bb4c031c Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Tue, 21 May 2024 11:36:53 +0200 Subject: [PATCH 149/181] Update citation.cff. --- CITATION.cff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index e175a69cb5..f7c243f937 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -3,7 +3,7 @@ message: "If you use this software, please cite it as below." authors: - name: PyMeasure Developers title: "PyMeasure" -version: 0.13.1 +version: 0.14.0 doi: 10.5281/zenodo.595633 publisher: - name: Zenodo From 6573de56a1f7a5cd8e6a7d753c61d68f262a6e1a Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Wed, 22 May 2024 09:30:50 +0200 Subject: [PATCH 150/181] Update changelog. --- CHANGES.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 06e4b52ca3..575ec65967 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,12 @@ -Version 0.14.0 (2024-05-21) +Version 0.14.0 (2024-05-22) =========================== Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 - Improve academic quotability with an up to date Zenodo DOI and with citation information. +- Add default :code:`queue` method and a :code:`FileInputWidget`, allowing to more quickly get started with the PyMeasure user interface (:code:`ManagedWindow`). - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. @@ -28,7 +29,7 @@ Deprecated features Instruments mechanics --------------------- -- Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019) +- Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019, #1047) - Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) - Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) - Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) From 711a85ca81b746745820e8a426e0f4df39e8dcc9 Mon Sep 17 00:00:00 2001 From: Sionwage <138145542+Sionwage@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:43:28 -0500 Subject: [PATCH 151/181] Add support for Hewlett Packard 8753E VNA (#1004) * Wiped branch and branched from the current pymeasure master to add my Hewlett Packard Instrument Files. * Fixed flake8 errors. * Removed "HP8753E" replaced with self or nothing. * Fixed flake8 linting errors. * Fixed title overline too short and added extra new line for hp8753E.rst to match another one in the same folder for the docs. * Fixing sphinx document error for `pymeasure.instruments.hp.hp8753e.HP8753E:2: WARNING: Field list ends without a blank line; unexpected unindent.` https://stackoverflow.com/questions/61888521/python-sphinx-warning-definition-list-ends-without-a-blank-line-unexpected-uni * Cleaned up HP8753E.measuring_parameter * Fixed the nonetype split error for measuring_parameters by hardcoding the docstring instead of trying to do a f-string. * Lost IFBW somewhere? * Updates requested by BenediktBurger * Added the pytest skip back for "test_hp8753e_with_device.py" * Rewrote the tests to work with the Prologix Adapter or pyvisa test fixture using the --device-address flag. Also adjusted the sleep delays for allowing the VNA to catch up. * Updated AUTHORS.txt * Fixed HP8753E Test for `set_single_frequency_scan` and HP8753E docstring ending on a blank line * Fixed incorrectly written assertion and used the data variable for the `test_hp8753e_with_device_scan_single` * Fix linting errors * Fixing some kind of local git/github disconnect * Fixing items to finish submitting PR * Not sure why these reverted * Removed temp_prologix variable and added it to the prior statement. * Fix copyright and removed "Property is dynamic and allows remapping min/max limits." --------- Co-authored-by: Sionwage Co-authored-by: Sionwage --- AUTHORS.txt | 3 +- docs/api/instruments/hp/hp8753E.rst | 8 + docs/api/instruments/hp/index.rst | 1 + pymeasure/instruments/hp/__init__.py | 1 + pymeasure/instruments/hp/hp8753e.py | 466 ++++++++++++++++++ tests/instruments/hp/test_hp8753e.py | 141 ++++++ .../hp/test_hp8753e_with_device.py | 349 +++++++++++++ 7 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 docs/api/instruments/hp/hp8753E.rst create mode 100644 pymeasure/instruments/hp/hp8753e.py create mode 100644 tests/instruments/hp/test_hp8753e.py create mode 100644 tests/instruments/hp/test_hp8753e_with_device.py diff --git a/AUTHORS.txt b/AUTHORS.txt index f848753bdf..4bda7f0101 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -81,4 +81,5 @@ Douwe den Blanken Till Zürner J. A. Wilcox Nick James Kirkby -Konrad Gralher \ No newline at end of file +Konrad Gralher +David Ziliak \ No newline at end of file diff --git a/docs/api/instruments/hp/hp8753E.rst b/docs/api/instruments/hp/hp8753E.rst new file mode 100644 index 0000000000..c6a1a3856f --- /dev/null +++ b/docs/api/instruments/hp/hp8753E.rst @@ -0,0 +1,8 @@ +################################ +HP 8753E Vector Network Analyzer +################################ + + +.. autoclass:: pymeasure.instruments.hp.HP8753E + :members: + :show-inheritance: diff --git a/docs/api/instruments/hp/index.rst b/docs/api/instruments/hp/index.rst index cf25d2001c..8a4a1223ed 100644 --- a/docs/api/instruments/hp/index.rst +++ b/docs/api/instruments/hp/index.rst @@ -18,6 +18,7 @@ If the instrument you are looking for is not here, also check :doc:`Agilent<../a hp8116A hp856xx hp8657B + hp8753E hp11713A hp437B hplegacyinstrument diff --git a/pymeasure/instruments/hp/__init__.py b/pymeasure/instruments/hp/__init__.py index 107f11f620..f6d545a61b 100644 --- a/pymeasure/instruments/hp/__init__.py +++ b/pymeasure/instruments/hp/__init__.py @@ -30,6 +30,7 @@ from .hp8657b import HP8657B from .hp856Xx import HP8560A from .hp856Xx import HP8561B +from .hp8753e import HP8753E from .hp11713a import HP11713A from .hp437b import HP437B from .hpsystempsu import HP6632A diff --git a/pymeasure/instruments/hp/hp8753e.py b/pymeasure/instruments/hp/hp8753e.py new file mode 100644 index 0000000000..88c3cf44f0 --- /dev/null +++ b/pymeasure/instruments/hp/hp8753e.py @@ -0,0 +1,466 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging +from time import sleep +from time import time as now + +import numpy as np +from pyvisa import VisaIOError + +from pymeasure.instruments import Instrument +from pymeasure.instruments.validators import strict_discrete_set, strict_range + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +class HP8753E(Instrument): + """Represents the HP8753E Vector Network Analyzer + and provides a high-level interface for taking sweeps of the + scattering or measurement parameters. + + :param name: Optional set the name of the instrument (str). + :param min_frequency: Optional set the minimum validator for `HP8753E.start_frequency`, + `HP8753E.stop_frequency`, `HP8753E.span_frequency`, `HP8753E.center_frequency` + on initialization (float in Hz). + :param max_frequency: Optional set the maximum validator for `HP8753E.start_frequency`, + `HP8753E.stop_frequency`, `HP8753E.span_frequency`, `HP8753E.center_frequency` + on initialization (float in Hz). + :param min_power: Optional set the minimum validator for `HP8753E.power` on initialization + (float in dBm). + :param max_power: Optional set the maximum validator for `HP8753E.power` on initialization + (float in dBm). + + + """ + + def __init__( + self, + adapter=None, + name=None, + min_frequency=30.0e3, + max_frequency=6.0e9, + min_power=-70, + max_power=10, + **kwargs, + ): + super().__init__(adapter=adapter, name=name, includeSCPI=False, **kwargs) + + self._manu = "" + self._model = "" + self._fw = "" + self._sn = "" + self._options = "" + + self.start_frequency_values = [min_frequency, max_frequency] + self.stop_frequency_values = [min_frequency, max_frequency] + self.center_frequency_values = [min_frequency, max_frequency] + self.span_frequency_values = [0, max_frequency - min_frequency] + + self.power_values = [min_power, max_power] + + if name is None: + # written this way to pass 'test_all_instruments.py' while allowing the + # *IDN? to populate the name of the VNA + try: + self._manu, self._model, _, self._fw = self.id + except ValueError: + self._manu = "Hewlett Packard" + self._model = "8753E" + self._desc = "Vector Network Analyzer" + name = self.name = f"{self._manu} {self._model} {self._desc}" + else: + self.name = name + + ALLOWED_BANDWIDTH = [10, 30, 100, 300, 1000, 3000, 3700, 6000] + SCAN_POINT_VALUES = [3, 11, 21, 26, 51, 101, 201, 401, 801, 1601] + MEASURING_PARAMETERS = ["S11", "S12", "S21", "S22", "A/R", "B/R", "A/B", "A", "B", "R"] + MEASURING_PARAMETER_MAP = { + "A/R": "AR", + "B/R": "BR", + "A/B": "AB", + "A": "MEASA", + "B": "MEASB", + "R": "MEASR", + } + + start_frequency = Instrument.control( + "STAR?", + "STAR %e Hz", + """Control the start frequency in Hz. (float).""", + validator=strict_range, + cast=float, + dynamic=True, + values=[30.0e3, 6.0e9], + ) + + stop_frequency = Instrument.control( + "STOP?", + "STOP %e Hz", + """Control the stop frequency in Hz. (float).""", + validator=strict_range, + cast=float, + dynamic=True, + values=[30e3, 6e9], + ) + + center_frequency = Instrument.control( + "CENT?", + "CENT %e Hz", + """Control the center frequency in Hz (float).""", + cast=float, + validator=strict_range, + dynamic=True, + values=[30e3, 6e9], + ) + + span_frequency = Instrument.control( + "SPAN?", + "SPAN %e Hz", + """Control the span of the sweep frequency in Hz (float).""", + cast=float, + validator=strict_range, + dynamic=True, + values=[0, 6e9 - 30e3], + ) + + sweep_time = Instrument.control( + "SWET?", + "SWET%.2e", + """Control the sweep time in seconds. (float truncated from 0.0 to 36_400.0) + """, + validator=strict_range, + cast=float, + values=[0.01, 36_400.0], + ) + + def set_sweep_time_fastest(self): + """Set instrument scan sweep time to select fastest possible time.""" + self.write("SWEA") + + averages = Instrument.control( + "AVERFACT?", + "AVERFACT%d", + """Control the number of averages for a scan sweep. (int truncated from 1 to 999). + """, + cast=lambda x: int(float(x)), # need float() to convert scientific notation in strings + validator=strict_range, + values=[1, 999], + ) + + averaging_enabled = Instrument.control( + "AVERO?", + "AVERO%d", + """Control whether or not averaging is enabled. (boolean)""", + cast=bool, + validator=strict_discrete_set, + map_values=True, + values={True: 1, False: 0, "ON": 1, "OFF": 0, "1": True, "0": False}, + ) + + correction_enabled = Instrument.control( + "CORR?", + "CORR%d", + """Control whether or not correction is enabled. (boolean)""", + cast=bool, + validator=strict_discrete_set, + map_values=True, + values={True: 1, False: 0, "ON": 1, "OFF": 0}, + ) + + def averaging_restart(self): + """Restart sweep averaging.""" + self.write("AVERREST") + + def emit_beep(self): + """Make the VNA emit a beep""" + self.write("EMIB") + + power = Instrument.control( + "POWE?", + "POWE%.3e", + """Control the output RF power of the instrument's active port. (float).""", + cast=float, + validator=strict_range, + dynamic=True, + values=[-70, 10], + ) + + output_enabled = Instrument.control( + "SOUP?", + "SOUP%s", + """Control RF Output State (boolean)""", + cast=bool, + map_values=True, + validator=strict_discrete_set, + values={True: 1, False: 0, "ON": 1, "OFF": 0}, + ) + + trigger_hold = Instrument.control( + "HOLD?", + "%s", + """Control Sweep Scan Trigger State (boolean)""", + cast=str, + set_process=lambda x: "HOLD" if x else "CONT", + get_process=lambda x: x == "1", + ) + + trigger_continuous = Instrument.control( + "CONT?", + "%s", + """Control Sweep Scan Trigger State (boolean)""", + cast=str, + set_process=lambda x: "CONT" if x else "HOLD", + get_process=lambda x: x == "1", + ) + + scan_points = Instrument.control( + "POIN?", + "POIN%d", + f"""Control the number of points used for a scan sweep. + From the set {SCAN_POINT_VALUES}""", + cast=lambda x: int(float(x)), + validator=strict_discrete_set, + values=SCAN_POINT_VALUES, + ) + + id = Instrument.measurement( + "*IDN?", + """Get the identification of the instrument (list of strings)""", + cast=str, + ) + + sn = Instrument.measurement( + "OUTPSERN", + """Get the serial number for the instrument""", + ) + + options = Instrument.measurement( + "OUTPOPTS", + """Get the installed options for the instrument""", + ) + + @property + def manu(self): + """Get the manufacturer of the instrument.""" + if self._manu == "": + self._manu, self._model, _, self._fw = self.id + return self._manu + + @property + def model(self): + """Get the model of the instrument.""" + if self._model == "": + self._manu, self._model, _, self._fw = self.id + return self._model + + @property + def fw(self): + """Get the firmware of the instrument.""" + if self._fw == "": + self._manu, self._model, _, self._fw = self.id + return self._fw + + def set_fixed_frequency(self, frequency): + """Set the sweep to be a fixed frequency in Hz.""" + self.start_frequency = frequency + self.stop_frequency = frequency + self.scan_points = 3 + + @property + def measuring_parameter(self): + """Get the active Scattering or Measuring Parameter being measured. + (str from ['S11', 'S21', 'S12', 'S22', 'A', 'B', 'R'])""" + for parameter in self.MEASURING_PARAMETERS: + if parameter in self.MEASURING_PARAMETER_MAP: + if int(self.ask(f"{self.MEASURING_PARAMETER_MAP[parameter]}?")) == 1: + return parameter + elif int(self.ask(f"{parameter}?")) == 1: + return parameter + return None + + @measuring_parameter.setter + def measuring_parameter(self, value): + """Set the active Measuring Parameter to be measured. + (str from ['S11', 'S21', 'S12', 'S22', 'A', 'B', 'R'])""" + if value in self.MEASURING_PARAMETERS: + if value in self.MEASURING_PARAMETER_MAP: + self.write("%s" % self.MEASURING_PARAMETER_MAP[value]) + else: + self.write("%s" % value) + else: + raise ValueError( + f"Invalid value '{value}' scattering parameter requested for \ + {self._manu} {self._model}. Valid values are: {self.MEASURING_PARAMETERS}" + ) + + IFBW = Instrument.control( + "IFBW?", + "IFBW%d", + f"""Control the IF Bandwidth of the instrument for a scan sweep. + (int from the set {ALLOWED_BANDWIDTH}).""", + cast=lambda x: int(float(x)), + validator=strict_discrete_set, + values=ALLOWED_BANDWIDTH, + ) + + def reset(self): + """Reset the instrument. May cause RF Output power to be enabled!""" + self.write("*RST") + sleep(0.25) + + def scan(self, timeout=10): + """Initiates a scan with the number of averages specified and + blocks until the operation is complete. + + :param timeout: Optional value in seconds to add to the sweep time before timeout occurs. + :raise TimeoutError: If the VNA fails to complete the sweep before timeout occurs. + :return: None. + """ + + # get time to perform sweep + sweep_time = self.sweep_time + timeout + + # get number of averages if enabled + if self.averaging_enabled: + sweep_time = sweep_time * self.averages + + self.scan_single() + + # create a time limit + start = now() + + # All queries will block until the scan is done, so use NOOP? to check. + # These queries will time out after several seconds though, + # so query repeatedly until the scan finishes. + while True: + try: + # status = self.ask("NOOP?") + self.ask("NOOP?") + break + except VisaIOError: + pass + + # calculate time sweep should be complete by + if now() > (start + sweep_time): + raise TimeoutError( + f"VNA Scan took longer than {sweep_time} seconds to complete and timed out." + ) + + sleep(0.5) + while self.adapter.connection.bytes_in_buffer > 0: + _ = self.read() + sleep(0.25) + + def scan_single(self): + """Initiates a single scan or N scans averaged based on averaging + This function is not blocking. + """ + if self.averaging_enabled: + self.write(f"NUMG{self.averages}") + else: + self.write("SING") + + @property + def frequencies(self): + """ + Get a list of frequencies from the last scan. + + :return: An array of frequencies sized by number of points in sweep) + :rtype: numpy.ndarray + """ + return np.linspace(self.start_frequency, self.stop_frequency, num=self.scan_points) + + @property + def data_complex(self, timeout=10): + """ + Get the complex s-parameter measurements from the last scan. + This function is blocking until it is completed. + + :param timeout: Optional value in seconds to wait until a timeout occurs. + :raise TimeoutError: If the VNA fails to complete the sweep before timeout occurs. + :return: An array of s-parameters for the measurement_parameter. + :rtype: numpy.ndarray + """ + + points = 0 + counter = 0 + + start = now() + while points == 0: + try: + # get number of points + points = self.scan_points + except VisaIOError: + sleep(0.1) + counter += 1 + pass + + if now() > start + timeout: + raise TimeoutError( + f"Failed to read data. Data transfer method timed \ + out after {timeout} seconds" + ) + + # read the extra self.scan_points out of the buffer + for i in range(counter): + self.read() + sleep(0.1) + + # get time to perform sweep + # sweep_time = self.sweep_time + + # Only written for ASCII data transfer + self.write("FORM4") + + temp_data = [] + start = now() + self.write("OUTPDATA") + + while len(temp_data) < (points): + try: + if self.adapter.connection.bytes_in_buffer >= 50: + temp_data.append(self.read_bytes(50).decode("utf-8").strip("\n").split(",")) + else: + sleep(0.001) + + # break the loop if the sweep gets interrupted or takes too long + if now() > start + timeout: + raise TimeoutError( + f"Failed to read data. Data transfer method timed \ + out after {timeout} seconds" + ) + except VisaIOError: + pass + + # process string data into complex numbers + preformat_data = [float(point[0]) + float(point[1]) * 1j for point in temp_data] + data = np.array(preformat_data) + + return data + + def shutdown(self): + """Shutdown - Disables RF Output.""" + self.output_enabled = False diff --git a/tests/instruments/hp/test_hp8753e.py b/tests/instruments/hp/test_hp8753e.py new file mode 100644 index 0000000000..de7ad20637 --- /dev/null +++ b/tests/instruments/hp/test_hp8753e.py @@ -0,0 +1,141 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging + +from pymeasure.instruments.hp import HP8753E +from pymeasure.test import expected_protocol + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +def test_hp8753e_id(): + """Verify *IDN? communication""" + with expected_protocol( + HP8753E, + [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), ("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n")], + ) as inst: + assert inst.id == ["HEWLETT PACKARD", "8753E", "0", "7.10"] + + +def test_hp8753e_start_frequency(): + with expected_protocol( + HP8753E, + [ + ("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), + ("STAR?", "30E+003\n"), + ("STAR 4.000000e+05 Hz", None), + ("STAR?", "400E+003\n"), + ], + ) as inst: + assert inst.start_frequency == 30_000 + inst.start_frequency = 400e3 + assert inst.start_frequency == 400_000 + + +def test_hp8753e_set_sweep_time_fastest(): + with expected_protocol( + HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), ("SWEA", None)] + ) as inst: + assert inst.set_sweep_time_fastest() is None + + +def test_hp8753e_manu(): + with expected_protocol(HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n")]) as inst: + assert inst.manu == "HEWLETT PACKARD" + + +def test_hp8753e_model(): + with expected_protocol(HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n")]) as inst: + assert inst.model == "8753E" + + +def test_hp8753e_fw(): + with expected_protocol(HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n")]) as inst: + assert inst.fw == "7.10" + + +def test_hp8753e_set_single_frequency_scan(): + with expected_protocol( + HP8753E, + [ + ("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), + ("STAR 5.000000e+07 Hz", None), + ("STOP 5.000000e+07 Hz", None), + ("POIN3", None), + ], + ) as inst: + inst.set_fixed_frequency(50e6) + + +def test_hp8753e_measuring_parameter(): + with expected_protocol( + HP8753E, + [ + ("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), + ("S21", None), + ("S11?", "0\n"), + ("S12?", "0\n"), + ("S21?", "1\n"), + ], + ) as inst: + assert inst.MEASURING_PARAMETER_MAP == { + "A/R": "AR", + "B/R": "BR", + "A/B": "AB", + "A": "MEASA", + "B": "MEASB", + "R": "MEASR", + } + inst.measuring_parameter = "S21" + assert inst.measuring_parameter == "S21" + + +def test_hp8753e_reset(): + with expected_protocol( + HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), ("*RST", None)] + ) as inst: + inst.reset() + + +def test_hp8753e_shutdown(): + with expected_protocol( + HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), ("SOUP0", None)] + ) as inst: + inst.shutdown() + + +def test_hp8753e_averaging_restart(): + with expected_protocol( + HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), ("AVERREST", None)] + ) as inst: + inst.averaging_restart() + + +def test_hp8753e_emit_beep(): + with expected_protocol( + HP8753E, [("*IDN?", "HEWLETT PACKARD,8753E,0,7.10\n"), ("EMIB", None)] + ) as inst: + inst.emit_beep() diff --git a/tests/instruments/hp/test_hp8753e_with_device.py b/tests/instruments/hp/test_hp8753e_with_device.py new file mode 100644 index 0000000000..4a68a7c93f --- /dev/null +++ b/tests/instruments/hp/test_hp8753e_with_device.py @@ -0,0 +1,349 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import logging +from time import sleep + +import numpy +import pytest + +from pymeasure.adapters import PrologixAdapter +from pymeasure.instruments.hp import HP8753E + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +@pytest.fixture(scope="module") +def hp8753e(connected_device_address): + try: + # This allows a PrologixAdapter to be used for running the tests as well + # `pytest --device-address PrologixAdapter,ASRL4::INSTR,16 -k "test_hp8753e_with_device"` + + if "PrologixAdapter" not in connected_device_address: + vna = HP8753E(connected_device_address) + else: + _, prologix_address, gpib_address, *other_address_info = connected_device_address.split( + "," + ) + + prologix = PrologixAdapter( + resource_name=prologix_address, + visa_library="@py", + auto=1, + address=gpib_address, + ) + + # need to ensure `eot_enable` is set to zero otherwise you will have to read twice to + # get rid of the extra new line character + prologix.write("++eot_enable 0") + vna = HP8753E(adapter=prologix) + + except IOError: + print("Not able to connect to vna") + assert False + + yield vna + del vna + + +def test_init_HP8753E(hp8753e): + hp8753e.reset() + hp8753e.emit_beep() + + +def test_hp8753e_with_device_id(hp8753e): + assert hp8753e.id == ["HEWLETT PACKARD", "8753E", "0", "7.10"] + + +def test_hp8753e_with_device_name(hp8753e): + assert hp8753e.name == "HEWLETT PACKARD 8753E Vector Network Analyzer" + + +def test_hp8753e_with_device_fw(hp8753e): + hp8753e._fw = "" + assert hp8753e.fw == "7.10" + + +def test_hp8753e_with_device_manu(hp8753e): + hp8753e._manu = "" + assert hp8753e.manu == "HEWLETT PACKARD" + + +def test_hp8753e_with_device_model(hp8753e): + hp8753e._model = "" + assert hp8753e.model == "8753E" + + +def test_hp8753e_with_device_sn(hp8753e): + assert hp8753e.sn == "US37390178" + + +def test_hp8753e_with_device_options(hp8753e): + assert hp8753e.options == "1D5 002 006 010" + + +def test_hp8753e_with_device_sweep_time(hp8753e): + hp8753e.reset() + assert hp8753e.sweep_time == 0.175000059904 + hp8753e.sweep_time = 1 + assert hp8753e.sweep_time == 1.00000006144 + hp8753e.set_sweep_time_fastest() + assert hp8753e.sweep_time == 0 + + +def test_hp8753e_with_device_measuring_parameters(hp8753e): + for parameter in hp8753e.MEASURING_PARAMETER_MAP: + hp8753e.measuring_parameter = parameter + assert hp8753e.measuring_parameter == parameter + + assert hp8753e.MEASURING_PARAMETER_MAP == { + "S11": "S11", + "S12": "S12", + "S21": "S21", + "S22": "S22", + "A/R": "AR", + "B/R": "BR", + "A/B": "AB", + "A": "MEASA", + "B": "MEASB", + "R": "MEASR", + } + + +def test_hp8753e_with_device_scan_points(hp8753e): + for points in hp8753e.SCAN_POINT_VALUES: + hp8753e.scan_points = points + assert hp8753e.scan_points == points + + +def test_hp8753e_with_device_ifbw(hp8753e): + for allowed_bandwidth in hp8753e.ALLOWED_BANDWIDTH: + hp8753e.IFBW = allowed_bandwidth + assert hp8753e.IFBW == allowed_bandwidth + + hp8753e.IFBW = 1000 + assert hp8753e.IFBW == 1000 + + +def test_hp8753e_with_device_start_frequency(hp8753e): + hp8753e.start_frequency = 30_000.0 + assert hp8753e.start_frequency == 30_000.0 + hp8753e.start_frequency = 30_600.0 + assert hp8753e.start_frequency == 30_600.0 + hp8753e.start_frequency = 4_000_000.0 + assert hp8753e.start_frequency == 4_000_000.0 + + +def test_hp8753e_with_device_stop_frequency(hp8753e): + hp8753e.stop_frequency = 130_000.0 + assert hp8753e.stop_frequency == 130_000.0 + hp8753e.stop_frequency = 130_600.0 + assert hp8753e.stop_frequency == 130_600.0 + hp8753e.stop_frequency = 14_000_000.0 + assert hp8753e.stop_frequency == 14_000_000.0 + + +def test_hp8753e_with_device_center_and_span_frequency(hp8753e): + hp8753e.reset() + assert hp8753e.start_frequency == 30_000.0 + assert hp8753e.stop_frequency == 6_000_000_000.0 + + cent_freqs = [30_000, 120_306, 35_890_230, 385_239_000, 2_150_932_000, 5_907_600_000, 6e9] + spans = [20000, 523459, 125033000, 250_000_000, 3_000_000_000, 5e9] + + for freq in cent_freqs: + for span in spans: + if freq - span / 2 >= 30e3 and freq + span / 2 <= 6e9: + hp8753e.center_frequency = freq + hp8753e.span_frequency = span + assert hp8753e.span_frequency == span + assert hp8753e.center_frequency == freq + + +def test_hp8753e_with_device_set_single_frequency_scan(hp8753e): + for frequency in [30_000, 1_000_000, 500_000_000, 4_000_000_000, 6_000_000_000]: + hp8753e.set_single_frequency_scan(frequency) + assert hp8753e.start_frequency == frequency + assert hp8753e.stop_frequency == frequency + assert hp8753e.scan_points == 3 + + +def test_hp8753e_with_device_scan_single(hp8753e): + assert hp8753e.averaging_enabled is False + hp8753e.scan_single() + data = hp8753e.data_complex + assert isinstance(data, numpy.ndarray) + assert isinstance(data[12], numpy.complex128) + hp8753e.averaging_enabled = True + assert hp8753e.averaging_enabled is True + hp8753e.averages = 2 + assert hp8753e.averages == 2 + hp8753e.scan_single() + data = hp8753e.data_complex + assert isinstance(data, numpy.ndarray) + assert isinstance(data[12], numpy.complex128) + + +def test_hp8753e_with_device_scan(hp8753e): + hp8753e.reset() + hp8753e.averaging_enabled = False + hp8753e.scan(timeout=10) + hp8753e.averaging_enabled = True + assert hp8753e.averaging_enabled is True + hp8753e.averages = 2 + assert hp8753e.averages == 2 + hp8753e.scan(timeout=10) + data = hp8753e.data_complex + assert isinstance(data, numpy.ndarray) + assert isinstance(data[12], numpy.complex128) + + +def test_hp8753e_with_device_data_complex(hp8753e): + hp8753e.reset() + assert hp8753e.averaging_enabled is False + hp8753e.trigger_continuous = True + assert hp8753e.trigger_continuous is True + hp8753e.scan_single() + + data = hp8753e.data_complex + assert len(data) == 201 + assert isinstance(data, numpy.ndarray) + assert isinstance(data[12], numpy.complex128) + + # repeat with average of 2 enabled + hp8753e.trigger_hold = True + assert hp8753e.trigger_hold is True + hp8753e.averaging_enabled = True + assert hp8753e.averaging_enabled is True + hp8753e.averages = 4 + assert hp8753e.averages == 4 + hp8753e.averaging_restart() + hp8753e.scan(timeout=20) + + data = hp8753e.data_complex + assert len(data) == 201 + assert isinstance(data, numpy.ndarray) + assert isinstance(data[12], numpy.complex128) + + +def test_hp8753e_with_device_np_frequencies(hp8753e): + hp8753e.reset() + numpy.testing.assert_equal( + hp8753e.frequencies, numpy.arange(30e3, 6e9 + (6e9 - 30e3) / 200, (6e9 - 30e3) / 200) + ) + + +def test_hp8753e_with_device_reset(hp8753e): + frequency = 500_000_000 + hp8753e.set_single_frequency_scan(frequency) + assert hp8753e.start_frequency == frequency + assert hp8753e.stop_frequency == frequency + assert hp8753e.scan_points == 3 + hp8753e.reset() + assert hp8753e.start_frequency == 30_000 + assert hp8753e.stop_frequency == 6_000_000_000 + assert hp8753e.scan_points == 201 + + +def test_hp8753e_with_device_averaging_enabled(hp8753e): + + hp8753e.averaging_enabled = True + assert hp8753e.averaging_enabled is True + hp8753e.averaging_enabled = False + assert hp8753e.averaging_enabled is False + + +def test_hp8753e_with_device_averages(hp8753e): + + hp8753e.averaging_enabled = True + hp8753e.averages = 32 + assert hp8753e.averages == 32 + hp8753e.averages = 18 + assert hp8753e.averages == 18 + hp8753e.averages = 1 + assert hp8753e.averages == 1 + hp8753e.averaging_enabled = False + + +@pytest.mark.skip( + reason="This test will wear out the mechanical relays and should be run only as necessary" +) +def test_hp8753e_with_device_power(hp8753e): + + hp8753e.reset() + assert hp8753e.output_enabled is True + powers = [-60.14, -28.76, 5.41, 0.35] + for power in powers: + hp8753e.power = power + assert hp8753e.power == power + sleep(0.3) + + hp8753e.power = 0.0 + assert hp8753e.power == 0.0 + + # test power input < -70 + + # test power input > 10 + + hp8753e.shutdown() + + +@pytest.mark.skip( + reason="This test will wear out the mechanical relays and should be run only as necessary" +) +def test_hp8753e_with_device_output_enabled(hp8753e): + + hp8753e.reset() + assert hp8753e.power == 0 + assert hp8753e.output_enabled is True + hp8753e.output_enabled = False + assert hp8753e.output_enabled is False + hp8753e.output_enabled = True + assert hp8753e.output_enabled is True + hp8753e.shutdown() + assert hp8753e.output_enabled is False + + +def test_hp8753e_with_device_trigger(hp8753e): + hp8753e.reset() + assert hp8753e.trigger_continuous is True + + hp8753e.trigger_hold = True + assert hp8753e.trigger_hold is True + assert hp8753e.trigger_continuous is False + + hp8753e.trigger_hold = False + assert hp8753e.trigger_hold is False + assert hp8753e.trigger_continuous is True + + hp8753e.trigger_continuous = True + assert hp8753e.trigger_continuous is True + assert hp8753e.trigger_hold is False + + hp8753e.trigger_continuous = False + assert hp8753e.trigger_continuous is False + assert hp8753e.trigger_hold is True + + hp8753e.reset() From e8fc447b5e2f9da117a2dd0084cf9107e8cb2e37 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:12:49 -0500 Subject: [PATCH 152/181] initial commit of Keithley DAQ 6510 Data Acquisition/Multimeter System --- AUTHORS.txt | 3 ++- docs/api/instruments/keithley/index.rst | 1 + pymeasure/instruments/keithley/__init__.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index f848753bdf..c158bee5d4 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -81,4 +81,5 @@ Douwe den Blanken Till Zürner J. A. Wilcox Nick James Kirkby -Konrad Gralher \ No newline at end of file +Konrad Gralher +David Sun \ No newline at end of file diff --git a/docs/api/instruments/keithley/index.rst b/docs/api/instruments/keithley/index.rst index a88255fffb..44f5cf2780 100644 --- a/docs/api/instruments/keithley/index.rst +++ b/docs/api/instruments/keithley/index.rst @@ -22,3 +22,4 @@ This section contains specific documentation on the Keithley instruments that ar keithley2200 keithleyDMM6500 keithley2182 + keithleyDAQ6510 diff --git a/pymeasure/instruments/keithley/__init__.py b/pymeasure/instruments/keithley/__init__.py index c41a651302..b789aded80 100644 --- a/pymeasure/instruments/keithley/__init__.py +++ b/pymeasure/instruments/keithley/__init__.py @@ -35,3 +35,4 @@ from .keithley2200 import Keithley2200 from .keithleyDMM6500 import KeithleyDMM6500 from .keithley2182 import Keithley2182 +from .keithleyDAQ6510 import KeithleyDAQ6510 From 2a87002f610b40917decc54f47695048d0c562a0 Mon Sep 17 00:00:00 2001 From: bernhardlang <82966268+bernhardlang@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:25:57 +0200 Subject: [PATCH 153/181] Update docs/dev/contribute.rst Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> --- docs/dev/contribute.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/contribute.rst b/docs/dev/contribute.rst index f87934f25d..3ac07e6ee5 100644 --- a/docs/dev/contribute.rst +++ b/docs/dev/contribute.rst @@ -45,7 +45,9 @@ This will allow you to edit the files of PyMeasure and see the changes reflected Depending on your Python installation you may get an error messages saying that the file setup.py is missing or similar. Updating pip may solve the problem .. code-block:: bash + python -m pip install pip --upgrade + Working on a new feature ======================== From c9f1d01a517a1cfed8b369d16e31ccf7439d3320 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 11 Jun 2024 22:46:00 +0200 Subject: [PATCH 154/181] Fix the encoding of the writen datafiles to utf-8 --- pymeasure/experiment/results.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pymeasure/experiment/results.py b/pymeasure/experiment/results.py index 183a609437..55a3913170 100644 --- a/pymeasure/experiment/results.py +++ b/pymeasure/experiment/results.py @@ -207,6 +207,7 @@ class Results: DELIMITER = ',' LINE_BREAK = "\n" CHUNK_SIZE = 1000 + ENCODING = "utf-8" def __init__(self, procedure, data_filename): if not isinstance(procedure, Procedure): @@ -233,7 +234,7 @@ def __init__(self, procedure, data_filename): # TODO: Correctly store and retrieve status else: for filename in self.data_filenames: - with open(filename, 'w') as f: + with open(filename, 'w', encoding=Results.ENCODING) as f: f.write(self.header()) f.write(self.labels()) self._data = None @@ -329,7 +330,7 @@ def store_metadata(self): return for filename in self.data_filenames: - with open(filename, 'r+') as f: + with open(filename, 'r+', encoding=Results.ENCODING) as f: contents = f.readlines() contents.insert(self._header_count - 1, c_header) @@ -414,7 +415,7 @@ def load(data_filename, procedure_class=None): header = "" header_read = False header_count = 0 - with open(data_filename) as f: + with open(data_filename, "r", encoding=Results.ENCODING) as f: while not header_read: line = f.readline() if line.startswith(Results.COMMENT): From ed6a4ba42d04baf088cf427ca542083092499ccf Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 11 Jun 2024 23:35:53 +0200 Subject: [PATCH 155/181] Add change to changes.rst --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 575ec65967..d2554c691f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +Upcoming version +================ + + +Automation +---------- +- Explicitly set encoding when writing and reading data to file, allowing the use of special characters (@CasperSchippers, #1123). Version 0.14.0 (2024-05-22) =========================== From fa0b5a63a0119f6dc7cabe62a4464ae099bbe488 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Wed, 12 Jun 2024 22:23:05 +0200 Subject: [PATCH 156/181] Update CHANGES.rst Co-authored-by: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d2554c691f..1ca1df9a7e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ Upcoming version Automation ---------- -- Explicitly set encoding when writing and reading data to file, allowing the use of special characters (@CasperSchippers, #1123). +- Explicitly set encoding to utf8 when writing and reading data to file, allowing the use of special characters (@CasperSchippers, #1123). Version 0.14.0 (2024-05-22) =========================== From a2a60a59f15e6f25490104b34f3b90d012f72063 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Mon, 17 Jun 2024 17:40:57 +0200 Subject: [PATCH 157/181] Update pymeasure_numpy2.yml --- .github/pymeasure_numpy2.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/pymeasure_numpy2.yml b/.github/pymeasure_numpy2.yml index 8a73797f5c..b5fc4b9ee1 100644 --- a/.github/pymeasure_numpy2.yml +++ b/.github/pymeasure_numpy2.yml @@ -3,8 +3,9 @@ channels: - conda-forge dependencies: - cloudpickle=1.6.0 -# - pandas>=2.2.2 # The version on conda-forge seems not be built with numpy==2, but the pip version has been -# - pint=0.18 # A pint version that has not yet been released (>0.23) is required to work with numpy2 + - numpy=2.0.0 + - pandas=2.2.2 + - pint=0.24 - pyqt=5.15.7 - pyqtgraph=0.12.4 - pyserial=3.4 @@ -22,8 +23,4 @@ dependencies: - sphinx=5.3.0 - sphinx_rtd_theme=1.2.2 # pip is currently not needed, but the recommended tool when packages that are unavailable on conda are required - - pip # don't pin, to gain newest conda compatibility fixes - - pip: - - numpy==2.0.0rc1 - - pandas==2.2.2 - - git+https://github.com/hgrecco/pint.git#egg=pint +# - pip # don't pin, to gain newest conda compatibility fixes From f36bf81b9247180438d6ba455a4f4d3f02e9f57c Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Thu, 20 Jun 2024 15:53:14 -0500 Subject: [PATCH 158/181] Added new reStructuredText file for the Keithley DAQ6510 --- docs/api/instruments/keithley/keithleyDAQ6510.rst | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/api/instruments/keithley/keithleyDAQ6510.rst diff --git a/docs/api/instruments/keithley/keithleyDAQ6510.rst b/docs/api/instruments/keithley/keithleyDAQ6510.rst new file mode 100644 index 0000000000..aab0843d0b --- /dev/null +++ b/docs/api/instruments/keithley/keithleyDAQ6510.rst @@ -0,0 +1,8 @@ +########################################################### +Keithley DAQ6510 Data Acquisition Logging Multimeter System +########################################################### + +.. autoclass:: pymeasure.instruments.keithley.KeithleyDAQ6510 + :members: + :show-inheritance: + :inherited-members: CommonBase \ No newline at end of file From b091cfc7c7de2e98e06c23ac04a50b95a2d40171 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Thu, 20 Jun 2024 15:57:51 -0500 Subject: [PATCH 159/181] bare-bones instrument file for the Keithley DAQ6510 Data Acquisition and Logging Multimeter System --- .../instruments/keithley/keithleyDAQ6510.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 pymeasure/instruments/keithley/keithleyDAQ6510.py diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py new file mode 100644 index 0000000000..6c7b81d238 --- /dev/null +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -0,0 +1,103 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.instruments import Instrument +from pymeasure.instruments.generic_types import SCPIMixin +from .buffer import KeithleyBuffer + + +class KeithleyDAQ6510(KeithleyBuffer, SCPIMixin, Instrument): + """ Represents the Keithley DAQ6510 Data Acquisition Logging Multimeter System + and provides a high-level interface for interacting with the instrument. + + .. code-block:: python + + keithley = KeithleyDAQ6510("GPIB::1") + keithley = KeithleyDAQ6510("TCPIP::192.168.1.1::INSTR") + + """ + + def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): + super().__init__( + adapter, + name, + **kwargs + ) + + open_channel = Instrument.setting( + ":ROUT:OPEN (@%d)", + """Set a single channel to open.""" + ) + + close_channel = Instrument.setting( + ":ROUT:CLOS (@%d)", + """Set a single channel to closed.""" + ) + + def no_errors(self): + """ + Check to see if the instrument has any errors returned or not. + + :return: ``True`` if there are no errors, ``False`` if there are errors. + """ + return len(self.check_errors() == 0) + + def use_mux(self): + """ + Enables MUX switching. Forces open channels 134 and 135 for isolation and + closes channel 133 to separate MUX1 and MUX2. + + :return: ``True`` if the channels were opened and closed successfully. + """ + self.open_channels([134, 135]) + self.close_channels([133]) + return self.ask(":ROUT:CLOS?") == "(@133)\n" + + def open_channels(self, channel_list): + """ + Configures multiple channels to be open. + + :param channel_list: List of channels to be set to open. + """ + for channel in channel_list: + self.open_channel = channel + + def close_channels(self, channel_list): + """ + Configures multiple channels to be closed. + + :param channel_list: List of channels to be set to closed. + """ + for channel in channel_list: + self.close_channel = channel + + def beep(self, frequency, duration): + """ + Sound a system beep. + + :param frequency: A frequency in Hz from 20 and 8000 Hz + :param duration: The amount of time to play the tone, between 0.001 s to 100 s + :return: None + """ + self.write(f":SYST:BEEP {frequency:g}, {duration:g}") From 7df9e660f0e47d5c0474f9d35ce08d355a2d600a Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:43:10 -0500 Subject: [PATCH 160/181] added basic test with device --- .../test_keithleyDAQ6510_with_device.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/instruments/keithley/test_keithleyDAQ6510_with_device.py diff --git a/tests/instruments/keithley/test_keithleyDAQ6510_with_device.py b/tests/instruments/keithley/test_keithleyDAQ6510_with_device.py new file mode 100644 index 0000000000..2aa34176c9 --- /dev/null +++ b/tests/instruments/keithley/test_keithleyDAQ6510_with_device.py @@ -0,0 +1,61 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest +import logging +from pymeasure.instruments.keithley import KeithleyDAQ6510 + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + + +@pytest.fixture(scope="module") +def daq6510(connected_device_address): + instr = KeithleyDAQ6510(connected_device_address) + instr.adapter.connection.timeout = 10000 + return instr + + +@pytest.fixture +def reset_daq(daq6510): + daq6510.clear() + daq6510.reset() + return daq6510 + + +def test_correct_model_by_idn(reset_daq): + assert "6510" in reset_daq.id.lower() + + +def test_beep(reset_daq): + reset_daq.beep(440, 0.1) + assert len(reset_daq.check_errors()) == 0 + + +def test_mux(reset_daq): + assert reset_daq.use_mux() + + +def test_rear(reset_daq): + assert reset_daq.ask(":ROUT:TERM?") == "REAR\n" From 55e8132a3ecc0ecb983115644ff878a4e4e6863c Mon Sep 17 00:00:00 2001 From: bernhardlang <82966268+bernhardlang@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:25:50 +0200 Subject: [PATCH 161/181] Update AUTHORS.txt --- AUTHORS.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index df73ecaed6..f3ac520bcb 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -65,4 +65,5 @@ Robert Roos Sebastien Weber Sebastian Neusch Ulrich Sauter -Guus Kuiper \ No newline at end of file +Guus Kuiper +Bernhard Lang From 670643e7e06d7d9b496806fc4ee0c518fd255142 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:41:26 -0500 Subject: [PATCH 162/181] changed open_ and close_channel to be methods rather than properties --- .../instruments/keithley/keithleyDAQ6510.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 6c7b81d238..3ba3b1de64 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -45,16 +45,6 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): **kwargs ) - open_channel = Instrument.setting( - ":ROUT:OPEN (@%d)", - """Set a single channel to open.""" - ) - - close_channel = Instrument.setting( - ":ROUT:CLOS (@%d)", - """Set a single channel to closed.""" - ) - def no_errors(self): """ Check to see if the instrument has any errors returned or not. @@ -74,6 +64,22 @@ def use_mux(self): self.close_channels([133]) return self.ask(":ROUT:CLOS?") == "(@133)\n" + def open_channel(self, channel): + """ + Set a single channel to open. + + :param channel: Channel to be set to open. + """ + self.write(f":ROUT:OPEN (@{channel})") + + def close_channel(self, channel): + """ + Set a single channel to closed. + + :param channel: Channel to be set to closed. + """ + self.write(f":ROUT:CLOS (@{channel})") + def open_channels(self, channel_list): """ Configures multiple channels to be open. From 73e91238d46d697d4eb3f31b2c2dcd62dba89641 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:54:48 -0500 Subject: [PATCH 163/181] change references to old property open_ and close_channel --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 3ba3b1de64..2872c62baa 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -87,7 +87,7 @@ def open_channels(self, channel_list): :param channel_list: List of channels to be set to open. """ for channel in channel_list: - self.open_channel = channel + self.open_channel(channel) def close_channels(self, channel_list): """ @@ -96,7 +96,7 @@ def close_channels(self, channel_list): :param channel_list: List of channels to be set to closed. """ for channel in channel_list: - self.close_channel = channel + self.close_channel(channel) def beep(self, frequency, duration): """ From f13c3d202f93aef09fff56bb96d48c3216eb5069 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:54:48 -0500 Subject: [PATCH 164/181] change references to old properties open_ and close_channel --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 3ba3b1de64..2872c62baa 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -87,7 +87,7 @@ def open_channels(self, channel_list): :param channel_list: List of channels to be set to open. """ for channel in channel_list: - self.open_channel = channel + self.open_channel(channel) def close_channels(self, channel_list): """ @@ -96,7 +96,7 @@ def close_channels(self, channel_list): :param channel_list: List of channels to be set to closed. """ for channel in channel_list: - self.close_channel = channel + self.close_channel(channel) def beep(self, frequency, duration): """ From 67de1d9b40288255113d90b46fb04f8d961c0d02 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:45:49 -0500 Subject: [PATCH 165/181] added properties and methods to measure voltage, current, and resistance. --- .../instruments/keithley/keithleyDAQ6510.py | 184 ++++++++++++++++-- 1 file changed, 173 insertions(+), 11 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 2872c62baa..6b4ba184dd 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -24,7 +24,13 @@ from pymeasure.instruments import Instrument from pymeasure.instruments.generic_types import SCPIMixin +from pymeasure.instruments.validators import strict_discrete_set, truncated_range from .buffer import KeithleyBuffer +import logging + +# Set up logging +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) class KeithleyDAQ6510(KeithleyBuffer, SCPIMixin, Instrument): @@ -36,8 +42,129 @@ class KeithleyDAQ6510(KeithleyBuffer, SCPIMixin, Instrument): keithley = KeithleyDAQ6510("GPIB::1") keithley = KeithleyDAQ6510("TCPIP::192.168.1.1::INSTR") + keithley.apply_current() # Sets up to source current + """ + sense_mode = Instrument.control( + ":SENS:FUNC?", ":SENS:FUNC %s", + """ A string property that controls the reading mode, which can + take the values 'current' or 'voltage'. The convenience methods + :meth:`~.KeithleyDAQ6510.sense_current` and :meth:`~.KeithleyDAQ6510.sense_voltage` + can also be used. """, + validator=strict_discrete_set, + values={'current': 'CURR', 'voltage': 'VOLT'}, + map_values=True + ) + + ############### + # Current (A) # + ############### + + current = Instrument.measurement( + ":READ?", + """ Reads the current in Amps, if configured for this reading. + """ + ) + + current_range = Instrument.control( + ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", + """ A floating point property that controls the measurement current + range in Amps. If the measurement function is DC current, available ranges are 10E-6 A to 3A. + If the measurement function is AC current, available ranges are 100E-6 to 3A. + Auto-range is disabled when this property is set. """, + validator=truncated_range, + values=[10E-6, 3] + ) + + current_nplc = Instrument.control( + ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", + """ A floating point property that controls the number of power line cycles + (NPLC) for the DC current measurements, which sets the integration period + and measurement speed. Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + The smallest value is the shortest time, and results in the fastest reading rate, + but increases the reading noise and decreases the number of usable digits. + The largest value is the longest time, and results in the lowest reading rate, + but increases the number of usable digits. """ + ) + + ############### + # Voltage (V) # + ############### + + voltage = Instrument.measurement( + ":READ?", + """ Reads the voltage in Volts, if configured for this reading. + """ + ) + + voltage_range = Instrument.control( + ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", + """ A floating point property that controls the measurement voltage + range in Volts. If the measurement function is DC voltage, available ranges are 100E-3 V to 1000 V. + If the measurement function is AC voltage, available ranges are 100E-3 to 750 V. + Auto-range is disabled when this property is set. """, + validator=truncated_range, + values=[100E-3, 1000] + ) + + voltage_nplc = Instrument.control( + ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", + """ A floating point property that controls the number of power line cycles + (NPLC) for the DC voltage measurements, which sets the integration period + and measurement speed. Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + The smallest value is the shortest time, and results in the fastest reading rate, + but increases the reading noise and decreases the number of usable digits. + The largest value is the longest time, and results in the lowest reading rate, + but increases the number of usable digits. """ + ) + + #################### + # Resistance (Ohm) # + #################### + + resistance = Instrument.measurement( + ":READ?", + """ Reads the resistance in Ohms, if configured for this reading. + """ + ) + + resistance_range = Instrument.control( + ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", + """ A floating point property that controls the resistance range + in Ohms. If the measurement function is 2-wire resistance, the available ranges are 10 to 100E6 Ohms. + If the measurement function is 4-wire resistance with offset compensation off, + the available ranges are 1 to 100E6 Ohms. If the measurement function is 4-wire resistance + with offset compensation on, the available ranges are 1 to 10E3 Ohms. + Auto-range is disabled when this property is set. """, + validator=truncated_range, + values=[1, 100E6] + ) + + offset_compensated = Instrument.control( + ":SENS:RES:OCOM?", ":SENS:RES:OCOM %s", + """ A string property that determines if offset compensation is used or not. + Valid values are OFF, ON, and AUTO. """, + validator=strict_discrete_set, + values=["OFF", "ON", "AUTO"], + map_values=False + ) + + resistance_nplc = Instrument.control( + ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", + """ A floating point property that controls the number of power line cycles + (NPLC) for the 2-wire resistance measurements, which sets the integration period + and measurement speed. Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + The smallest value is the shortest time, and results in the fastest reading rate, + but increases the reading noise and decreases the number of usable digits. + The largest value is the longest time, and results in the lowest reading rate, + but increases the number of usable digits. """ + ) + + #################### + # Methods # + #################### + def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): super().__init__( adapter, @@ -45,24 +172,59 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): **kwargs ) - def no_errors(self): + def measure_resistance(self, nplc=1, resistance=100e6, auto_range=True): + """ Configures the measurement of resistance. + + :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + :param resistance: Upper limit of resistance in Ohms, from 10 to 100E6 (2-wire), + 1 to 100E6 (4-wire with OCOM off), or 1 to 10E3 (4-wire with OCOM on). + :param auto_range: A boolean value to enable auto_range if ``True``, else uses the set resistance. """ - Check to see if the instrument has any errors returned or not. + log.info(f"{self.name} is measuring resistance.") + self.write(f":SENS:FUNC \"RES\";:SENS:RES:NPLC {nplc};") + if auto_range: + self.write(":SENS:RES:RANG:AUTO ON;") + else: + self.resistance_range = resistance + self.check_errors() - :return: ``True`` if there are no errors, ``False`` if there are errors. + def measure_voltage(self, nplc=1, voltage=1000, auto_range=True): + """ Configures the measurement of voltage. + + :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + :param voltage: Upper limit of voltage in Volts, from 100E-3 to 1000 V (DC) or 100E-3 to 750 V (AC). + :param auto_range: A boolean value to enable auto_range if ``True``, else uses the set resistance. """ - return len(self.check_errors() == 0) + log.info(f"{self.name} is measuring voltage.") + self.write(f":SENS:FUNC \"VOLT\";:SENS:VOLT:NPLC {nplc};") + if auto_range: + self.write(":SENS:VOLT:RANG:AUTO ON;") + else: + self.voltage_range = voltage + self.check_errors() + + def measure_current(self, nplc=1, current=3, auto_range=True): + """ Configures the measurement of voltage. + + :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + :param current: Upper limit of current in Amps, from 10E-6 to 3 A (DC) or 100E-6 to 3A (AC). + :param auto_range: A boolean value to enable auto_range if ``True``, else uses the set resistance. + """ + log.info(f"{self.name} is measuring voltage.") + self.write(f":SENS:FUNC \"CURR\";:SENS:CURR:NPLC {nplc};") + if auto_range: + self.write(":SENS:CURR:RANG:AUTO ON;") + else: + self.current_range = current + self.check_errors() - def use_mux(self): + def no_errors(self): """ - Enables MUX switching. Forces open channels 134 and 135 for isolation and - closes channel 133 to separate MUX1 and MUX2. + Check to see if the instrument has any errors returned or not. - :return: ``True`` if the channels were opened and closed successfully. + :return: ``True`` if there are no errors, ``False`` if there are errors. """ - self.open_channels([134, 135]) - self.close_channels([133]) - return self.ask(":ROUT:CLOS?") == "(@133)\n" + return len(self.check_errors() == 0) def open_channel(self, channel): """ From eade89ab21e316443e574de4ba09cbc304c30c90 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:38:26 -0500 Subject: [PATCH 166/181] update docstring at beginning --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 6b4ba184dd..30a58b2d84 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -42,7 +42,17 @@ class KeithleyDAQ6510(KeithleyBuffer, SCPIMixin, Instrument): keithley = KeithleyDAQ6510("GPIB::1") keithley = KeithleyDAQ6510("TCPIP::192.168.1.1::INSTR") - keithley.apply_current() # Sets up to source current + print(keithley.current) # Prints the current in Amps + keithley.current_range = 10E-6 # Select the 10 uA range + + print(keithley.voltage) # Prints the voltage in Volts + keithley.voltage_range = 100e-3 # Select the 100 mV range + + print(keithley.resistance) # Prints the resistance in Ohms + keithley.offset_compensated = "ON" # Turns offset-compensated ohms on + + keithley.open_channels([134, 135]) # Open channels 134 and 135 on the MUX card + keithley.close_channel(133) # Close channel 133 on the MUX card """ From cbd7ce45a0bb8b249ded80d5610b9d3d9ce1679e Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:39:10 -0500 Subject: [PATCH 167/181] move __init__ to beginning --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 30a58b2d84..bbe0dd4bc6 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -56,6 +56,13 @@ class KeithleyDAQ6510(KeithleyBuffer, SCPIMixin, Instrument): """ + def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): + super().__init__( + adapter, + name, + **kwargs + ) + sense_mode = Instrument.control( ":SENS:FUNC?", ":SENS:FUNC %s", """ A string property that controls the reading mode, which can @@ -175,13 +182,6 @@ class KeithleyDAQ6510(KeithleyBuffer, SCPIMixin, Instrument): # Methods # #################### - def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): - super().__init__( - adapter, - name, - **kwargs - ) - def measure_resistance(self, nplc=1, resistance=100e6, auto_range=True): """ Configures the measurement of resistance. From 0395fa037a6c2d85d8982297a83630eee1d4c5e6 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:39:50 -0500 Subject: [PATCH 168/181] fix incorrect SCPI command --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index bbe0dd4bc6..434c3193ff 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -159,7 +159,7 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): ) offset_compensated = Instrument.control( - ":SENS:RES:OCOM?", ":SENS:RES:OCOM %s", + ":SENS:FRES:OCOM?", ":SENS:FRES:OCOM %s", """ A string property that determines if offset compensation is used or not. Valid values are OFF, ON, and AUTO. """, validator=strict_discrete_set, From 29f353824cf03d666f722a7b66974fc19f4509c4 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:05:44 -0500 Subject: [PATCH 169/181] fixed lines that were too long, also fixed some typos where text was copied and certain words were not changed (e.g. resistance -> voltage) --- .../instruments/keithley/keithleyDAQ6510.py | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 434c3193ff..3fb82c395f 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -87,8 +87,9 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", """ A floating point property that controls the measurement current - range in Amps. If the measurement function is DC current, available ranges are 10E-6 A to 3A. - If the measurement function is AC current, available ranges are 100E-6 to 3A. + range in Amps. If the measurement function is DC current, + available ranges are 10E-6 A to 3A. If the measurement function is AC current, + available ranges are 100E-6 to 3A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[10E-6, 3] @@ -118,8 +119,9 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", """ A floating point property that controls the measurement voltage - range in Volts. If the measurement function is DC voltage, available ranges are 100E-3 V to 1000 V. - If the measurement function is AC voltage, available ranges are 100E-3 to 750 V. + range in Volts. If the measurement function is DC voltage, + available ranges are 100E-3 V to 1000 V. If the measurement function is AC voltage, + available ranges are 100E-3 to 750 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[100E-3, 1000] @@ -149,9 +151,10 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", """ A floating point property that controls the resistance range - in Ohms. If the measurement function is 2-wire resistance, the available ranges are 10 to 100E6 Ohms. - If the measurement function is 4-wire resistance with offset compensation off, - the available ranges are 1 to 100E6 Ohms. If the measurement function is 4-wire resistance + in Ohms. If the measurement function is 2-wire resistance, + the available ranges are 10 to 100E6 Ohms. If the measurement function is + 4-wire resistance with offset compensation off, the available ranges + are 1 to 100E6 Ohms. If the measurement function is 4-wire resistance with offset compensation on, the available ranges are 1 to 10E3 Ohms. Auto-range is disabled when this property is set. """, validator=truncated_range, @@ -185,10 +188,12 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): def measure_resistance(self, nplc=1, resistance=100e6, auto_range=True): """ Configures the measurement of resistance. - :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) + or 12 (50 Hz or 400 Hz). :param resistance: Upper limit of resistance in Ohms, from 10 to 100E6 (2-wire), - 1 to 100E6 (4-wire with OCOM off), or 1 to 10E3 (4-wire with OCOM on). - :param auto_range: A boolean value to enable auto_range if ``True``, else uses the set resistance. + 1 to 100E6 (4-wire with OCOM off), or 1 to 10E3 (4-wire with OCOM on). + :param auto_range: A boolean value to enable auto_range if ``True``, + else uses the set resistance. """ log.info(f"{self.name} is measuring resistance.") self.write(f":SENS:FUNC \"RES\";:SENS:RES:NPLC {nplc};") @@ -201,9 +206,12 @@ def measure_resistance(self, nplc=1, resistance=100e6, auto_range=True): def measure_voltage(self, nplc=1, voltage=1000, auto_range=True): """ Configures the measurement of voltage. - :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). - :param voltage: Upper limit of voltage in Volts, from 100E-3 to 1000 V (DC) or 100E-3 to 750 V (AC). - :param auto_range: A boolean value to enable auto_range if ``True``, else uses the set resistance. + :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) + or 12 (50 Hz or 400 Hz). + :param voltage: Upper limit of voltage in Volts, from 100E-3 to 1000 V (DC) + or 100E-3 to 750 V (AC). + :param auto_range: A boolean value to enable auto_range if ``True``, + else uses the set voltage. """ log.info(f"{self.name} is measuring voltage.") self.write(f":SENS:FUNC \"VOLT\";:SENS:VOLT:NPLC {nplc};") @@ -214,13 +222,16 @@ def measure_voltage(self, nplc=1, voltage=1000, auto_range=True): self.check_errors() def measure_current(self, nplc=1, current=3, auto_range=True): - """ Configures the measurement of voltage. - - :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). - :param current: Upper limit of current in Amps, from 10E-6 to 3 A (DC) or 100E-6 to 3A (AC). - :param auto_range: A boolean value to enable auto_range if ``True``, else uses the set resistance. + """ Configures the measurement of current. + + :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) + or 12 (50 Hz or 400 Hz). + :param current: Upper limit of current in Amps, from 10E-6 to 3 A (DC) + or 100E-6 to 3A (AC). + :param auto_range: A boolean value to enable auto_range if ``True``, + else uses the set current. """ - log.info(f"{self.name} is measuring voltage.") + log.info(f"{self.name} is measuring current.") self.write(f":SENS:FUNC \"CURR\";:SENS:CURR:NPLC {nplc};") if auto_range: self.write(":SENS:CURR:RANG:AUTO ON;") From 203848bcfa99746be04b69f27080753f8ea87e93 Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 25 Jun 2024 22:15:08 +0200 Subject: [PATCH 170/181] Found 2 more places where then encoding should be enforced (reading using pandas) --- pymeasure/experiment/results.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pymeasure/experiment/results.py b/pymeasure/experiment/results.py index 55a3913170..ea374e03bf 100644 --- a/pymeasure/experiment/results.py +++ b/pymeasure/experiment/results.py @@ -448,7 +448,10 @@ def data(self): comment=Results.COMMENT, header=0, names=self._data.columns, - chunksize=Results.CHUNK_SIZE, skiprows=skiprows, iterator=True + chunksize=Results.CHUNK_SIZE, + skiprows=skiprows, + iterator=True, + encoding=Results.ENCODING, ) try: tmp_frame = pd.concat(chunks, ignore_index=True) @@ -471,7 +474,8 @@ def reload(self): self.data_filename, comment=Results.COMMENT, chunksize=Results.CHUNK_SIZE, - iterator=True + iterator=True, + encoding=Results.ENCODING, ) try: self._data = pd.concat(chunks, ignore_index=True) From 8a125016f311d41d05a8e50fc1af3b12e137906a Mon Sep 17 00:00:00 2001 From: Casper Schippers Date: Tue, 25 Jun 2024 22:18:03 +0200 Subject: [PATCH 171/181] Update CHANGES.rst --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1ca1df9a7e..e54d0cbf53 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,8 @@ Upcoming version Automation ---------- -- Explicitly set encoding to utf8 when writing and reading data to file, allowing the use of special characters (@CasperSchippers, #1123). +- Explicitly set encoding to utf8 when writing and reading data to file, allowing the use of special characters. + Previously the encoding was not explicitly set, this could potentially disrupt loading old data-files; if this is required, the encoading can be changed by changing (e.g., monkey-patching) the :code:`pymeasure.experiment.Results.ENCODING` property. (@CasperSchippers, #1123) Version 0.14.0 (2024-05-22) =========================== From 9f0c508d3d47ec73dd82b17256f11600ddf43ca6 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:04:08 -0500 Subject: [PATCH 172/181] update property docstrings to properly indicate property type --- .../instruments/keithley/keithleyDAQ6510.py | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 3fb82c395f..6d1b28e9bb 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -64,11 +64,10 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): ) sense_mode = Instrument.control( - ":SENS:FUNC?", ":SENS:FUNC %s", - """ A string property that controls the reading mode, which can - take the values 'current' or 'voltage'. The convenience methods - :meth:`~.KeithleyDAQ6510.sense_current` and :meth:`~.KeithleyDAQ6510.sense_voltage` - can also be used. """, + ":SENS:FUNC?", ":SENS:FUNC \"%s\"", + """ Control the reading mode, which can take the values 'current' or 'voltage'. + The convenience methods :meth:`~.KeithleyDAQ6510.sense_current` and + :meth:`~.KeithleyDAQ6510.sense_voltage` can also be used. """, validator=strict_discrete_set, values={'current': 'CURR', 'voltage': 'VOLT'}, map_values=True @@ -80,15 +79,14 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): current = Instrument.measurement( ":READ?", - """ Reads the current in Amps, if configured for this reading. + """ Measure the current in Amps, if configured for this reading. """ ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", - """ A floating point property that controls the measurement current - range in Amps. If the measurement function is DC current, - available ranges are 10E-6 A to 3A. If the measurement function is AC current, + """ Control the measurement current range in Amps. If the measurement function is DC, + available ranges are 10E-6 A to 3A. If the measurement function is AC, available ranges are 100E-6 to 3A. Auto-range is disabled when this property is set. """, validator=truncated_range, @@ -97,13 +95,12 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): current_nplc = Instrument.control( ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", - """ A floating point property that controls the number of power line cycles - (NPLC) for the DC current measurements, which sets the integration period - and measurement speed. Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). - The smallest value is the shortest time, and results in the fastest reading rate, - but increases the reading noise and decreases the number of usable digits. - The largest value is the longest time, and results in the lowest reading rate, - but increases the number of usable digits. """ + """ Control the number of power line cycles (NPLC) for the DC current measurements, + which sets the integration period and measurement speed. + Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). The smallest value is the + shortest time, and results in the fastest reading rate, but increases the reading noise + and decreases the number of usable digits. The largest value is the longest time, and + results in the lowest reading rate, but increases the number of usable digits. """ ) ############### @@ -112,26 +109,24 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): voltage = Instrument.measurement( ":READ?", - """ Reads the voltage in Volts, if configured for this reading. + """ Measure the voltage in Volts, if configured for this reading. """ ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", - """ A floating point property that controls the measurement voltage - range in Volts. If the measurement function is DC voltage, - available ranges are 100E-3 V to 1000 V. If the measurement function is AC voltage, - available ranges are 100E-3 to 750 V. - Auto-range is disabled when this property is set. """, + """ Control the measurement voltage range in Volts. If the measurement function is DC, + available ranges are 100E-3 V to 1000 V. If the measurement function is AC, available + ranges are 100E-3 to 750 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[100E-3, 1000] ) voltage_nplc = Instrument.control( ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", - """ A floating point property that controls the number of power line cycles - (NPLC) for the DC voltage measurements, which sets the integration period - and measurement speed. Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + """ Control the number of power line cycles (NPLC) for the DC voltage measurements, + which sets the integration period and measurement speed. + Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). The smallest value is the shortest time, and results in the fastest reading rate, but increases the reading noise and decreases the number of usable digits. The largest value is the longest time, and results in the lowest reading rate, @@ -144,14 +139,13 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): resistance = Instrument.measurement( ":READ?", - """ Reads the resistance in Ohms, if configured for this reading. + """ Measure the resistance in Ohms, if configured for this reading. """ ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", - """ A floating point property that controls the resistance range - in Ohms. If the measurement function is 2-wire resistance, + """ Control the resistance range in Ohms. If the measurement function is 2-wire, the available ranges are 10 to 100E6 Ohms. If the measurement function is 4-wire resistance with offset compensation off, the available ranges are 1 to 100E6 Ohms. If the measurement function is 4-wire resistance @@ -163,7 +157,7 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): offset_compensated = Instrument.control( ":SENS:FRES:OCOM?", ":SENS:FRES:OCOM %s", - """ A string property that determines if offset compensation is used or not. + """ Control if offset compensation is used or not. Valid values are OFF, ON, and AUTO. """, validator=strict_discrete_set, values=["OFF", "ON", "AUTO"], @@ -172,9 +166,9 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): resistance_nplc = Instrument.control( ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", - """ A floating point property that controls the number of power line cycles - (NPLC) for the 2-wire resistance measurements, which sets the integration period - and measurement speed. Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). + """ Control the number of power line cycles (NPLC) for the 2-wire resistance measurements, + which sets the integration period and measurement speed. + Takes values from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). The smallest value is the shortest time, and results in the fastest reading rate, but increases the reading noise and decreases the number of usable digits. The largest value is the longest time, and results in the lowest reading rate, From ebbecdfc3ccd82320165e3e73aa7f626a7011a4e Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:58:16 -0500 Subject: [PATCH 173/181] change some docstrings to imperative --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 6d1b28e9bb..86dee8ce50 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -180,7 +180,7 @@ def __init__(self, adapter, name="Keithley DAQ6510", **kwargs): #################### def measure_resistance(self, nplc=1, resistance=100e6, auto_range=True): - """ Configures the measurement of resistance. + """ Configure the measurement of resistance. :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). @@ -198,7 +198,7 @@ def measure_resistance(self, nplc=1, resistance=100e6, auto_range=True): self.check_errors() def measure_voltage(self, nplc=1, voltage=1000, auto_range=True): - """ Configures the measurement of voltage. + """ Configure the measurement of voltage. :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). @@ -216,7 +216,7 @@ def measure_voltage(self, nplc=1, voltage=1000, auto_range=True): self.check_errors() def measure_current(self, nplc=1, current=3, auto_range=True): - """ Configures the measurement of current. + """ Configure the measurement of current. :param nplc: Number of power line cycles (NPLC) from 5E-4 to 15 (60 Hz) or 12 (50 Hz or 400 Hz). @@ -259,7 +259,7 @@ def close_channel(self, channel): def open_channels(self, channel_list): """ - Configures multiple channels to be open. + Configure multiple channels to be open. :param channel_list: List of channels to be set to open. """ @@ -268,7 +268,7 @@ def open_channels(self, channel_list): def close_channels(self, channel_list): """ - Configures multiple channels to be closed. + Configure multiple channels to be closed. :param channel_list: List of channels to be set to closed. """ From b7ec6d6dc30d458fe7d0807499c65a459a70076e Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:01:41 -0500 Subject: [PATCH 174/181] remove `no_errors()` method --- pymeasure/instruments/keithley/keithleyDAQ6510.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pymeasure/instruments/keithley/keithleyDAQ6510.py b/pymeasure/instruments/keithley/keithleyDAQ6510.py index 86dee8ce50..c80c0b42f8 100644 --- a/pymeasure/instruments/keithley/keithleyDAQ6510.py +++ b/pymeasure/instruments/keithley/keithleyDAQ6510.py @@ -233,14 +233,6 @@ def measure_current(self, nplc=1, current=3, auto_range=True): self.current_range = current self.check_errors() - def no_errors(self): - """ - Check to see if the instrument has any errors returned or not. - - :return: ``True`` if there are no errors, ``False`` if there are errors. - """ - return len(self.check_errors() == 0) - def open_channel(self, channel): """ Set a single channel to open. From b472518cf0089c39d14ec00c567df1758eb32924 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:47:19 -0500 Subject: [PATCH 175/181] created protocol test for the keithleyDAQ6510 --- .../keithley/test_keithleyDAQ6510.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/instruments/keithley/test_keithleyDAQ6510.py diff --git a/tests/instruments/keithley/test_keithleyDAQ6510.py b/tests/instruments/keithley/test_keithleyDAQ6510.py new file mode 100644 index 0000000000..bfdb103359 --- /dev/null +++ b/tests/instruments/keithley/test_keithleyDAQ6510.py @@ -0,0 +1,90 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2024 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest +import logging +from pymeasure.test import expected_protocol +from pymeasure.instruments.keithley import KeithleyDAQ6510 + + +def test_init(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None)], + ): + pass # Verify the expected communication. + + +def test_current_range_set(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None), + (b':SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG 1', None)], + ) as inst: + inst.current_range = 1.0 + + +def test_current_range_get(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None), + (b':SENS:CURR:RANG?', b'1.0\n')], + ) as inst: + assert inst.current_range == 1.0 + + +def test_current_nplc_set(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None), + (b':SENS:CURR:NPLC 2', None)], + ) as inst: + inst.current_nplc = 2 + + +def test_current_nplc_get(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None), + (b':SENS:CURR:NPLC?', b'2\n')], + ) as inst: + assert inst.current_nplc == 2.0 + + +def test_id(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None), + (b'*IDN?', b'KEITHLEY INSTRUMENTS,MODEL DAQ6510,04591126,1.7.12b\n')], + ) as inst: + assert inst.id == 'KEITHLEY INSTRUMENTS,MODEL DAQ6510,04591126,1.7.12b' + + +def test_reset(): + with expected_protocol( + KeithleyDAQ6510, + [(b'*LANG SCPI', None), + (b'*RST', None)], + ) as inst: + assert inst.reset() is None From 3429500926baee1f145a3c9c54b8af96043c6f25 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:52:07 -0500 Subject: [PATCH 176/181] remove unused imports --- tests/instruments/keithley/test_keithleyDAQ6510.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/instruments/keithley/test_keithleyDAQ6510.py b/tests/instruments/keithley/test_keithleyDAQ6510.py index bfdb103359..d3dc582cee 100644 --- a/tests/instruments/keithley/test_keithleyDAQ6510.py +++ b/tests/instruments/keithley/test_keithleyDAQ6510.py @@ -22,8 +22,6 @@ # THE SOFTWARE. # -import pytest -import logging from pymeasure.test import expected_protocol from pymeasure.instruments.keithley import KeithleyDAQ6510 From 18249d7203529ebbe57545f0d008d561dcb3fca6 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:21:42 -0500 Subject: [PATCH 177/181] removed additional `*LANG SCPI`s from test --- .../keithley/test_keithleyDAQ6510.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/instruments/keithley/test_keithleyDAQ6510.py b/tests/instruments/keithley/test_keithleyDAQ6510.py index d3dc582cee..dd6ed455e9 100644 --- a/tests/instruments/keithley/test_keithleyDAQ6510.py +++ b/tests/instruments/keithley/test_keithleyDAQ6510.py @@ -37,8 +37,7 @@ def test_init(): def test_current_range_set(): with expected_protocol( KeithleyDAQ6510, - [(b'*LANG SCPI', None), - (b':SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG 1', None)], + [(b':SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG 1', None)], ) as inst: inst.current_range = 1.0 @@ -46,8 +45,7 @@ def test_current_range_set(): def test_current_range_get(): with expected_protocol( KeithleyDAQ6510, - [(b'*LANG SCPI', None), - (b':SENS:CURR:RANG?', b'1.0\n')], + [(b':SENS:CURR:RANG?', b'1.0\n')], ) as inst: assert inst.current_range == 1.0 @@ -55,8 +53,7 @@ def test_current_range_get(): def test_current_nplc_set(): with expected_protocol( KeithleyDAQ6510, - [(b'*LANG SCPI', None), - (b':SENS:CURR:NPLC 2', None)], + [(b':SENS:CURR:NPLC 2', None)], ) as inst: inst.current_nplc = 2 @@ -64,8 +61,7 @@ def test_current_nplc_set(): def test_current_nplc_get(): with expected_protocol( KeithleyDAQ6510, - [(b'*LANG SCPI', None), - (b':SENS:CURR:NPLC?', b'2\n')], + [(b':SENS:CURR:NPLC?', b'2\n')], ) as inst: assert inst.current_nplc == 2.0 @@ -73,8 +69,7 @@ def test_current_nplc_get(): def test_id(): with expected_protocol( KeithleyDAQ6510, - [(b'*LANG SCPI', None), - (b'*IDN?', b'KEITHLEY INSTRUMENTS,MODEL DAQ6510,04591126,1.7.12b\n')], + [(b'*IDN?', b'KEITHLEY INSTRUMENTS,MODEL DAQ6510,04591126,1.7.12b\n')], ) as inst: assert inst.id == 'KEITHLEY INSTRUMENTS,MODEL DAQ6510,04591126,1.7.12b' @@ -82,7 +77,6 @@ def test_id(): def test_reset(): with expected_protocol( KeithleyDAQ6510, - [(b'*LANG SCPI', None), - (b'*RST', None)], + [(b'*RST', None)], ) as inst: assert inst.reset() is None From 3e0cad9f42192cf2e491d7f80722564e7cba877d Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:26:36 -0500 Subject: [PATCH 178/181] completely remove all `*LANG SCPI` calls --- tests/instruments/keithley/test_keithleyDAQ6510.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/instruments/keithley/test_keithleyDAQ6510.py b/tests/instruments/keithley/test_keithleyDAQ6510.py index dd6ed455e9..715ea7d33f 100644 --- a/tests/instruments/keithley/test_keithleyDAQ6510.py +++ b/tests/instruments/keithley/test_keithleyDAQ6510.py @@ -26,14 +26,6 @@ from pymeasure.instruments.keithley import KeithleyDAQ6510 -def test_init(): - with expected_protocol( - KeithleyDAQ6510, - [(b'*LANG SCPI', None)], - ): - pass # Verify the expected communication. - - def test_current_range_set(): with expected_protocol( KeithleyDAQ6510, From a6900b75cfa29e01f2ec76eaf3e4f5448d6566b1 Mon Sep 17 00:00:00 2001 From: David Sun <54460365+Aphelion82@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:04:47 -0500 Subject: [PATCH 179/181] re-added init test --- tests/instruments/keithley/test_keithleyDAQ6510.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/instruments/keithley/test_keithleyDAQ6510.py b/tests/instruments/keithley/test_keithleyDAQ6510.py index 715ea7d33f..8353427796 100644 --- a/tests/instruments/keithley/test_keithleyDAQ6510.py +++ b/tests/instruments/keithley/test_keithleyDAQ6510.py @@ -26,6 +26,12 @@ from pymeasure.instruments.keithley import KeithleyDAQ6510 +def test_init(): + with expected_protocol( + KeithleyDAQ6510, + ): + pass + def test_current_range_set(): with expected_protocol( KeithleyDAQ6510, From 06556898491f933fa06ebfb41ec94f4a74ef1bcc Mon Sep 17 00:00:00 2001 From: Michele Sardo <37845722+msmttchr@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:25:01 +0200 Subject: [PATCH 180/181] Fix documentation errors (#1133) --- docs/api/instruments/agilent/agilentB1500.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/instruments/agilent/agilentB1500.rst b/docs/api/instruments/agilent/agilentB1500.rst index 3646fbe0fb..dd2056eb01 100644 --- a/docs/api/instruments/agilent/agilentB1500.rst +++ b/docs/api/instruments/agilent/agilentB1500.rst @@ -31,7 +31,7 @@ method/attribute names in this instrument driver. =========== ============================================= Command Property/Method =========== ============================================= -``AAD`` :meth:`SMU.adc_type` +``AAD`` :attr:`SMU.adc_type` ``AB`` :meth:`~AgilentB1500.abort` ``AIT`` :meth:`~AgilentB1500.adc_setup` ``AV`` :meth:`~AgilentB1500.adc_averaging` @@ -39,7 +39,7 @@ Command Property/Method ``BC`` :meth:`~AgilentB1500.clear_buffer` ``CL`` :meth:`SMU.disable` ``CM`` :attr:`~AgilentB1500.auto_calibration` -``CMM`` :meth:`SMU.meas_op_mode` +``CMM`` :attr:`SMU.meas_op_mode` ``CN`` :meth:`SMU.enable` ``DI`` :meth:`SMU.force` mode: ``'CURRENT'`` ``DV`` :meth:`SMU.force` mode: ``'VOLTAGE'`` @@ -47,7 +47,7 @@ Command Property/Method ``ERRX?`` :meth:`~AgilentB1500.check_errors` ``FL`` :attr:`SMU.filter` ``FMT`` :meth:`~AgilentB1500.data_format` -``*IDN?`` :meth:`~AgilentB1500.id` +``*IDN?`` :attr:`~AgilentB1500.id` ``*LRN?`` :meth:`~AgilentB1500.query_learn`, |br| multiple methods to read/format settings directly ``MI`` :meth:`SMU.sampling_source` mode: ``'CURRENT'`` From cf5294793936072e82e6d22e30297b37e3e083b4 Mon Sep 17 00:00:00 2001 From: Benedikt Burger <67148916+BenediktBurger@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:01:57 +0200 Subject: [PATCH 181/181] Fix test for Keithley6510 --- tests/instruments/keithley/test_keithleyDAQ6510.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/instruments/keithley/test_keithleyDAQ6510.py b/tests/instruments/keithley/test_keithleyDAQ6510.py index 8353427796..95e79ddbba 100644 --- a/tests/instruments/keithley/test_keithleyDAQ6510.py +++ b/tests/instruments/keithley/test_keithleyDAQ6510.py @@ -29,9 +29,11 @@ def test_init(): with expected_protocol( KeithleyDAQ6510, + [], ): pass + def test_current_range_set(): with expected_protocol( KeithleyDAQ6510,