Skip to content
Merged
9 changes: 9 additions & 0 deletions docs/changes/newsfragments/8678.improved_driver
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
``CopperMountainM5xxx.averages``, ``AimTTiChannel.curr_range``, ``RigolDG1062Burst.ncycles``,
``Keithley7510Sense.average_count`` and the ``dwell``, ``pause``, ``curve_number`` and
``excitation_range_number`` of ``LakeshoreModel372Channel`` now reject non-integer values.
Previously these were sent to the instrument, or for ``CopperMountainM5xxx.averages`` silently
truncated. Whole-number floats such as ``2.0`` are still accepted and sent as integers.
Also removed the incorrect unit of ``AimTTiChannel.curr_range``, which is a range number, fixed
the label of ``LakeshoreModel372Channel.temperature_coefficient``, and removed incorrect units
from ``RigolDS1074Z.trigger_mode``, ``waveform_npoints``, ``waveform_yorigin`` and
``waveform_yref``.
Comment thread
jenshnielsen marked this conversation as resolved.
7 changes: 4 additions & 3 deletions src/qcodes/instrument_drivers/AimTTi/_AimTTi_PL_P.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,15 @@ def __init__(
)
"""Parameter curr"""

self.curr_range: Parameter = self.add_parameter(
self.curr_range: Parameter[int, Self] = self.add_parameter(
"curr_range",
get_cmd=f"IRANGE{channel}?",
get_parser=int,
set_cmd=self._set_current_range,
label="Current Range",
unit="A",
vals=vals.Numbers(1, 2),
# accept whole-number floats such as 2.0 but send them as int
vals=vals.PermissiveInts(1, 2),
set_parser=round,
docstring="Set the current range of the output. "
"Here, the integer 1 is for the Low range, "
"and integer 2 is for the High range.",
Expand Down
9 changes: 5 additions & 4 deletions src/qcodes/instrument_drivers/CopperMountain/_M5xxx.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
ParamRawDataType,
create_on_off_val_mapping,
)
from qcodes.validators import Bool, Enum, Ints, Numbers
from qcodes.validators import Bool, Enum, Ints, Numbers, PermissiveInts

if TYPE_CHECKING:
from typing import Unpack
Expand Down Expand Up @@ -114,17 +114,18 @@ def __init__(
)
"""Turns the averaging trigger function ON/OFF."""

self.averages: Parameter = self.add_parameter(
self.averages: Parameter[int, Self] = self.add_parameter(
"averages",
label="Averages",
get_cmd="SENS1:AVER:COUN?",
set_cmd="SENS1:AVER:COUN {}",
get_parser=int,
set_parser=int,
set_parser=round,
unit="",
docstring="Sets or reads out the averaging factor "
"when the averaging function is turned on.",
vals=Numbers(min_value=1, max_value=999),
# accept whole-number floats such as 2.0 but send them as int
vals=PermissiveInts(min_value=1, max_value=999),
)
"""Sets or reads out the averaging factor
when the averaging function is turned on."""
Expand Down
6 changes: 4 additions & 2 deletions src/qcodes/instrument_drivers/Keithley/Keithley_7510.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
create_on_off_val_mapping,
invert_val_mapping,
)
from qcodes.validators import Arrays, Enum, Ints, Lists, Numbers
from qcodes.validators import Arrays, Enum, Ints, Lists, Numbers, PermissiveInts

if TYPE_CHECKING:
from collections.abc import Sequence
Expand Down Expand Up @@ -589,7 +589,9 @@ def __init__(
get_cmd=f":SENSe:{self._proper_function}:AVERage:COUNt?",
set_cmd=f":SENSe:{self._proper_function}:AVERage:COUNt {{}}",
get_parser=int,
vals=Numbers(1, 100),
# accept whole-number floats such as 10.0 but send them as int
vals=PermissiveInts(1, 100),
set_parser=round,
docstring="Set the number of measurements that are averaged when "
"filtering is enabled.",
)
Expand Down
15 changes: 10 additions & 5 deletions src/qcodes/instrument_drivers/Lakeshore/Lakeshore_model_372.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,9 @@ def __init__(
docstring="Specifies a value for the autoscanning dwell time.",
unit="s",
get_parser=int,
vals=vals.Numbers(1, 200),
# accept whole-number floats such as 10.0 but send them as int
vals=vals.PermissiveInts(1, 200),
set_parser=round,
parameter_class=GroupParameter,
)
"""Specifies a value for the autoscanning dwell time."""
Expand All @@ -289,7 +291,8 @@ def __init__(
docstring="Specifies a value for the change pause time",
unit="s",
get_parser=int,
vals=vals.Numbers(3, 200),
vals=vals.PermissiveInts(3, 200),
set_parser=round,
parameter_class=GroupParameter,
)
"""Specifies a value for the change pause time"""
Expand All @@ -301,7 +304,8 @@ def __init__(
"curves. Do not change this parameter "
"unless you know what you are doing.",
get_parser=int,
vals=vals.Numbers(0, 59),
vals=vals.PermissiveInts(0, 59),
set_parser=round,
parameter_class=GroupParameter,
)
"""
Expand All @@ -310,7 +314,7 @@ def __init__(
"""
self.temperature_coefficient: GroupParameter = self.add_parameter(
"temperature_coefficient",
label="Change pause time",
label="Temperature coefficient",
docstring="Sets the temperature coefficient that "
"will be used for temperature control if "
"no curve is selected (negative or "
Expand Down Expand Up @@ -363,7 +367,8 @@ def __init__(
"current excitation); refer to the manual "
"for the table of ranges",
get_parser=int,
vals=vals.Numbers(1, 22),
vals=vals.PermissiveInts(1, 22),
set_parser=round,
parameter_class=GroupParameter,
)
"""
Expand Down
4 changes: 3 additions & 1 deletion src/qcodes/instrument_drivers/rigol/Rigol_DG1062.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ def __init__(
set_cmd=f":SOUR{channel}:BURS:NCYC {{}}",
# returned in scientific notation, e.g. 1.000000E+01
get_parser=lambda s: int(float(s)),
vals=vals.Numbers(min_value=1, max_value=500000),
# accept whole-number floats such as 10.0 but send them as int
vals=vals.PermissiveInts(min_value=1, max_value=500000),
set_parser=round,
)
"""Parameter ncycles"""

Expand Down
12 changes: 8 additions & 4 deletions src/qcodes/instrument_drivers/rigol/Rigol_DS1074Z.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,15 @@ def __init__(
"waveform_npoints",
get_cmd="WAV:POIN?",
set_cmd="WAV:POIN {}",
unit="s",
get_parser=int,
)
"""Parameter waveform_npoints"""

self.waveform_yorigin: Parameter[float, Self] = self.add_parameter(
"waveform_yorigin", get_cmd="WAVeform:YORigin?", unit="V", get_parser=float
# vertical offset in digitizing levels
"waveform_yorigin",
get_cmd="WAVeform:YORigin?",
get_parser=float,
)
"""Parameter waveform_yorigin"""

Expand All @@ -139,15 +141,17 @@ def __init__(
"""Parameter waveform_yincrem"""

self.waveform_yref: Parameter[float, Self] = self.add_parameter(
"waveform_yref", get_cmd=":WAVeform:YREFerence?", unit="V", get_parser=float
# vertical reference position in digitizing levels
"waveform_yref",
get_cmd=":WAVeform:YREFerence?",
get_parser=float,
)
"""Parameter waveform_yref"""

self.trigger_mode: Parameter[str, Self] = self.add_parameter(
"trigger_mode",
get_cmd=":TRIGger:MODE?",
set_cmd=":TRIGger:MODE {}",
unit="V",
vals=Enum("edge", "pulse", "video", "pattern"),
get_parser=str,
)
Expand Down
31 changes: 31 additions & 0 deletions tests/drivers/test_aimtti_pl601p.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from typing import TYPE_CHECKING

import numpy as np
import pytest

from qcodes.instrument_drivers.AimTTi import AimTTiPL601

if TYPE_CHECKING:
from collections.abc import Generator

from pytest_mock import MockerFixture


@pytest.fixture(scope="function", name="driver")
def _make_driver() -> "Generator[AimTTiPL601, None, None]":
Expand All @@ -25,3 +28,31 @@ def test_idn(driver: AimTTiPL601) -> None:
"serial": "514710",
"vendor": "THURLBY THANDAR",
} == driver.IDN()


@pytest.mark.parametrize(
"value", [2, 2.0, np.float64(2)], ids=["int", "float", "numpy_float"]
)
def test_curr_range_accepts_whole_numbers(
driver: AimTTiPL601, mocker: "MockerFixture", value: float
) -> None:
# the range is 1 (low) or 2 (high); whole-number floats are sent as int
channel = driver.ch1
# the output is switched off while changing the range; start from off so
# that its state does not have to be queried
channel.output.cache.set(False)
write_raw = mocker.patch.object(driver, "write_raw")

channel.curr_range(value) # pyright: ignore[reportArgumentType]

write_raw.assert_any_call("IRANGE1 2")


def test_curr_range_rejects_non_integer(
driver: AimTTiPL601, mocker: "MockerFixture"
) -> None:
write_raw = mocker.patch.object(driver, "write_raw")

with pytest.raises(TypeError):
driver.ch1.curr_range(1.5) # pyright: ignore[reportArgumentType]
write_raw.assert_not_called()
27 changes: 27 additions & 0 deletions tests/drivers/test_coppermountain_m5065.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from typing import TYPE_CHECKING

import numpy as np
import pytest

from qcodes.instrument_drivers.CopperMountain import CopperMountainM5065

if TYPE_CHECKING:
from collections.abc import Generator

from pytest_mock import MockerFixture


class DummyM5065:
def __init__(self, *args: object, **kwargs: object) -> None:
Expand Down Expand Up @@ -66,6 +69,30 @@ def test_averages_param(vna: CopperMountainM5065) -> None:
assert vna.averages() == 2


@pytest.mark.parametrize("value", [3.0, np.float64(3)], ids=["float", "numpy_float"])
def test_averages_accepts_whole_floats(
vna: CopperMountainM5065, mocker: "MockerFixture", value: float
) -> None:
# whole-number floats, e.g. from numpy, are accepted and sent as int
write_raw = mocker.spy(vna, "write_raw")

vna.averages(value) # pyright: ignore[reportArgumentType]

write_raw.assert_called_once_with("SENS1:AVER:COUN 3")
assert vna.averages() == 3


def test_averages_rejects_non_integer(
vna: CopperMountainM5065, mocker: "MockerFixture"
) -> None:
# previously 2.5 was silently truncated to 2 by set_parser=int
write_raw = mocker.spy(vna, "write_raw")

with pytest.raises(TypeError):
vna.averages(2.5) # pyright: ignore[reportArgumentType]
write_raw.assert_not_called()


def test_averages_enabled_enables_averages_trigger(
vna: CopperMountainM5065,
) -> None:
Expand Down
27 changes: 27 additions & 0 deletions tests/drivers/test_keithley_7510.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING

import hypothesis.strategies as st
import numpy as np
import pytest
from hypothesis import HealthCheck, given, settings

Expand All @@ -9,6 +10,8 @@
if TYPE_CHECKING:
from collections.abc import Generator

from pytest_mock import MockerFixture


@pytest.fixture(scope="function")
def dmm_7510_driver() -> "Generator[Keithley7510, None, None]":
Expand Down Expand Up @@ -82,6 +85,30 @@ def test_average_count_is_int(dmm_7510_driver: Keithley7510) -> None:
assert average_count == 25


@pytest.mark.parametrize("value", [25.0, np.float64(25)], ids=["float", "numpy_float"])
def test_average_count_accepts_whole_floats(
dmm_7510_driver: Keithley7510, mocker: "MockerFixture", value: float
) -> None:
# whole-number floats, e.g. from numpy, are accepted and sent as int
sense = dmm_7510_driver.sense
write_raw = mocker.spy(dmm_7510_driver, "write_raw")

sense.average_count(value) # pyright: ignore[reportArgumentType]

write_raw.assert_called_once_with(":SENSe:voltage:AVERage:COUNt 25")
assert sense.average_count() == 25


def test_average_count_rejects_non_integer(
dmm_7510_driver: Keithley7510, mocker: "MockerFixture"
) -> None:
write_raw = mocker.spy(dmm_7510_driver, "write_raw")

with pytest.raises(TypeError):
dmm_7510_driver.sense.average_count(2.5) # pyright: ignore[reportArgumentType]
write_raw.assert_not_called()


def test_digitize_acq_rate_is_int(dmm_7510_driver: Keithley7510) -> None:
acq_rate = dmm_7510_driver.digi_sense.acq_rate()
assert isinstance(acq_rate, int)
Expand Down
44 changes: 44 additions & 0 deletions tests/drivers/test_lakeshore_372.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Literal

import numpy as np
import pytest

from qcodes.instrument import Instrument, InstrumentBase
Expand All @@ -19,6 +20,8 @@
if TYPE_CHECKING:
from collections.abc import Callable, Generator

from pytest_mock import MockerFixture

log = logging.getLogger(__name__)

VISA_LOGGER = f"{InstrumentBase.__module__}.com.visa"
Expand Down Expand Up @@ -296,6 +299,47 @@ def test_setpoint(lakeshore_372: LakeshoreModel372Mock) -> None:
assert h.setpoint() == setpoint


# the simulated channel 1 starts with INSET 1,100,3,0,1 and INTYPE 0,1,0,5,0,1
@pytest.mark.parametrize(
("name", "command"),
[
("dwell", "INSET 1, 1, 10, 3, 0, 1"),
("pause", "INSET 1, 1, 100, 10, 0, 1"),
("curve_number", "INSET 1, 1, 100, 3, 10, 1"),
("excitation_range_number", "INTYPE 1, 0, 10, 0, 5, 0, 1"),
],
)
@pytest.mark.parametrize("value", [10.0, np.float64(10)], ids=["float", "numpy_float"])
def test_channel_integer_settings_accept_whole_floats(
lakeshore_372: LakeshoreModel372Mock,
mocker: MockerFixture,
name: str,
command: str,
value: float,
) -> None:
# whole-number floats, e.g. from numpy, are accepted and sent as int
param = lakeshore_372.channels[0].parameters[name]
write_raw = mocker.patch.object(lakeshore_372, "write_raw")

param(value)

write_raw.assert_called_once_with(command)


@pytest.mark.parametrize(
"name", ["dwell", "pause", "curve_number", "excitation_range_number"]
)
def test_channel_integer_settings_reject_non_integer(
lakeshore_372: LakeshoreModel372Mock, mocker: MockerFixture, name: str
) -> None:
param = lakeshore_372.channels[0].parameters[name]
write_raw = mocker.patch.object(lakeshore_372, "write_raw")

with pytest.raises(TypeError):
param(10.5)
write_raw.assert_not_called()


def test_select_range_limits(lakeshore_372: LakeshoreModel372Mock) -> None:
h = lakeshore_372.sample_heater
ranges = list(range(1, 9))
Expand Down
Loading
Loading