From 923a8292cf50ff012f46e5c06acc0d950cf59cdf Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 06:59:06 +0100 Subject: [PATCH 1/7] Annotate Keysight B1500 enum parameters The staircase sweep setup methods now convert their enum or int arguments before setting the parameters, and the B1517A one takes the WM post sweep condition rather than the B1520A WMDCV one. --- .../Keysight/keysightb1500/KeysightB1517A.py | 46 ++++++----- .../Keysight/keysightb1500/KeysightB1520A.py | 80 ++++++++++--------- .../b1500_driver_tests/test_b1517a_smu.py | 4 +- 3 files changed, 71 insertions(+), 59 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py index 22d414b525ff..f02fdb2e0602 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py @@ -79,7 +79,7 @@ def __init__( "power_compliance": None, } - self.sweep_auto_abort: Parameter = self.add_parameter( + self.sweep_auto_abort: Parameter[constants.Abort, Self] = self.add_parameter( name="sweep_auto_abort", set_cmd=self._set_sweep_auto_abort, get_cmd=self._get_sweep_auto_abort, @@ -127,20 +127,22 @@ def __init__( sweep source forces the pulse base value after sweep. """ - self.post_sweep_voltage_condition: Parameter = self.add_parameter( - name="post_sweep_voltage_condition", - set_cmd=self._set_post_sweep_voltage_condition, - get_cmd=self._get_post_sweep_voltage_condition, - set_parser=constants.WM.Post, - get_parser=constants.WM.Post, - vals=vals.Enum(*list(constants.WM.Post)), - initial_cache_value=constants.WM.Post.START, - docstring=textwrap.dedent( - """ + self.post_sweep_voltage_condition: Parameter[constants.WM.Post, Self] = ( + self.add_parameter( + name="post_sweep_voltage_condition", + set_cmd=self._set_post_sweep_voltage_condition, + get_cmd=self._get_post_sweep_voltage_condition, + set_parser=constants.WM.Post, + get_parser=constants.WM.Post, + vals=vals.Enum(*list(constants.WM.Post)), + initial_cache_value=constants.WM.Post.START, + docstring=textwrap.dedent( + """ Source output value after the measurement is normally completed. If this parameter is not set, the sweep sources force the start value. """ - ), + ), + ) ) """ Source output value after the measurement is normally completed. If @@ -294,7 +296,7 @@ def __init__( get_parser=self._get_sweep_delays_parser, ) - self.sweep_mode: Parameter = self.add_parameter( + self.sweep_mode: Parameter[constants.SweepMode, Self] = self.add_parameter( name="sweep_mode", set_cmd=self._set_sweep_mode, get_cmd=self._get_sweep_mode, @@ -323,7 +325,7 @@ def __init__( 4: Log sweep (double stair, start to stop to start.) """ - self.sweep_range: Parameter = self.add_parameter( + self.sweep_range: Parameter[constants.VOutputRange, Self] = self.add_parameter( name="sweep_range", set_cmd=self._set_sweep_range, get_cmd=self._get_sweep_range, @@ -955,7 +957,7 @@ def __init__( IOutputRange.MIN_100mA, ] - self.measurement_mode: Parameter = self.add_parameter( + self.measurement_mode: Parameter[MM.Mode, Self] = self.add_parameter( name="measurement_mode", get_cmd=None, set_cmd=self._set_measurement_mode, @@ -1022,7 +1024,7 @@ def __init__( ) """Parameter current""" - self.time_axis: Parameter = self.add_parameter( + self.time_axis: Parameter[npt.NDArray, Self] = self.add_parameter( name="time_axis", get_cmd=self._get_time_axis, vals=vals.Arrays(shape=(self._get_number_of_samples,)), @@ -1358,7 +1360,7 @@ def setup_staircase_sweep( v_start: float, v_end: float, n_steps: int, - post_sweep_voltage_val: constants.WMDCV.Post | int = constants.WMDCV.Post.STOP, + post_sweep_voltage_val: constants.WM.Post | int = constants.WM.Post.STOP, av_coef: int = -1, enable_filter: bool = True, v_src_range: constants.OutputRange = constants.VOutputRange.AUTO, @@ -1420,10 +1422,12 @@ def setup_staircase_sweep( self.iv_sweep.delay(delay) self.iv_sweep.step_delay(step_delay) self.iv_sweep.measure_delay(measure_delay) - self.iv_sweep.sweep_auto_abort(abort_enabled) - self.iv_sweep.post_sweep_voltage_condition(post_sweep_voltage_val) - self.iv_sweep.sweep_mode(sweep_mode) - self.iv_sweep.sweep_range(v_src_range) + self.iv_sweep.sweep_auto_abort(constants.Abort(abort_enabled)) + self.iv_sweep.post_sweep_voltage_condition( + constants.WM.Post(post_sweep_voltage_val) + ) + self.iv_sweep.sweep_mode(constants.SweepMode(sweep_mode)) + self.iv_sweep.sweep_range(constants.VOutputRange(v_src_range)) self.iv_sweep.sweep_start(v_start) self.iv_sweep.sweep_end(v_end) self.iv_sweep.sweep_steps(n_steps) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py index 073dc5b3afdf..5207962eaebc 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py @@ -53,7 +53,7 @@ def __init__( ): super().__init__(parent, name, **kwargs) - self.sweep_auto_abort: Parameter = self.add_parameter( + self.sweep_auto_abort: Parameter[constants.Abort, Self] = self.add_parameter( name="sweep_auto_abort", set_cmd=self._set_sweep_auto_abort, get_cmd=self._get_sweep_auto_abort, @@ -86,16 +86,17 @@ def __init__( - Overflow on the AD converter """ - self.post_sweep_voltage_condition: Parameter = self.add_parameter( - name="post_sweep_voltage_condition", - set_cmd=self._set_post_sweep_voltage_condition, - get_cmd=self._get_post_sweep_voltage_condition, - set_parser=constants.WMDCV.Post, - get_parser=constants.WMDCV.Post, - vals=vals.Enum(*list(constants.WMDCV.Post)), - initial_cache_value=constants.WMDCV.Post.START, - docstring=textwrap.dedent( - """ + self.post_sweep_voltage_condition: Parameter[constants.WMDCV.Post, Self] = ( + self.add_parameter( + name="post_sweep_voltage_condition", + set_cmd=self._set_post_sweep_voltage_condition, + get_cmd=self._get_post_sweep_voltage_condition, + set_parser=constants.WMDCV.Post, + get_parser=constants.WMDCV.Post, + vals=vals.Enum(*list(constants.WMDCV.Post)), + initial_cache_value=constants.WMDCV.Post.START, + docstring=textwrap.dedent( + """ This command also sets the post measurement condition of the MFCMU. After the measurement is normally completed, the DC bias sweep source @@ -107,7 +108,8 @@ def __init__( the start value, and the pulsed bias sweep source forces the pulse base value after sweep. """ - ), + ), + ) ) """ This command also sets the post measurement @@ -524,13 +526,14 @@ def __init__( ) """Instrument module correction""" - self.phase_compensation_mode: Parameter = self.add_parameter( - name="phase_compensation_mode", - set_cmd=self._set_phase_compensation_mode, - get_cmd=None, - set_parser=constants.ADJ.Mode, - docstring=textwrap.dedent( - """ + self.phase_compensation_mode: Parameter[constants.ADJ.Mode, Self] = ( + self.add_parameter( + name="phase_compensation_mode", + set_cmd=self._set_phase_compensation_mode, + get_cmd=None, + set_parser=constants.ADJ.Mode, + docstring=textwrap.dedent( + """ This parameter selects the MFCMU phase compensation mode. This command initializes the MFCMU. The available modes are captured in :class:`constants.ADJ.Mode`: @@ -546,7 +549,8 @@ def __init__( For mode=2, the KeysightB1500 performs the phase compensation before every measurement. It is useful when there are wide load fluctuations by changing the bias and so on.""" - ), + ), + ) ) """ This parameter selects the MFCMU phase compensation mode. This @@ -642,7 +646,7 @@ def __init__( get_parser=self._get_adc_mode_parser, ) - self.ranging_mode: Parameter = self.add_parameter( + self.ranging_mode: Parameter[constants.RangingMode, Self] = self.add_parameter( name="ranging_mode", set_cmd=self._set_ranging_mode, vals=vals.Enum(*list(constants.RangingMode)), @@ -691,7 +695,7 @@ def __init__( the FC command. """ - self.measurement_mode: Parameter = self.add_parameter( + self.measurement_mode: Parameter[MM.Mode, Self] = self.add_parameter( name="measurement_mode", get_cmd=None, set_cmd=self._set_measurement_mode, @@ -719,19 +723,21 @@ def __init__( guide for more information. """ - self.impedance_model: Parameter = self.add_parameter( - name="impedance_model", - set_cmd=self._set_impedance_model, - get_cmd=None, - vals=vals.Enum(*list(constants.IMP.MeasurementMode)), - set_parser=constants.IMP.MeasurementMode, - initial_value=constants.IMP.MeasurementMode.Cp_D, - docstring=textwrap.dedent( - """ + self.impedance_model: Parameter[constants.IMP.MeasurementMode, Self] = ( + self.add_parameter( + name="impedance_model", + set_cmd=self._set_impedance_model, + get_cmd=None, + vals=vals.Enum(*list(constants.IMP.MeasurementMode)), + set_parser=constants.IMP.MeasurementMode, + initial_value=constants.IMP.MeasurementMode.Cp_D, + docstring=textwrap.dedent( + """ The IMP command specifies the parameter measured by the MFCMU. Look at the ``constants.IMP.MeasurementMode`` for all the modes. """ - ), + ), + ) ) """ The IMP command specifies the parameter measured by the MFCMU. @@ -1124,8 +1130,10 @@ def setup_staircase_cv( self.adc_coef(adc_coef) self.frequency(freq) self.voltage_ac(ac_rms) - self.cv_sweep.sweep_auto_abort(abort_enabled) - self.cv_sweep.post_sweep_voltage_condition(post_sweep_voltage_condition) + self.cv_sweep.sweep_auto_abort(constants.Abort(abort_enabled)) + self.cv_sweep.post_sweep_voltage_condition( + constants.WMDCV.Post(post_sweep_voltage_condition) + ) self.cv_sweep.hold_time(hold_delay) self.cv_sweep.delay(delay) self.cv_sweep.step_delay(step_delay) @@ -1136,9 +1144,9 @@ def setup_staircase_cv( self.cv_sweep.sweep_end(v_end) self.cv_sweep.sweep_steps(n_steps) self.measurement_mode(constants.MM.Mode.CV_DC_SWEEP) - self.impedance_model(imp_model) + self.impedance_model(constants.IMP.MeasurementMode(imp_model)) self.ac_dc_volt_monitor(volt_monitor) - self.ranging_mode(ranging_mode) + self.ranging_mode(constants.RangingMode(ranging_mode)) self.measurement_range_for_non_auto(fixed_range_val) self.setup_fnc_already_run = True diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py index d2f36cae9694..95ed7aa66b55 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py @@ -334,7 +334,7 @@ def test_measurement_mode_to_enum_value(smu: KeysightB1517A) -> None: def test_measurement_mode_to_int_value(smu: KeysightB1517A) -> None: mainframe = smu.parent assert isinstance(mainframe, MagicMock) - smu.measurement_mode(10) + smu.measurement_mode(10) # pyright: ignore[reportArgumentType] mainframe.write.assert_called_once_with("MM 10,1") new_mode = smu.measurement_mode() @@ -561,7 +561,7 @@ def test_set_post_sweep_voltage_cond(smu: KeysightB1517A) -> None: mainframe = smu.parent assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "WM2,2;WT1.0,0.0,0.0,0.0,0.0" - smu.iv_sweep.post_sweep_voltage_condition(constants.WMDCV.Post.STOP) + smu.iv_sweep.post_sweep_voltage_condition(constants.WM.Post.STOP) mainframe.write.assert_called_once_with("WM 2,2") From c7f327c739b5ef044b2dfc226054a497b3be93bb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 06:59:08 +0100 Subject: [PATCH 2/7] Annotate Keysight B220x parameters --- src/qcodes/instrument_drivers/Keysight/keysight_b220x.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py index a235bafd5c9c..e1fc69375e88 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py @@ -147,7 +147,7 @@ def __init__( 'mbb' (make before break) """ - self.bias_input_port: Parameter = self.add_parameter( + self.bias_input_port: Parameter[int, Self] = self.add_parameter( name="bias_input_port", get_cmd=f":BIAS:PORT? {self._card}", set_cmd=f":BIAS:PORT {self._card},{{}}", @@ -175,7 +175,7 @@ def __init__( ) """Param: True for ON, False for OFF""" - self.gnd_input_port: Parameter = self.add_parameter( + self.gnd_input_port: Parameter[int, Self] = self.add_parameter( name="gnd_input_port", get_cmd=f":AGND:PORT? {self._card}", set_cmd=f":AGND:PORT {self._card},{{}}", @@ -201,7 +201,7 @@ def __init__( ) """Parameter gnd_mode""" - self.unused_inputs: Parameter = self.add_parameter( + self.unused_inputs: Parameter[list[int], Self] = self.add_parameter( name="unused_inputs", get_cmd=f":AGND:UNUSED? {self._card}", set_cmd=f":AGND:UNUSED {self._card},'{{}}'", @@ -213,7 +213,7 @@ def __init__( ) """Parameter unused_inputs""" - self.couple_ports: Parameter = self.add_parameter( + self.couple_ports: Parameter[list[int], Self] = self.add_parameter( name="couple_ports", get_cmd=f":COUP:PORT? {self._card}", set_cmd=f":COUP:PORT {self._card},'{{}}'", From a1fdc8158b3adf276635921f7fe5c214fab410ab Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 07:07:45 +0100 Subject: [PATCH 3/7] Add newsfragment for PR 8695 --- docs/changes/newsfragments/8695.improved_driver | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/changes/newsfragments/8695.improved_driver diff --git a/docs/changes/newsfragments/8695.improved_driver b/docs/changes/newsfragments/8695.improved_driver new file mode 100644 index 000000000000..ed2993f01c27 --- /dev/null +++ b/docs/changes/newsfragments/8695.improved_driver @@ -0,0 +1,8 @@ +The enum parameters of the ``KeysightB1517A`` and ``KeysightB1520A`` modules of the Keysight +B1500, such as ``sweep_mode``, ``measurement_mode`` and ``impedance_model``, are now annotated +with their enum type, e.g. ``Parameter[constants.SweepMode, Self]``, and +``KeysightB1517A.time_axis`` as a numpy array. The parameters of the Keysight B220x are annotated +as well. The ``post_sweep_voltage_val`` argument of ``KeysightB1517A.setup_staircase_sweep`` is +now typed as ``constants.WM.Post``, the enum used by the B1517A sweep, instead of +``constants.WMDCV.Post``, which belongs to the B1520A CV sweep. Both have the same values, so +existing calls keep working. From 7f30452f20265d15e3b37ea69848e0123958aa3f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 07:14:19 +0100 Subject: [PATCH 4/7] Turn Keysight E4980A correction actions into methods open and short were command-only parameters, so calling them without an argument, as the example does, returned the cached value and did not execute the correction. --- docs/changes/newsfragments/8695.breaking | 6 +++++ .../instrument/sims/Keysight_E4980A.yaml | 9 +++++++ .../Keysight/keysight_e4980a.py | 26 +++++++++---------- tests/drivers/test_keysight_e4980a.py | 22 ++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 docs/changes/newsfragments/8695.breaking diff --git a/docs/changes/newsfragments/8695.breaking b/docs/changes/newsfragments/8695.breaking new file mode 100644 index 000000000000..e2ff1f7248be --- /dev/null +++ b/docs/changes/newsfragments/8695.breaking @@ -0,0 +1,6 @@ +The ``open`` and ``short`` parameters of ``KeysightE4980ACorrection`` have been replaced by +methods of the same name. Both were command-only parameters, so calling them without an +argument, e.g. ``meter.correction.open()`` as in the example notebook, only returned the cached +value and did not execute the OPEN or SHORT correction. Calling them like this now sends the +command. Code that passed a value to them, e.g. ``meter.correction.open(None)``, must drop the +argument, and they are no longer part of the instrument's parameters or snapshot. diff --git a/src/qcodes/instrument/sims/Keysight_E4980A.yaml b/src/qcodes/instrument/sims/Keysight_E4980A.yaml index b218c9e7a343..64e02e9133ed 100644 --- a/src/qcodes/instrument/sims/Keysight_E4980A.yaml +++ b/src/qcodes/instrument/sims/Keysight_E4980A.yaml @@ -49,6 +49,15 @@ devices: setter: q: ":FUNCtion:IMPedance:RANGe:AUTO {}" + # command-only, no reply + correction_open: + setter: + q: ":CORRection:OPEN" + + correction_short: + setter: + q: ":CORRection:SHORt" + resources: GPIB::1::INSTR: device: device 1 diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py index 19793ca568a6..8c6eb8cc2b16 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py @@ -153,13 +153,6 @@ def __init__( ) -> None: super().__init__(parent, name, **kwargs) - self.open: Parameter = self.add_parameter( - "open", - set_cmd=":CORRection:OPEN", - docstring="Executes OPEN correction based on all frequency points.", - ) - """Executes OPEN correction based on all frequency points.""" - self.open_state: Parameter[bool, Self] = self.add_parameter( "open_state", get_cmd=":CORRection:OPEN:STATe?", @@ -169,13 +162,6 @@ def __init__( ) """Enables or disable OPEN correction""" - self.short: Parameter = self.add_parameter( - "short", - set_cmd=":CORRection:SHORt", - docstring="Executes SHORT correction based on all frequency points.", - ) - """Executes SHORT correction based on all frequency points.""" - self.short_state: Parameter[bool, Self] = self.add_parameter( "short_state", get_cmd=":CORRection:SHORt:STATe?", @@ -185,6 +171,18 @@ def __init__( ) """Enables or disable SHORT correction.""" + def open(self) -> None: + """ + Executes OPEN correction based on all frequency points. + """ + self.write(":CORRection:OPEN") + + def short(self) -> None: + """ + Executes SHORT correction based on all frequency points. + """ + self.write(":CORRection:SHORt") + class KeysightE4980A(VisaInstrument): """ diff --git a/tests/drivers/test_keysight_e4980a.py b/tests/drivers/test_keysight_e4980a.py index 844a5f98ae5f..2489917d1788 100644 --- a/tests/drivers/test_keysight_e4980a.py +++ b/tests/drivers/test_keysight_e4980a.py @@ -8,6 +8,8 @@ if TYPE_CHECKING: from collections.abc import Generator + from pytest_mock import MockerFixture + @pytest.fixture(name="driver") def _make_driver() -> "Generator[E4980A.KeysightE4980A, None, None]": @@ -74,3 +76,23 @@ def test_range_is_float(driver: E4980A.KeysightE4980A) -> None: driver.range(1000) assert driver.range() == 1000.0 + + +@pytest.mark.parametrize( + ("action", "command"), + [("open", ":CORRection:OPEN"), ("short", ":CORRection:SHORt")], +) +def test_correction_methods_send_command( + driver: E4980A.KeysightE4980A, mocker: "MockerFixture", action: str, command: str +) -> None: + # these used to be parameters, so calling them without an argument only + # returned the cached value and did not send anything + write_raw = mocker.spy(driver, "write_raw") + if action == "open": + driver.correction.open() + else: + driver.correction.short() + + write_raw.assert_called_once_with(command) + # the instrument does not reply, so the next query is unaffected + assert driver.IDN()["model"] == "E4980A" From 5d77bb428d9a7cc2d2b1d86e2ceb4de1da5aa68f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 07:24:48 +0100 Subject: [PATCH 5/7] Annotate Agilent E8257D and E8267C phase --- docs/changes/newsfragments/8695.improved_driver | 3 ++- src/qcodes/instrument/sims/Agilent_E8257D.yaml | 10 ++++++++++ src/qcodes/instrument/sims/Agilent_E8267C.yaml | 10 ++++++++++ .../instrument_drivers/agilent/Agilent_E8257D.py | 2 +- .../instrument_drivers/agilent/Agilent_E8267C.py | 2 +- tests/drivers/test_agilent_e8257d.py | 16 ++++++++++++++++ tests/drivers/test_agilent_e8267c.py | 15 +++++++++++++++ 7 files changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/changes/newsfragments/8695.improved_driver b/docs/changes/newsfragments/8695.improved_driver index ed2993f01c27..26ba5ff7d419 100644 --- a/docs/changes/newsfragments/8695.improved_driver +++ b/docs/changes/newsfragments/8695.improved_driver @@ -1,7 +1,8 @@ The enum parameters of the ``KeysightB1517A`` and ``KeysightB1520A`` modules of the Keysight B1500, such as ``sweep_mode``, ``measurement_mode`` and ``impedance_model``, are now annotated with their enum type, e.g. ``Parameter[constants.SweepMode, Self]``, and -``KeysightB1517A.time_axis`` as a numpy array. The parameters of the Keysight B220x are annotated +``KeysightB1517A.time_axis`` as a numpy array. The parameters of the Keysight B220x and the +``phase`` of the ``AgilentE8257D`` and ``AgilentE8267C`` (``float``, in degrees) are annotated as well. The ``post_sweep_voltage_val`` argument of ``KeysightB1517A.setup_staircase_sweep`` is now typed as ``constants.WM.Post``, the enum used by the B1517A sweep, instead of ``constants.WMDCV.Post``, which belongs to the B1520A CV sweep. Both have the same values, so diff --git a/src/qcodes/instrument/sims/Agilent_E8257D.yaml b/src/qcodes/instrument/sims/Agilent_E8257D.yaml index af4816d6bbbc..c7e645089acd 100644 --- a/src/qcodes/instrument/sims/Agilent_E8257D.yaml +++ b/src/qcodes/instrument/sims/Agilent_E8257D.yaml @@ -25,6 +25,16 @@ devices: setter: q: "OUTP {}" + phase: + # *RST value according to the E8257D/67D SCPI command reference; + # the query returns radians + default: "+0.00000000E+000" + getter: + q: "PHASE?" + r: "{}" + setter: + q: "PHASE {}" + resources: GPIB::1::INSTR: device: device 1 diff --git a/src/qcodes/instrument/sims/Agilent_E8267C.yaml b/src/qcodes/instrument/sims/Agilent_E8267C.yaml index c6f82f5887d6..d00b23abca21 100644 --- a/src/qcodes/instrument/sims/Agilent_E8267C.yaml +++ b/src/qcodes/instrument/sims/Agilent_E8267C.yaml @@ -36,6 +36,16 @@ devices: setter: q: "PULM:INT:PWID {}" + phase: + # *RST value according to the E8257D/67D SCPI command reference; + # the query returns radians + default: "+0.00000000E+000" + getter: + q: "PHAS?" + r: "{}" + setter: + q: "PHAS {}" + resources: GPIB::1::INSTR: device: device 1 diff --git a/src/qcodes/instrument_drivers/agilent/Agilent_E8257D.py b/src/qcodes/instrument_drivers/agilent/Agilent_E8257D.py index 2dfbdf89f12c..87abb7a0451e 100644 --- a/src/qcodes/instrument_drivers/agilent/Agilent_E8257D.py +++ b/src/qcodes/instrument_drivers/agilent/Agilent_E8257D.py @@ -104,7 +104,7 @@ def __init__( ) """Parameter frequency""" - self.phase: Parameter = self.add_parameter( + self.phase: Parameter[float, Self] = self.add_parameter( name="phase", label="Phase", unit="deg", diff --git a/src/qcodes/instrument_drivers/agilent/Agilent_E8267C.py b/src/qcodes/instrument_drivers/agilent/Agilent_E8267C.py index f2f9d0a3e237..cbe552f1273a 100644 --- a/src/qcodes/instrument_drivers/agilent/Agilent_E8267C.py +++ b/src/qcodes/instrument_drivers/agilent/Agilent_E8267C.py @@ -64,7 +64,7 @@ def __init__( vals=Numbers(min_value=10e-9, max_value=20e-9), ) """Parameter pulse_width""" - self.phase: Parameter = self.add_parameter( + self.phase: Parameter[float, Self] = self.add_parameter( name="phase", label="Phase", unit="deg", diff --git a/tests/drivers/test_agilent_e8257d.py b/tests/drivers/test_agilent_e8257d.py index 42de831800d6..c4d8c0cb6f63 100644 --- a/tests/drivers/test_agilent_e8257d.py +++ b/tests/drivers/test_agilent_e8257d.py @@ -143,3 +143,19 @@ def test_limits_from_options(e8257d: AgilentE8257D) -> None: e8257d.power.validate(-135) with pytest.raises(ValueError): e8257d.power.validate(11) + + +def test_phase_is_float_in_degrees( + e8257d: AgilentE8257D, mocker: "MockerFixture" +) -> None: + assert e8257d.phase.unit == "deg" + + phase = e8257d.phase() + assert isinstance(phase, float) + assert phase == 0.0 + + # the instrument takes and returns the phase in radians + write_raw = mocker.spy(e8257d, "write_raw") + e8257d.phase(90) + write_raw.assert_called_once_with("PHASE 1.57079633") + assert e8257d.phase() == pytest.approx(90, abs=1e-6) diff --git a/tests/drivers/test_agilent_e8267c.py b/tests/drivers/test_agilent_e8267c.py index e374d5865e3f..efb9aac22858 100644 --- a/tests/drivers/test_agilent_e8267c.py +++ b/tests/drivers/test_agilent_e8267c.py @@ -28,3 +28,18 @@ def test_pulse_width_is_float_in_seconds(e8267c: AgilentE8267C) -> None: e8267c.pulse_width(15e-9) assert e8267c.pulse_width() == pytest.approx(15e-9) + + +def test_phase_is_float_in_degrees(e8267c: AgilentE8267C) -> None: + assert e8267c.phase.unit == "deg" + + phase = e8267c.phase() + assert isinstance(phase, float) + assert phase == 0.0 + + # the instrument takes and returns the phase in radians + e8267c.phase(-90) + assert e8267c.phase() == pytest.approx(-90) + + with pytest.raises(ValueError): + e8267c.phase(180) From 476b62612e517d06e1ec058cd915d6e7e09c26c9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 07:44:06 +0100 Subject: [PATCH 6/7] Fix Keysight 344xxA resolution keywords Since NumericScpiMnemonic became a PEP 695 type alias, get_args returns an empty tuple for it, so "MIN", "MAX" and "DEF" were formatted as numbers and raised a ValueError. --- .../newsfragments/8695.improved_driver.1 | 2 ++ .../instrument/sims/Keysight_34465A.yaml | 3 ++- .../private/Keysight_344xxA_submodules.py | 9 ++++--- tests/drivers/test_keysight_34465a.py | 24 +++++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8695.improved_driver.1 diff --git a/docs/changes/newsfragments/8695.improved_driver.1 b/docs/changes/newsfragments/8695.improved_driver.1 new file mode 100644 index 000000000000..fb0b111897c0 --- /dev/null +++ b/docs/changes/newsfragments/8695.improved_driver.1 @@ -0,0 +1,2 @@ +Setting ``resolution`` of the Keysight 344xxA DMMs to ``"MIN"``, ``"MAX"`` or ``"DEF"`` raised a +``ValueError`` instead of sending the setting to the instrument. diff --git a/src/qcodes/instrument/sims/Keysight_34465A.yaml b/src/qcodes/instrument/sims/Keysight_34465A.yaml index 8caf02abe68d..75a3493eb287 100644 --- a/src/qcodes/instrument/sims/Keysight_34465A.yaml +++ b/src/qcodes/instrument/sims/Keysight_34465A.yaml @@ -127,7 +127,8 @@ devices: q: "SENSe:VOLTage:DC:RESolution?" r: "{}" setter: - q: "SENSe:VOLTage:DC:RESolution {}" + # the driver sets the resolution with the short form + q: "VOLT:DC:RES {}" current_ac_autorange: default: 0 getter: diff --git a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py index 94f7deb654fc..32c46737d03e 100644 --- a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py +++ b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py @@ -2,7 +2,7 @@ from bisect import bisect_left from contextlib import ExitStack from functools import partial -from typing import TYPE_CHECKING, Any, Literal, Self, get_args +from typing import TYPE_CHECKING, Any, Literal, Self import numpy as np import numpy.typing as npt @@ -1273,8 +1273,11 @@ def _set_resolution(self, value: float | NumericScpiMnemonic) -> None: # convert both value*range and the resolution factors # to strings with few digits, so we avoid floating point # rounding errors. - if value in get_args(NumericScpiMnemonic): - str_value = str(value) # already str, but mypy does not know this + str_value: str + if isinstance(value, str): + # one of the NumericScpiMnemonic strings, as enforced by the + # validator + str_value = value else: res_fac_strs = [f"{(v * rang):.1e}" for v in self._resolution_factors] str_value = f"{value:.1e}" diff --git a/tests/drivers/test_keysight_34465a.py b/tests/drivers/test_keysight_34465a.py index bcd5f175a6e6..9df99528ebef 100644 --- a/tests/drivers/test_keysight_34465a.py +++ b/tests/drivers/test_keysight_34465a.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: from collections.abc import Generator + from pytest_mock import MockerFixture + class _AskCallable(Protocol): def __call__(self, cmd: str) -> str: ... @@ -167,3 +169,25 @@ def test_display_text(driver: Keysight34465A) -> None: driver.display.text(original_text) restored_text = driver.display.text() assert restored_text == original_text + + +@pytest.mark.parametrize("value", ["MIN", "MAX", "DEF"]) +def test_set_resolution_to_mnemonic( + driver: Keysight34465A, mocker: "MockerFixture", value: str +) -> None: + write_raw = mocker.spy(driver, "write_raw") + + driver.resolution(value) + + write_raw.assert_any_call(f"VOLT:DC:RES {value}") + + +def test_set_resolution_to_number( + driver: Keysight34465A, mocker: "MockerFixture" +) -> None: + write_raw = mocker.spy(driver, "write_raw") + driver.range(1) + + driver.resolution(3e-5) + + write_raw.assert_any_call("VOLT:DC:RES 3.0e-05") From 8df123b668f5568ff6430b2dffc60cc3eded221d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 30 Oct 2026 07:44:15 +0100 Subject: [PATCH 7/7] Annotate Keysight parameters that take SCPI keywords Getting these parameters returns a number, but they can also be set to keywords such as MIN or MAX, which stay in the cache until the next get, so they are annotated as float | str and their docstrings say so. --- .../newsfragments/8695.improved_driver | 6 + .../Keysight/KeysightAgilent_33XXX.py | 89 +++++++++-- .../private/Keysight_344xxA_submodules.py | 146 +++++++++++++++--- 3 files changed, 206 insertions(+), 35 deletions(-) diff --git a/docs/changes/newsfragments/8695.improved_driver b/docs/changes/newsfragments/8695.improved_driver index 26ba5ff7d419..ef5430be69b7 100644 --- a/docs/changes/newsfragments/8695.improved_driver +++ b/docs/changes/newsfragments/8695.improved_driver @@ -7,3 +7,9 @@ as well. The ``post_sweep_voltage_val`` argument of ``KeysightB1517A.setup_stair now typed as ``constants.WM.Post``, the enum used by the B1517A sweep, instead of ``constants.WMDCV.Post``, which belongs to the B1520A CV sweep. Both have the same values, so existing calls keep working. + +Parameters of the Keysight 344xxA DMMs and 33XXX waveform generators that can also be set to +SCPI keywords such as ``"MIN"``, ``"MAX"``, ``"DEF"`` or ``"INF"``, e.g. +``Keysight344xxASample.timer``, are annotated as ``Parameter[float | str, Self]`` (or +``int | str`` for counts). Getting them always returns a number, but the cached value is the +string that was set until the parameter is read again, as now explained in their docstrings. diff --git a/src/qcodes/instrument_drivers/Keysight/KeysightAgilent_33XXX.py b/src/qcodes/instrument_drivers/Keysight/KeysightAgilent_33XXX.py index 60ff7edc82d2..b9a0c4b84b8e 100644 --- a/src/qcodes/instrument_drivers/Keysight/KeysightAgilent_33XXX.py +++ b/src/qcodes/instrument_drivers/Keysight/KeysightAgilent_33XXX.py @@ -180,7 +180,7 @@ def val_parser(parser: type, inputstring: str) -> float | int: ) """Parameter pulse_width""" - self.edges: Parameter = self.add_parameter( + self.edges: Parameter[float | str, Self] = self.add_parameter( "edges", label=f"Channel {channum} edges width", set_cmd=f"SOURce{channum}:FUNCtion:PULSe:TRANsition {{}}", @@ -188,8 +188,22 @@ def val_parser(parser: type, inputstring: str) -> float | int: get_parser=float, unit="s", vals=vals.MultiType(vals.Numbers(), vals.Enum("MIN", "MAX")), + docstring=( + "Sets the transition times for the leading and trailing edges of " + "the pulse. Besides a number, this can be set to 'MIN' or 'MAX', " + "which the instrument converts to the corresponding number. " + "Getting the parameter always returns a number, but until it is " + "read again, the cached value is the string that was set. The " + "value is therefore annotated as ``float | str``." + ), ) - """Sets the transition times for the leading and trailing edges of the pulse.""" + """ + Sets the transition times for the leading and trailing edges of the pulse. + Besides a number, this can be set to "MIN" or "MAX", which the instrument + converts to the corresponding number. Getting the parameter always returns + a number, but until it is read again, the cached value is the string that + was set. The value is therefore annotated as ``float | str``. + """ # TRIGGER MENU self.trigger_source: Parameter[str, Self] = self.add_parameter( @@ -214,15 +228,24 @@ def val_parser(parser: type, inputstring: str) -> float | int: # Older models do not have all the fancy trigger options if self._parent.model[2] in ["5", "6"]: - self.trigger_count: Parameter = self.add_parameter( + self.trigger_count: Parameter[float, Self] = self.add_parameter( "trigger_count", label=f"Channel {channum} trigger count", set_cmd=f"TRIGger{channum}:COUNt {{}}", get_cmd=f"TRIGger{channum}:COUNt?", vals=vals.Ints(1, 1000000), get_parser=partial(val_parser, int), + docstring=( + "Number of triggers. Reads back as ``float('inf')`` if the " + "instrument reports an infinite count (9.9e37), so the value " + "is annotated as ``float``." + ), ) - """Parameter trigger_count""" + """ + Number of triggers. Reads back as ``float("inf")`` if the instrument + reports an infinite count (9.9e37), so the value is annotated as + ``float``. + """ self.trigger_delay: Parameter[float, Self] = self.add_parameter( "trigger_delay", @@ -279,15 +302,29 @@ def val_parser(parser: type, inputstring: str) -> float | int: ) """Parameter burst_mode""" - self.burst_ncycles: Parameter = self.add_parameter( + self.burst_ncycles: Parameter[float | str, Self] = self.add_parameter( "burst_ncycles", label=f"Channel {channum} burst no. of cycles", set_cmd=f"SOURce{channum}:BURSt:NCYCles {{}}", get_cmd=f"SOURce{channum}:BURSt:NCYCLes?", get_parser=partial(val_parser, int), vals=vals.MultiType(vals.Ints(1), vals.Enum("MIN", "MAX", "INF")), + docstring=( + "Number of cycles in a burst. Besides a number, this can be set " + "to 'MIN', 'MAX' or 'INF', which the instrument converts to the " + "corresponding number. Getting the parameter always returns a " + "number, ``float('inf')`` for an infinite count, but until it is " + "read again, the cached value is the string that was set. The " + "value is therefore annotated as ``float | str``." + ), ) - """Parameter burst_ncycles""" + """ + Number of cycles in a burst. Besides a number, this can be set to "MIN", + "MAX" or "INF", which the instrument converts to the corresponding number. + Getting the parameter always returns a number, ``float("inf")`` for an + infinite count, but until it is read again, the cached value is the string + that was set. The value is therefore annotated as ``float | str``. + """ self.burst_phase: Parameter[float, Self] = self.add_parameter( "burst_phase", @@ -325,7 +362,7 @@ def val_parser(parser: type, inputstring: str) -> float | int: ) """The burst period is the time between the starts of consecutive bursts when trigger is immediate.""" - self.output_load: Parameter = self.add_parameter( + self.output_load: Parameter[float | str, Self] = self.add_parameter( "output_load", label=f"Channel {channum} output load", set_cmd=f"OUTPut{channum}:LOAD {{}}", @@ -333,8 +370,26 @@ def val_parser(parser: type, inputstring: str) -> float | int: get_parser=partial(val_parser, float), unit="ohms", vals=vals.MultiType(vals.Numbers(1, 10000), vals.Enum("INF", "MIN", "MAX")), + docstring=( + "Sets expected output termination. Should equal the load " + "impedance attached to the output. Besides a number, this can be " + "set to 'INF' (high impedance), 'MIN' or 'MAX', which the " + "instrument converts to the corresponding number. Getting the " + "parameter always returns a number, ``float('inf')`` for high " + "impedance, but until it is read again, the cached value is the " + "string that was set. The value is therefore annotated as " + "``float | str``." + ), ) - """Sets expected output termination. Should equal the load impedance attached to the output.""" + """ + Sets expected output termination. Should equal the load impedance attached + to the output. Besides a number, this can be set to "INF" (high + impedance), "MIN" or "MAX", which the instrument converts to the + corresponding number. Getting the parameter always returns a number, + ``float("inf")`` for high impedance, but until it is read again, the + cached value is the string that was set. The value is therefore annotated + as ``float | str``. + """ self.auto_range: Parameter[str, Self] = self.add_parameter( "auto_range", @@ -352,7 +407,7 @@ def val_parser(parser: type, inputstring: str) -> float | int: "6", ]: # Older models do not support all arbitrary options max_srate = self._parent._max_srate[self.model] - self.srate: Parameter = self.add_parameter( + self.srate: Parameter[float | str, Self] = self.add_parameter( "srate", label=f"Channel {channum} sample rate", set_cmd=f"SOURce{channum}:FUNCtion:ARBitrary:SRATe {{}}", @@ -362,8 +417,22 @@ def val_parser(parser: type, inputstring: str) -> float | int: vals=vals.MultiType( vals.Numbers(1e-6, max_srate), vals.Enum("MIN", "MAX", "DEF") ), + docstring=( + "Sets the sample rate for the arbitrary waveform. Besides a " + "number, this can be set to 'MIN', 'MAX' or 'DEF', which the " + "instrument converts to the corresponding number. Getting the " + "parameter always returns a number, but until it is read " + "again, the cached value is the string that was set. The " + "value is therefore annotated as ``float | str``." + ), ) - """Sets the sample rate for the arbitrary waveform.""" + """ + Sets the sample rate for the arbitrary waveform. Besides a number, this + can be set to "MIN", "MAX" or "DEF", which the instrument converts to + the corresponding number. Getting the parameter always returns a + number, but until it is read again, the cached value is the string + that was set. The value is therefore annotated as ``float | str``. + """ self.add_function( "load_arb", diff --git a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py index 32c46737d03e..9eae886d94ba 100644 --- a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py +++ b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py @@ -46,7 +46,7 @@ def __init__( else: _max_trigger_count = 1e6 - self.count: Parameter = self.add_parameter( + self.count: Parameter[float | str, Self] = self.add_parameter( "count", label="Trigger Count", set_cmd="TRIGger:COUNt {}", @@ -67,7 +67,13 @@ def __init__( A variable trigger count is not available from the front panel. However, when you return to remote control of the instrument, - the trigger count returns to the previous value you selected.""" + the trigger count returns to the previous value you selected. + + Besides a number, the count can be set to "MIN", "MAX", "DEF" or + "INF" (infinite, read back as 9.9e37), which the instrument converts + to the corresponding number. Getting the parameter always returns a + number, but until it is read again, the cached value is the string + that was set. The value is therefore annotated as ``float | str``.""" ), ) """ @@ -81,9 +87,15 @@ def __init__( A variable trigger count is not available from the front panel. However, when you return to remote control of the instrument, the trigger count returns to the previous value you selected. + + Besides a number, the count can be set to "MIN", "MAX", "DEF" or + "INF" (infinite, read back as 9.9e37), which the instrument converts + to the corresponding number. Getting the parameter always returns a + number, but until it is read again, the cached value is the string + that was set. The value is therefore annotated as ``float | str``. """ - self.delay: Parameter = self.add_parameter( + self.delay: Parameter[float | str, Self] = self.add_parameter( "delay", label="Trigger Delay", unit="s", @@ -102,7 +114,13 @@ def __init__( measurements, step size depends on AC bandwidth. Selecting a specific trigger delay disables the automatic - trigger delay.""" + trigger delay. + + Besides a number, the delay can be set to "MIN", "MAX" or "DEF", + which the instrument converts to the corresponding number. Getting + the parameter always returns a number, but until it is read again, + the cached value is the string that was set. The value is therefore + annotated as ``float | str``.""" ), ) """ @@ -116,6 +134,12 @@ def __init__( Selecting a specific trigger delay disables the automatic trigger delay. + + Besides a number, the delay can be set to "MIN", "MAX" or "DEF", + which the instrument converts to the corresponding number. Getting + the parameter always returns a number, but until it is read again, + the cached value is the string that was set. The value is therefore + annotated as ``float | str``. """ self.auto_delay_enabled: Parameter[bool, Self] = self.add_parameter( @@ -154,7 +178,7 @@ def __init__( """Parameter slope""" if self.parent.has_DIG or (self.parent.model == "34411A"): - self.level: Parameter = self.add_parameter( + self.level: Parameter[float | str, Self] = self.add_parameter( "level", label="Trigger Level", unit="V", @@ -171,7 +195,14 @@ def __init__( Note that for 100 mV to 100 V ranges and autorange is off, the trigger level can only be set within ±120% of the - range.""" + range. + + Besides a number, the level can be set to "MIN", "MAX" or + "DEF", which the instrument converts to the corresponding + number. Getting the parameter always returns a number, but + until it is read again, the cached value is the string that + was set. The value is therefore annotated as + ``float | str``.""" ), ) """ @@ -181,6 +212,13 @@ def __init__( Note that for 100 mV to 100 V ranges and autorange is off, the trigger level can only be set within ±120% of the range. + + Besides a number, the level can be set to "MIN", "MAX" or + "DEF", which the instrument converts to the corresponding + number. Getting the parameter always returns a number, but + until it is read again, the cached value is the string that + was set. The value is therefore annotated as + ``float | str``. """ _trigger_source_docstring = textwrap.dedent("""\ @@ -245,7 +283,7 @@ def __init__( else: _max_sample_count = int(1e6) - self.count: Parameter = self.add_parameter( + self.count: Parameter[int | str, Self] = self.add_parameter( "count", label="Sample Count", set_cmd="SAMPle:COUNt {}", @@ -265,7 +303,13 @@ def __init__( MEM option). For the model 34410A the maximum is 50,000 readings, and for the model 34411A the maximum is 1,000,000 readings. The latter does - not depend on the pretrigger count.""" + not depend on the pretrigger count. + + Besides a number, the count can be set to "MIN", "MAX" or "DEF", + which the instrument converts to the corresponding number. Getting + the parameter always returns a number, but until it is read again, + the cached value is the string that was set. The value is therefore + annotated as ``int | str``.""" ), ) """Specifies the number of measurements (samples) the instrument @@ -277,7 +321,13 @@ def __init__( MEM option). For the model 34410A the maximum is 50,000 readings, and for the model 34411A the maximum is 1,000,000 readings. The latter does -not depend on the pretrigger count.""" +not depend on the pretrigger count. + +Besides a number, the count can be set to "MIN", "MAX" or "DEF", +which the instrument converts to the corresponding number. Getting +the parameter always returns a number, but until it is read again, +the cached value is the string that was set. The value is therefore +annotated as ``int | str``.""" if self.parent.has_DIG or (self.parent.model == "34411A"): if self.parent.model == "34411A": @@ -287,7 +337,7 @@ def __init__( else: _max_pretrig_count = int(5e4) - 1 - self.pretrigger_count: Parameter = self.add_parameter( + self.pretrigger_count: Parameter[int | str, Self] = self.add_parameter( "pretrigger_count", label="Sample Pretrigger Count", set_cmd="SAMPle:COUNt:PRETrigger {}", @@ -306,7 +356,13 @@ def __init__( by the current number of sample counts as specified via the ``sample.count`` parameter. Refer to the doc of the ``sample.count`` parameter for information on the maximum - number of sample counts.""" + number of sample counts. + + Besides a number, the count can be set to "MIN", "MAX" or + "DEF", which the instrument converts to the corresponding + number. Getting the parameter always returns a number, but + until it is read again, the cached value is the string that + was set. The value is therefore annotated as ``int | str``.""" ), ) """Allows collection of the data being digitized the trigger. @@ -317,7 +373,13 @@ def __init__( by the current number of sample counts as specified via the ``sample.count`` parameter. Refer to the doc of the ``sample.count`` parameter for information on the maximum -number of sample counts.""" +number of sample counts. + +Besides a number, the count can be set to "MIN", "MAX" or +"DEF", which the instrument converts to the corresponding +number. Getting the parameter always returns a number, but +until it is read again, the cached value is the string that +was set. The value is therefore annotated as ``int | str``.""" if self.parent.is_34465A_34470A or self.parent.is_34410A_34411A: self.source: Parameter[str, Self] = self.add_parameter( @@ -332,7 +394,7 @@ def __init__( ) """Determines sampling time, immediate or using `sample.timer`""" - self.timer: Parameter = self.add_parameter( + self.timer: Parameter[float | str, Self] = self.add_parameter( "timer", label="Sample Timer", set_cmd="SAMPle:TIMer {}", @@ -358,7 +420,12 @@ def __init__( may generate a timing violation error when making measurements. Applying a value less than the absolute minimum will generate an - error.""" + error. + + Getting the parameter always returns a number, but until it is + read again after setting one of the special values, the cached + value is the string that was set. The value is therefore annotated + as ``float | str``.""" ), ) """The value is rounded by the instrument to the nearest step. For DC @@ -377,7 +444,12 @@ def __init__( may generate a timing violation error when making measurements. Applying a value less than the absolute minimum will generate an -error.""" +error. + +Getting the parameter always returns a number, but until it is +read again after setting one of the special values, the cached +value is the string that was set. The value is therefore annotated +as ``float | str``.""" self.timer_minimum: Parameter[float, Self] = self.add_parameter( "timer_minimum", @@ -784,7 +856,7 @@ def __init__( ) """Parameter range""" - self.resolution: Parameter = self.add_parameter( + self.resolution: Parameter[float | str, Self] = self.add_parameter( "resolution", get_cmd="SENSe:VOLTage:DC:RESolution?", get_parser=float, @@ -798,8 +870,8 @@ def __init__( measurements. The resolution is specified in the same units as the selected measurement function, not in number of digits. - You can also specify MIN (best resolution) or MAX (worst - resolution). + You can also specify MIN (best resolution), MAX (worst + resolution) or DEF (default resolution). To achieve normal mode (line frequency noise) rejection, use a resolution that corresponds to an integration time that is @@ -807,15 +879,20 @@ def __init__( Refer to "Resolution Table" or "Range, Resolution and NPLC" sections of the instrument's manual for the available ranges for - the resolution values.""" + the resolution values. + + Getting the parameter always returns a number, but until it is + read again after setting MIN, MAX or DEF, the cached value is the + string that was set. The value is therefore annotated as + ``float | str``.""" ), ) """Selects the measurement resolution for DC voltage and ratio measurements. The resolution is specified in the same units as the selected measurement function, not in number of digits. -You can also specify MIN (best resolution) or MAX (worst -resolution). +You can also specify MIN (best resolution), MAX (worst +resolution) or DEF (default resolution). To achieve normal mode (line frequency noise) rejection, use a resolution that corresponds to an integration time that is @@ -823,7 +900,12 @@ def __init__( Refer to "Resolution Table" or "Range, Resolution and NPLC" sections of the instrument's manual for the available ranges for -the resolution values.""" +the resolution values. + +Getting the parameter always returns a number, but until it is +read again after setting MIN, MAX or DEF, the cached value is the +string that was set. The value is therefore annotated as +``float | str``.""" self.autorange: Parameter[str, Self] = self.add_parameter( "autorange", @@ -941,7 +1023,7 @@ def __init__( mode is disabled (default), the integration time is set in PLC (power-line cycles).""" - self.aperture_time: Parameter = self.add_parameter( + self.aperture_time: Parameter[float | str, Self] = self.add_parameter( "aperture_time", label="Aperture time", set_cmd=self._set_apt_time, @@ -960,7 +1042,14 @@ def __init__( rejection characteristics (NPLC > 1). Setting the aperture time automatically enables the aperture - mode.""" + mode. + + Besides a number, the aperture time can be set to "MIN", + "MAX" or "DEF", which the instrument converts to the + corresponding number. Getting the parameter always returns a + number, but until it is read again, the cached value is the + string that was set. The value is therefore annotated as + ``float | str``.""" ), ) """Specifies the integration time in seconds (called aperture @@ -971,7 +1060,14 @@ def __init__( rejection characteristics (NPLC > 1). Setting the aperture time automatically enables the aperture -mode.""" +mode. + +Besides a number, the aperture time can be set to "MIN", +"MAX" or "DEF", which the instrument converts to the +corresponding number. Getting the parameter always returns a +number, but until it is read again, the cached value is the +string that was set. The value is therefore annotated as +``float | str``.""" #################################### # Submodules