From 9e47eb345863c5d377dbbd77507b36ebdc951963 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 16 Dec 2025 13:54:23 +0100 Subject: [PATCH 01/15] Make use of generic parameters in ZNB driver --- .../instrument_drivers/rohde_schwarz/ZNB.py | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/qcodes/instrument_drivers/rohde_schwarz/ZNB.py b/src/qcodes/instrument_drivers/rohde_schwarz/ZNB.py index 23cc98998dc1..1529846de6c1 100644 --- a/src/qcodes/instrument_drivers/rohde_schwarz/ZNB.py +++ b/src/qcodes/instrument_drivers/rohde_schwarz/ZNB.py @@ -8,7 +8,6 @@ import qcodes.validators as vals from qcodes.instrument import ( ChannelList, - Instrument, InstrumentBaseKWArgs, InstrumentChannel, VisaInstrument, @@ -19,7 +18,6 @@ ManualParameter, MultiParameter, Parameter, - ParamRawDataType, create_on_off_val_mapping, ) @@ -29,7 +27,12 @@ log = logging.getLogger(__name__) -class FixedFrequencyTraceIQ(MultiParameter): +class FixedFrequencyTraceIQ( + MultiParameter[ + tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]], + "RohdeSchwarzZNBChannel", + ] +): """ Parameter for sweep that returns the real (I) and imaginary (Q) parts of the VNA response. @@ -94,12 +97,16 @@ def get_raw(self) -> tuple[npt.NDArray, npt.NDArray]: `cw_check_sweep_first` is set to `True` then at the cost of a few ms overhead checks if the vna is setup correctly. """ - assert isinstance(self.instrument, RohdeSchwarzZNBChannel) i, q = self.instrument._get_cw_data() return i, q -class FixedFrequencyPointIQ(MultiParameter): +class FixedFrequencyPointIQ( + MultiParameter[ + tuple[float, float], + "RohdeSchwarzZNBChannel", + ] +): """ Parameter for sweep that returns the mean of the real (I) and imaginary (Q) parts of the VNA response. @@ -142,12 +149,16 @@ def get_raw(self) -> tuple[float, float]: parameter `cw_check_sweep_first` is set to `True` then at the cost of a few ms overhead checks if the vna is setup correctly. """ - assert isinstance(self.instrument, RohdeSchwarzZNBChannel) i, q = self.instrument._get_cw_data() return float(np.mean(i)), float(np.mean(q)) -class FixedFrequencyPointMagPhase(MultiParameter): +class FixedFrequencyPointMagPhase( + MultiParameter[ + tuple[float, float], + "RohdeSchwarzZNBChannel", + ] +): """ Parameter for sweep that returns the magnitude of mean of the real (I) and imaginary (Q) parts of the VNA response and it's phase. @@ -185,20 +196,24 @@ def __init__( **kwargs, ) - def get_raw(self) -> tuple[float, ...]: + def get_raw(self) -> tuple[float, float]: """ Gets the magnitude and phase of the mean of the raw real and imaginary part of the data. If the parameter `cw_check_sweep_first` is set to `True` for the instrument then at the cost of a few ms overhead checks if the vna is setup correctly. """ - assert isinstance(self.instrument, RohdeSchwarzZNBChannel) i, q = self.instrument._get_cw_data() s = np.mean(i) + 1j * np.mean(q) return float(np.abs(s)), float(np.angle(s)) -class FrequencySweepMagPhase(MultiParameter): +class FrequencySweepMagPhase( + MultiParameter[ + tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]], + "RohdeSchwarzZNBChannel", + ] +): """ Sweep that return magnitude and phase. """ @@ -247,14 +262,18 @@ def set_sweep(self, start: float, stop: float, npts: int) -> None: self.setpoints = ((f,), (f,)) self.shapes = ((npts,), (npts,)) - def get_raw(self) -> tuple[ParamRawDataType, ...]: - assert isinstance(self.instrument, RohdeSchwarzZNBChannel) + def get_raw(self) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]: with self.instrument.format.set_to("Complex"): data = self.instrument._get_sweep_data(force_polar=True) return abs(data), np.angle(data) -class FrequencySweepDBPhase(MultiParameter): +class FrequencySweepDBPhase( + MultiParameter[ + tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]], + "RohdeSchwarzZNBChannel", + ] +): """ Sweep that return magnitude in decibel (dB) and phase in radians. """ @@ -303,14 +322,18 @@ def set_sweep(self, start: float, stop: float, npts: int) -> None: self.setpoints = ((f,), (f,)) self.shapes = ((npts,), (npts,)) - def get_raw(self) -> tuple[ParamRawDataType, ...]: - assert isinstance(self.instrument, RohdeSchwarzZNBChannel) + def get_raw(self) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]: with self.instrument.format.set_to("Complex"): data = self.instrument._get_sweep_data(force_polar=True) return 20 * np.log10(np.abs(data)), np.angle(data) -class FrequencySweep(ArrayParameter): +class FrequencySweep( + ArrayParameter[ + npt.NDArray[np.floating], + "RohdeSchwarzZNBChannel", + ] +): """ Hardware controlled parameter class for Rohde Schwarz ZNB trace. @@ -332,7 +355,7 @@ class FrequencySweep(ArrayParameter): def __init__( self, name: str, - instrument: Instrument, + instrument: "RohdeSchwarzZNBChannel", start: float, stop: float, npts: int, @@ -370,8 +393,7 @@ def set_sweep(self, start: float, stop: float, npts: int) -> None: self.setpoints = (f,) self.shape = (npts,) - def get_raw(self) -> ParamRawDataType: - assert isinstance(self.instrument, RohdeSchwarzZNBChannel) + def get_raw(self) -> npt.NDArray[np.floating]: return self.instrument._get_sweep_data() @@ -1087,7 +1109,7 @@ def __init__( self._max_freq: float self._min_freq, self._max_freq = m_frequency[model] - self.num_ports: Parameter = self.add_parameter( + self.num_ports: Parameter[int, RohdeSchwarzZNBBase] = self.add_parameter( name="num_ports", get_cmd="INST:PORT:COUN?", get_parser=int ) """Parameter num_ports""" From 545af996986a02d7fb8a47985db53283f3c25688 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:26:40 +0100 Subject: [PATCH 02/15] Make KeysightB1517A use generic parameter args --- .../keysightb1500/KeysightB1500_module.py | 4 ++- .../Keysight/keysightb1500/KeysightB1517A.py | 25 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_module.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_module.py index d75e54840d9d..7c17819552ac 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_module.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_module.py @@ -279,7 +279,9 @@ def _convert_to_nan_if_dummy_value(value: float) -> float: return float("nan") if value > 1e99 else value -class KeysightB1500Module(InstrumentChannel): +class KeysightB1500Module( + InstrumentChannel["qcodes.instrument_drivers.Keysight.keysightb1500.KeysightB1500"] +): """Base class for all modules of B1500 Parameter Analyzer When subclassing, diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py index d1ccb9912282..3cc2dfd84a0f 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py @@ -1,6 +1,6 @@ import re import textwrap -from typing import TYPE_CHECKING, Any, Literal, NotRequired, cast, overload +from typing import TYPE_CHECKING, Any, Generic, Literal, NotRequired, overload import numpy as np import numpy.typing as npt @@ -9,6 +9,7 @@ import qcodes.validators as vals from qcodes.instrument import InstrumentBaseKWArgs, InstrumentChannel from qcodes.parameters import Group, GroupParameter, Parameter, ParamRawDataType +from qcodes.parameters.parameter_base import ParameterDataTypeVar from . import constants from .constants import ( @@ -50,7 +51,7 @@ class SweepSteps(TypedDict): power_compliance: float | None -class KeysightB1500IVSweeper(InstrumentChannel): +class KeysightB1500IVSweeper(InstrumentChannel["KeysightB1517A"]): def __init__( self, parent: "KeysightB1517A", @@ -701,7 +702,9 @@ def _get_sweep_steps_parser(response: str) -> SweepSteps: """ -class _ParameterWithStatus(Parameter): +class _ParameterWithStatus( + Parameter[ParameterDataTypeVar, "KeysightB1517A"], Generic[ParameterDataTypeVar] +): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -726,9 +729,11 @@ def snapshot_base( return snapshot -class _SpotMeasurementVoltageParameter(_ParameterWithStatus): +class _SpotMeasurementVoltageParameter( + _ParameterWithStatus[ParameterDataTypeVar], Generic[ParameterDataTypeVar] +): def set_raw(self, value: ParamRawDataType) -> None: - smu = cast("KeysightB1517A", self.instrument) + smu = self.instrument if smu._source_config["output_range"] is None: smu._source_config["output_range"] = constants.VOutputRange.AUTO @@ -752,7 +757,7 @@ def set_raw(self, value: ParamRawDataType) -> None: ) def get_raw(self) -> ParamRawDataType: - smu = cast("KeysightB1517A", self.instrument) + smu = self.instrument msg = MessageBuilder().tv( chnum=smu.channels[0], @@ -767,9 +772,11 @@ def get_raw(self) -> ParamRawDataType: return parsed["value"] -class _SpotMeasurementCurrentParameter(_ParameterWithStatus): +class _SpotMeasurementCurrentParameter( + _ParameterWithStatus[ParameterDataTypeVar], Generic[ParameterDataTypeVar] +): def set_raw(self, value: ParamRawDataType) -> None: - smu = cast("KeysightB1517A", self.instrument) + smu = self.instrument if smu._source_config["output_range"] is None: smu._source_config["output_range"] = constants.IOutputRange.AUTO @@ -793,7 +800,7 @@ def set_raw(self, value: ParamRawDataType) -> None: ) def get_raw(self) -> ParamRawDataType: - smu = cast("KeysightB1517A", self.instrument) + smu = self.instrument msg = MessageBuilder().ti( chnum=smu.channels[0], From 127915da0b2fc100efa1bca01857027b40f4cd7e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:28:13 +0100 Subject: [PATCH 03/15] Improve type checking of message_builder --- .../Keysight/keysightb1500/message_builder.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py index 10efae3fa635..d60ef45f4ca0 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py @@ -1,41 +1,39 @@ -from collections.abc import Callable from functools import wraps from operator import xor -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Generic, ParamSpec, TypeVar from . import constants if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable -def as_csv(comps: "Iterable[Any]", sep: str = ",") -> str: +def as_csv(comps: "Iterable[object]", sep: str = ",") -> str: """Returns items in iterable ls as comma-separated string""" return sep.join(format(x) for x in comps) -MessageBuilderMethodT = TypeVar( - "MessageBuilderMethodT", bound=Callable[..., "MessageBuilder"] -) +P = ParamSpec("P") +T = TypeVar("T") -def final_command(f: MessageBuilderMethodT) -> MessageBuilderMethodT: +def final_command(f: "Callable[P, MessageBuilder]") -> "Callable[P, MessageBuilder]": @wraps(f) - def wrapper(*args: Any, **kwargs: Any) -> "MessageBuilder": + def wrapper(*args: P.args, **kwargs: P.kwargs) -> "MessageBuilder": res: MessageBuilder = f(*args, **kwargs) res._msg.set_final() return res - return cast("MessageBuilderMethodT", wrapper) + return wrapper -class CommandList(list[Any]): +class CommandList(list[T], Generic[T]): def __init__(self) -> None: super().__init__() self.is_final = False - def append(self, obj: Any) -> None: + def append(self, obj: T) -> None: if self.is_final: raise ValueError( f"Cannot add commands after `{self[-1]}`. " @@ -67,7 +65,7 @@ class MessageBuilder: """ def __init__(self) -> None: - self._msg = CommandList() + self._msg: CommandList[str] = CommandList() @property def message(self) -> str: From 7f643d9f90f3c995ec507fe41993e14e563fbf54 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:37:23 +0100 Subject: [PATCH 04/15] Make use of generic parameters in KeysightN9030B driver --- .../Keysight/Keysight_N9030B.py | 208 ++++++++++-------- 1 file changed, 121 insertions(+), 87 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py b/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py index d401b2b9f46c..40ff4dcd3653 100644 --- a/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py +++ b/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py @@ -1,8 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Generic, Literal import numpy as np +import numpy.typing as npt +from typing_extensions import TypeVar from qcodes.instrument import ( InstrumentBaseKWArgs, @@ -16,6 +18,7 @@ ParamRawDataType, create_on_off_val_mapping, ) +from qcodes.parameters.parameter_base import ParameterDataTypeVar from qcodes.validators import Arrays, Bool, Enum, Ints, Numbers if TYPE_CHECKING: @@ -23,56 +26,65 @@ from typing_extensions import Unpack +_T = TypeVar( + "_T", + bound="KeysightN9030BSpectrumAnalyzerMode | KeysightN9030BPhaseNoiseMode", + default="KeysightN9030BSpectrumAnalyzerMode | KeysightN9030BPhaseNoiseMode", +) + -class FrequencyAxis(Parameter): +class FrequencyAxis( + Parameter[ + npt.NDArray[np.float64], + _T, + ], + Generic[_T], +): def __init__( self, - start: Parameter, - stop: Parameter, - npts: Parameter, + start: Parameter[float, _T], + stop: Parameter[float, _T], + npts: Parameter[int, _T], *args: Any, **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) - self._start: Parameter = start - self._stop: Parameter = stop - self._npts: Parameter = npts + self._start = start + self._stop = stop + self._npts = npts - def get_raw(self) -> ParamRawDataType: + def get_raw(self) -> npt.NDArray[np.float64]: start_val = self._start() stop_val = self._stop() npts_val = self._npts() - assert start_val is not None - assert stop_val is not None - assert npts_val is not None + if start_val is None or stop_val is None or npts_val is None: + raise RuntimeError("Start, Stop and Npts parameters must be set.") return np.linspace(start_val, stop_val, npts_val) -class Trace(ParameterWithSetpoints): +class Trace( + ParameterWithSetpoints[ParameterDataTypeVar, _T], Generic[ParameterDataTypeVar, _T] +): def __init__( self, number: int, *args: Any, - get_data: Callable[[int], ParamRawDataType], + get_data: Callable[[int], ParameterDataTypeVar], **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) - # the parameter classes should ideally be generic in instrument - # and root instrument classes so we can specialize here. - # for now we have to ignore a type error from pyright - self.instrument: ( - KeysightN9030BSpectrumAnalyzerMode | KeysightN9030BPhaseNoiseMode - ) + # while the parameter classes should ideally be generic in instrument + # type it is not generic in the root instrument type self.root_instrument: KeysightN9030B self.number = number self.get_data = get_data - def get_raw(self) -> ParamRawDataType: + def get_raw(self) -> ParameterDataTypeVar: return self.get_data(self.number) -class KeysightN9030BSpectrumAnalyzerMode(InstrumentChannel): +class KeysightN9030BSpectrumAnalyzerMode(InstrumentChannel["KeysightN9030B"]): """ Spectrum Analyzer Mode for Keysight N9030B instrument. """ @@ -86,6 +98,7 @@ def __init__( **kwargs: Unpack[InstrumentBaseKWArgs], ): super().__init__(parent, name, *arg, **kwargs) + self.root_instrument: KeysightN9030B self._additional_wait = additional_wait self._min_freq = -8e7 @@ -104,68 +117,82 @@ def __init__( self._max_freq = self._valid_max_freq[opt] # Frequency Parameters - self.start: Parameter = self.add_parameter( - name="start", - unit="Hz", - get_cmd=":SENSe:FREQuency:STARt?", - set_cmd=self._set_start, - get_parser=float, - vals=Numbers(self._min_freq, self._max_freq - 10), - docstring="Start Frequency", + self.start: Parameter[float, KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="start", + unit="Hz", + get_cmd=":SENSe:FREQuency:STARt?", + set_cmd=self._set_start, + get_parser=float, + vals=Numbers(self._min_freq, self._max_freq - 10), + docstring="Start Frequency", + ) ) """Start Frequency""" - self.stop: Parameter = self.add_parameter( - name="stop", - unit="Hz", - get_cmd=":SENSe:FREQuency:STOP?", - set_cmd=self._set_stop, - get_parser=float, - vals=Numbers(self._min_freq + 10, self._max_freq), - docstring="Stop Frequency", + self.stop: Parameter[float, KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="stop", + unit="Hz", + get_cmd=":SENSe:FREQuency:STOP?", + set_cmd=self._set_stop, + get_parser=float, + vals=Numbers(self._min_freq + 10, self._max_freq), + docstring="Stop Frequency", + ) ) """Stop Frequency""" - self.center: Parameter = self.add_parameter( - name="center", - unit="Hz", - get_cmd=":SENSe:FREQuency:CENTer?", - set_cmd=self._set_center, - get_parser=float, - vals=Numbers(self._min_freq + 5, self._max_freq - 5), - docstring="Sets and gets center frequency", + self.center: Parameter[float, KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="center", + unit="Hz", + get_cmd=":SENSe:FREQuency:CENTer?", + set_cmd=self._set_center, + get_parser=float, + vals=Numbers(self._min_freq + 5, self._max_freq - 5), + docstring="Sets and gets center frequency", + ) ) """Sets and gets center frequency""" - self.span: Parameter = self.add_parameter( - name="span", - unit="Hz", - get_cmd=":SENSe:FREQuency:SPAN?", - set_cmd=self._set_span, - get_parser=float, - vals=Numbers(10, self._max_freq - self._min_freq), - docstring="Changes span of frequency", + self.span: Parameter[float, KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="span", + unit="Hz", + get_cmd=":SENSe:FREQuency:SPAN?", + set_cmd=self._set_span, + get_parser=float, + vals=Numbers(10, self._max_freq - self._min_freq), + docstring="Changes span of frequency", + ) ) """Changes span of frequency""" - self.npts: Parameter = self.add_parameter( - name="npts", - get_cmd=":SENSe:SWEep:POINts?", - set_cmd=":SENSe:SWEep:POINts {}", - get_parser=int, - vals=Ints(1, 20001), - docstring="Number of points for the sweep", + self.npts: Parameter[int, KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="npts", + get_cmd=":SENSe:SWEep:POINts?", + set_cmd=":SENSe:SWEep:POINts {}", + get_parser=int, + vals=Ints(1, 20001), + docstring="Number of points for the sweep", + ) ) """Number of points for the sweep""" # Amplitude/Input Parameters - self.mech_attenuation: Parameter = self.add_parameter( - name="mech_attenuation", - unit="dB", - get_cmd=":SENS:POW:ATT?", - set_cmd=":SENS:POW:ATT {}", - get_parser=int, - vals=Ints(0, 70), - docstring="Internal mechanical attenuation", + self.mech_attenuation: Parameter[int, KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="mech_attenuation", + unit="dB", + get_cmd=":SENS:POW:ATT?", + set_cmd=":SENS:POW:ATT {}", + get_parser=int, + vals=Ints(0, 70), + docstring="Internal mechanical attenuation", + ) ) """Internal mechanical attenuation""" - self.preamp: Parameter = self.add_parameter( + self.preamp: Parameter[ + Literal["LOW", "FULL"], KeysightN9030BSpectrumAnalyzerMode + ] = self.add_parameter( name="preamp", get_cmd=":SENS:POW:GAIN:BAND?", set_cmd=":SENS:POW:GAIN:BAND {}", @@ -354,20 +381,25 @@ def __init__( """Sets up sweep type. Possible options are 'fft' and 'sweep'.""" # Array (Data) Parameters - self.freq_axis: FrequencyAxis = self.add_parameter( - name="freq_axis", - label="Frequency", - unit="Hz", - start=self.start, - stop=self.stop, - npts=self.npts, - vals=Arrays(shape=(self.npts.get_latest,)), - parameter_class=FrequencyAxis, - docstring="Creates frequency axis for the sweep from start, " - "stop and npts values.", + self.freq_axis: FrequencyAxis[KeysightN9030BSpectrumAnalyzerMode] = ( + self.add_parameter( + name="freq_axis", + label="Frequency", + unit="Hz", + start=self.start, + stop=self.stop, + npts=self.npts, + vals=Arrays(shape=(self.npts.get_latest,)), + parameter_class=FrequencyAxis[KeysightN9030BSpectrumAnalyzerMode], + docstring="Creates frequency axis for the sweep from start, " + "stop and npts values.", + ) ) + """Creates frequency axis for the sweep from start, stop and npts values.""" - self.trace: Trace = self.add_parameter( + self.trace: Trace[ + npt.NDArray[np.float64], KeysightN9030BSpectrumAnalyzerMode + ] = self.add_parameter( name="trace", label="Trace", unit="dB", @@ -375,7 +407,9 @@ def __init__( vals=Arrays(shape=(self.npts.get_latest,)), setpoints=(self.freq_axis,), get_data=self._get_data, - parameter_class=Trace, + parameter_class=Trace[ + npt.NDArray[np.float64], KeysightN9030BSpectrumAnalyzerMode + ], docstring="Gets trace data.", ) """Gets trace data.""" @@ -418,7 +452,7 @@ def _set_span(self, val: float) -> None: self.write(f":SENSe:FREQuency:SPAN {val}") self.update_trace() - def _get_data(self, trace_num: int) -> ParamRawDataType: + def _get_data(self, trace_num: int) -> npt.NDArray[np.float64]: """ Gets data from the measurement. """ @@ -443,8 +477,8 @@ def _get_data(self, trace_num: int) -> ParamRawDataType: is_big_endian=False, ) - data = np.array(data).reshape((-1, 2)) - return data[:, 1] + data_array = np.array(data).reshape((-1, 2)) + return data_array[:, 1] def update_trace(self) -> None: """ @@ -481,7 +515,7 @@ def autotune(self) -> None: self.center() -class KeysightN9030BPhaseNoiseMode(InstrumentChannel): +class KeysightN9030BPhaseNoiseMode(InstrumentChannel["KeysightN9030B"]): """ Phase Noise Mode for Keysight N9030B instrument. """ From 8c0c9f09e61bfd9ed8dafe72ce7be2a9d2d54fab Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:46:21 +0100 Subject: [PATCH 05/15] Make use of generic parameters in DPO7200xx driver --- src/qcodes/instrument_drivers/tektronix/DPO7200xx.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py b/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py index 57929e2c0c58..f179ff5b71e5 100644 --- a/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py +++ b/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py @@ -7,7 +7,7 @@ import textwrap import time from functools import partial -from typing import TYPE_CHECKING, Any, ClassVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, Generic import numpy as np import numpy.typing as npt @@ -26,6 +26,7 @@ ParameterWithSetpoints, create_on_off_val_mapping, ) +from qcodes.parameters.parameter_base import ParameterDataTypeVar from qcodes.validators import Arrays, Enum if TYPE_CHECKING: @@ -767,7 +768,10 @@ def _trigger_type(self, value: str) -> None: self.write(f"TRIGger:{self._identifier}:TYPE {value}") -class TektronixDPOMeasurementParameter(Parameter): +class TektronixDPOMeasurementParameter( + Parameter[ParameterDataTypeVar, "TektronixDPOMeasurement"], + Generic[ParameterDataTypeVar], +): """ A measurement parameter does not only return the instantaneous value of a measurement, but can also return some statistics. The accumulation @@ -778,7 +782,7 @@ class TektronixDPOMeasurementParameter(Parameter): """ def _get(self, metric: str) -> float: - measurement_channel = cast("TektronixDPOMeasurement", self.instrument) + measurement_channel = self.instrument if measurement_channel.type.get_latest() != self.name: measurement_channel.type(self.name) From 80b7b0555380506b1827d9a98abe4b5f5a96264c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:47:05 +0100 Subject: [PATCH 06/15] Make use of generic parameters in Yokogawa driver --- .../yokogawa/Yokogawa_GS200.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/qcodes/instrument_drivers/yokogawa/Yokogawa_GS200.py b/src/qcodes/instrument_drivers/yokogawa/Yokogawa_GS200.py index aabb4e981c86..eaeae9b0ebd3 100644 --- a/src/qcodes/instrument_drivers/yokogawa/Yokogawa_GS200.py +++ b/src/qcodes/instrument_drivers/yokogawa/Yokogawa_GS200.py @@ -1,5 +1,5 @@ from functools import partial -from typing import TYPE_CHECKING, Literal, cast +from typing import TYPE_CHECKING, Literal from qcodes.instrument import ( InstrumentBaseKWArgs, @@ -328,12 +328,14 @@ def __init__( ) """Parameter output""" - self.source_mode: Parameter = self.add_parameter( - "source_mode", - label="Source Mode", - get_cmd=":SOUR:FUNC?", - set_cmd=self._set_source_mode, - vals=Enum("VOLT", "CURR"), + self.source_mode: Parameter[Literal["VOLT", "CURR"], YokogawaGS200] = ( + self.add_parameter( + "source_mode", + label="Source Mode", + get_cmd=":SOUR:FUNC?", + set_cmd=self._set_source_mode, + vals=Enum("VOLT", "CURR"), + ) ) """Parameter source_mode""" @@ -410,12 +412,13 @@ def __init__( # We need to pass the source parameter for delegate parameters # (range and output_level) here according to the present # source_mode. - if self.source_mode() == "VOLT": - self.range.source = self.voltage_range - self.output_level.source = self.voltage - else: - self.range.source = self.current_range - self.output_level.source = self.current + match self.source_mode(): + case "VOLT": + self.range.source = self.voltage_range + self.output_level.source = self.voltage + case "CURR": + self.range.source = self.current_range + self.output_level.source = self.current self.voltage_limit: Parameter = self.add_parameter( "voltage_limit", @@ -429,7 +432,7 @@ def __init__( ) """Parameter voltage_limit""" - self.current_limit: Parameter = self.add_parameter( + self.current_limit: Parameter[float, YokogawaGS200] = self.add_parameter( "current_limit", label="Current Protection Limit", unit="I", @@ -621,10 +624,11 @@ def _set_output(self, output_level: float) -> None: ) else: mode = self.source_mode.get_latest() - if mode == "CURR": - self_range = 200e-3 - else: - self_range = 30.0 + match mode: + case "CURR": + self_range = 200e-3 + case "VOLT": + self_range = 30.0 # Check we are not trying to set an out of range value if self.range() is None or abs(output_level) > abs(self_range): @@ -670,7 +674,7 @@ def _update_measurement_module( # since the parameter is not generic in the data type this cannot # narrow None to ModeType even if that is the only valid values # for source_mode. - source_mode = cast("ModeType", self.source_mode.get_latest()) + source_mode = self.source_mode.get_latest() # Get source range if auto-range is off if source_range is None and not self.auto_range(): source_range = self.range() From 48abab38d4e3a6a5c685b04a5bed8bee8fbdbdb8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:48:24 +0100 Subject: [PATCH 07/15] Better types in dynacool driver --- .../QuantumDesign/DynaCoolPPMS/DynaCool.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py index c02bc3922013..080ba979864d 100644 --- a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py +++ b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py @@ -1,13 +1,7 @@ import warnings from functools import partial from time import sleep -from typing import ( - TYPE_CHECKING, - Any, - ClassVar, - Literal, - cast, -) +from typing import TYPE_CHECKING, ClassVar, Literal, TypeVar, cast import numpy as np from pyvisa import VisaIOError @@ -22,6 +16,8 @@ from qcodes.parameters import Parameter +_T = TypeVar("_T") + class DynaCool(VisaInstrument): """ @@ -281,7 +277,7 @@ def error_code(self) -> int: return self._error_code @staticmethod - def _pick_one(which_one: int, parser: type, resp: str) -> Any: + def _pick_one(which_one: int, parser: "Callable[[str], _T]", resp: str) -> _T: """ Since most of the API calls return several values in a comma-separated string, here's a convenience function to pick out the substring of @@ -367,7 +363,7 @@ def _field_ramp_setter(self, target: float) -> None: def _measured_field_getter(self) -> float: resp = self.ask("FELD?") - number_in_oersted = cast("float", DynaCool._pick_one(1, float, resp)) + number_in_oersted = DynaCool._pick_one(1, float, resp) number_in_tesla = number_in_oersted * 1e-4 return number_in_tesla From 9fa221c6f8c162f06d753daa9c21e5bb990ee1be Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 14:50:04 +0100 Subject: [PATCH 08/15] Make use of generic parameters in Alazar driver --- src/qcodes/instrument_drivers/AlazarTech/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/qcodes/instrument_drivers/AlazarTech/utils.py b/src/qcodes/instrument_drivers/AlazarTech/utils.py index 1bbf8b8b606c..34904c0562e1 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/utils.py +++ b/src/qcodes/instrument_drivers/AlazarTech/utils.py @@ -5,15 +5,19 @@ :mod:`.AlazarTech.helpers` module). """ -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Generic from qcodes.parameters import Parameter, ParamRawDataType +from qcodes.parameters.parameter_base import ParameterDataTypeVar if TYPE_CHECKING: - from .ATS import AlazarTechATS + # ruff does not detect that this is used as a generic parameter below + from .ATS import AlazarTechATS # noqa: F401 -class TraceParameter(Parameter): +class TraceParameter( + Parameter[ParameterDataTypeVar, "AlazarTechATS"], Generic[ParameterDataTypeVar] +): """ A parameter that keeps track of if its value has been synced to the ``Instrument``. To achieve that, this parameter sets @@ -38,6 +42,5 @@ def synced_to_card(self) -> bool: return self._synced_to_card def set_raw(self, value: ParamRawDataType) -> None: - instrument = cast("AlazarTechATS", self.instrument) - instrument._parameters_synced = False + self.instrument._parameters_synced = False self._synced_to_card = False From 300c97f5b94d7a616c92b5f1b30e2c833a7952da Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 15:05:54 +0100 Subject: [PATCH 09/15] Add Generic arguments to Keysight Keysight344xx driver --- .../private/Keysight_344xxA_submodules.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) 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 a0f7258838d9..9753a08cc56e 100644 --- a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py +++ b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py @@ -10,7 +10,6 @@ import qcodes.validators as vals from qcodes.instrument import ( - Instrument, InstrumentBaseKWArgs, InstrumentChannel, VisaInstrument, @@ -30,7 +29,7 @@ from typing_extensions import Unpack -class Keysight344xxATrigger(InstrumentChannel): +class Keysight344xxATrigger(InstrumentChannel["Keysight344xxA"]): """Implements triggering parameters and methods of Keysight 344xxA.""" def __init__( @@ -227,7 +226,7 @@ def force(self) -> None: self.write("*TRG") -class Keysight344xxASample(InstrumentChannel): +class Keysight344xxASample(InstrumentChannel["Keysight344xxA"]): """Implements sampling parameters of Keysight 344xxA.""" def __init__( @@ -425,7 +424,7 @@ def __init__( down per measurement).""" -class Keysight344xxADisplay(InstrumentChannel): +class Keysight344xxADisplay(InstrumentChannel["Keysight344xxA"]): """Implements interaction with the display of Keysight 344xxA.""" def __init__( @@ -494,15 +493,14 @@ def clear(self) -> None: self.text.get() # also update the parameter value -class TimeTrace(ParameterWithSetpoints): +class TimeTrace(ParameterWithSetpoints[npt.NDArray[np.float64], "Keysight344xxA"]): """ A parameter class that holds the data for a time trace type measurement, i.e. a measurement of N voltage or current values measured at fixed time intervals """ - def __init__(self, name: str, instrument: Instrument, **kwargs: Any): - self.instrument: Instrument + def __init__(self, name: str, instrument: "Keysight344xxA", **kwargs: Any): super().__init__(name=name, instrument=instrument, **kwargs) # the extra time needed to avoid timeouts during acquisition @@ -547,7 +545,7 @@ def _set_units_and_labels(self) -> None: conf = self.instrument.sense_function() self.unit, self.label = units_and_labels[conf] - def _acquire_time_trace(self) -> npt.NDArray: + def _acquire_time_trace(self) -> npt.NDArray[np.float64]: """ The function that prepares the measurement and fetches the data """ @@ -582,7 +580,7 @@ def _acquire_time_trace(self) -> npt.NDArray: return data - def get_raw(self) -> npt.NDArray: + def get_raw(self) -> npt.NDArray[np.float64]: self._validate_dt() self._set_units_and_labels() data = self._acquire_time_trace() @@ -1188,7 +1186,7 @@ def _get_parameter(self, sense_function: str = "DC Voltage") -> float: return float(response) - def fetch(self) -> npt.NDArray: + def fetch(self) -> npt.NDArray[np.float64]: """ Waits for measurements to complete and copies all available measurements to the instrument's output buffer. The readings remain @@ -1205,7 +1203,7 @@ def fetch(self) -> npt.NDArray: raw_vals: str = self.ask("FETCH?") return _raw_vals_to_array(raw_vals) - def read(self) -> npt.NDArray: + def read(self) -> npt.NDArray[np.float64]: """ Starts a new set of measurements, waits for all measurements to complete, and transfers all available measurements. @@ -1355,7 +1353,7 @@ def decrease_range( self.range(self.ranges[0]) -def _raw_vals_to_array(raw_vals: str) -> npt.NDArray: +def _raw_vals_to_array(raw_vals: str) -> npt.NDArray[np.float64]: """ Helper function that converts comma-delimited string of floating-point values to a numpy 1D array of them. Most data retrieval command of these From 1e9ceea621f18439667789a51c187526d5be7a46 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 15:11:33 +0100 Subject: [PATCH 10/15] Fix B1500 root instrument generic issues --- .../keysightb1500/KeysightB1500_base.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py index dacbfe6758ec..9affd77fbd1f 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py @@ -1,10 +1,11 @@ import re import textwrap from collections import defaultdict -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Generic from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs from qcodes.parameters import MultiParameter, Parameter, create_on_off_val_mapping +from qcodes.parameters.parameter_base import ParameterDataTypeVar from . import constants from .KeysightB1500_module import ( @@ -481,7 +482,11 @@ def enable_smu_filters( ) -class IVSweepMeasurement(MultiParameter, StatusMixin): +class IVSweepMeasurement( + MultiParameter[ParameterDataTypeVar, KeysightB1517A], + StatusMixin, + Generic[ParameterDataTypeVar], +): """ IV sweep measurement outputs a list of measured current parameters as a result of voltage sweep. @@ -506,14 +511,19 @@ def __init__(self, name: str, instrument: KeysightB1517A, **kwargs: Any): **kwargs, ) - self.instrument: KeysightB1517A - self.root_instrument: KeysightB1500 - self.param1 = _FMTResponse(None, None, None, None) self.param2 = _FMTResponse(None, None, None, None) self.source_voltage = _FMTResponse(None, None, None, None) self._fudge: float = 1.5 + @property + def root_instrument(self) -> KeysightB1500: + # since Parameter is not generic over RootInstrument type + # we override the property here to make the root_instrument type + # explicit + assert isinstance(self.instrument.root_instrument, KeysightB1500) + return self.instrument.root_instrument + def set_names_labels_and_units( self, names: "Sequence[str] | None" = None, From f72e93b9333c60c13ace82039078e43e1f27dc9e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 15:17:18 +0100 Subject: [PATCH 11/15] Fix Generic issues in B1520 driver --- .../Keysight/keysightb1500/KeysightB1520A.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py index 788ba756e3ca..bae54d06bbab 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1520A.py @@ -1,6 +1,6 @@ import re import textwrap -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import numpy as np @@ -38,7 +38,7 @@ ) -class KeysightB1500CVSweeper(InstrumentChannel): +class KeysightB1500CVSweeper(InstrumentChannel["KeysightB1520A"]): def __init__( self, parent: "KeysightB1520A", @@ -1151,7 +1151,10 @@ def setup_staircase_cv( """ -class KeysightB1500CVSweepMeasurement(MultiParameter, StatusMixin): +class KeysightB1500CVSweepMeasurement( + MultiParameter[tuple[tuple[float, ...], tuple[float, ...]], KeysightB1520A], + StatusMixin, +): """ CV sweep measurement outputs a list of primary (capacitance) and secondary parameter (disipation). @@ -1175,8 +1178,6 @@ def __init__(self, name: str, instrument: KeysightB1520A, **kwargs: Any): instrument=instrument, **kwargs, ) - self.instrument: KeysightB1520A - self.root_instrument: KeysightB1500 self.update_name_label_unit_from_impedance_model() @@ -1194,6 +1195,13 @@ def __init__(self, name: str, instrument: KeysightB1520A, **kwargs: Any): self.power_line_frequency: int = 50 self._fudge: float = 1.5 # fudge factor for setting timeout + @property + def root_instrument(self) -> "KeysightB1500": + # since Parameter is not generic over RootInstrument type + # we override the property here to make the root_instrument type + # explicit + return cast("KeysightB1500", self.instrument.root_instrument) + def get_raw(self) -> tuple[tuple[float, ...], tuple[float, ...]]: if not self.instrument.setup_fnc_already_run: raise Exception("Sweep setup has not yet been run successfully") From c0aaf3cedd72dddb8b42de765144df24523b5e87 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 15:20:33 +0100 Subject: [PATCH 12/15] Fix Generic issues in B1520 sampling meas --- .../KeysightB1500_sampling_measurement.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_sampling_measurement.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_sampling_measurement.py index 08ad8709f4ee..2fa6a80b67b6 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_sampling_measurement.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_sampling_measurement.py @@ -1,7 +1,8 @@ import warnings -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import numpy +import numpy.typing as npt from qcodes.parameters import ParameterWithSetpoints @@ -19,11 +20,13 @@ KeysightB1500, ) from qcodes.instrument_drivers.Keysight.keysightb1500.KeysightB1517A import ( - KeysightB1517A, + KeysightB1517A, # noqa: F401 # used in generic argument below ) -class SamplingMeasurement(ParameterWithSetpoints): +class SamplingMeasurement( + ParameterWithSetpoints[npt.NDArray[numpy.float64], "KeysightB1517A"] +): """ Performs sampling measurement using semiconductor parameter analyzer B1500A. @@ -36,11 +39,16 @@ class SamplingMeasurement(ParameterWithSetpoints): def __init__(self, name: str, **kwargs: Any): super().__init__(name, **kwargs) - self.instrument: KeysightB1517A - self.root_instrument: KeysightB1500 self.data = _FMTResponse(None, None, None, None) + @property + def root_instrument(self) -> "KeysightB1500": + # since Parameter is not generic over RootInstrument type + # we override the property here to make the root_instrument type + # explicit + return cast("KeysightB1500", self.instrument.root_instrument) + def get_raw(self) -> numpy.ndarray: """ This performs sampling measurements. However since the measurement From 8d8fe1a075191024dfaf9706295937c392a46dea Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 18 Dec 2025 15:21:24 +0100 Subject: [PATCH 13/15] Messagebuilder fix Liskov --- .../Keysight/keysightb1500/message_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py index d60ef45f4ca0..5bf2cd0d07d5 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py @@ -33,7 +33,7 @@ def __init__(self) -> None: super().__init__() self.is_final = False - def append(self, obj: T) -> None: + def append(self, value: T) -> None: if self.is_final: raise ValueError( f"Cannot add commands after `{self[-1]}`. " @@ -41,7 +41,7 @@ def append(self, obj: T) -> None: f"message." ) else: - super().append(obj) + super().append(value) def set_final(self) -> None: self.is_final = True From 2d550fe5bfb6ca51b5f425d3d5fab5cd93d28dcc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 19 Dec 2025 06:34:53 +0100 Subject: [PATCH 14/15] Use generic in KtM960x --- src/qcodes/instrument_drivers/Keysight/KtM960x.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/KtM960x.py b/src/qcodes/instrument_drivers/Keysight/KtM960x.py index 435439f657f6..527773135ff8 100644 --- a/src/qcodes/instrument_drivers/Keysight/KtM960x.py +++ b/src/qcodes/instrument_drivers/Keysight/KtM960x.py @@ -1,6 +1,6 @@ import ctypes from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic import qcodes.validators as vals from qcodes.instrument import Instrument, InstrumentBaseKWArgs @@ -10,6 +10,7 @@ ParamRawDataType, create_on_off_val_mapping, ) +from qcodes.parameters.parameter_base import ParameterDataTypeVar from . import KtM960xDefs @@ -17,7 +18,9 @@ from typing_extensions import Unpack -class Measure(MultiParameter): +class Measure( + MultiParameter[ParameterDataTypeVar, "KeysightM960x"], Generic[ParameterDataTypeVar] +): def __init__(self, name: str, instrument: "KeysightM960x") -> None: super().__init__( name=name, @@ -28,7 +31,6 @@ def __init__(self, name: str, instrument: "KeysightM960x") -> None: labels="Measurement Data", docstring="param that returns measurement values", ) - self.instrument: KeysightM960x def get_raw(self) -> tuple[ParamRawDataType, ...]: return self.instrument._measure() From 98f53fd531407a99c82b607a8fcec50b7dc5c114 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 20 Dec 2025 06:56:40 +0100 Subject: [PATCH 15/15] Fix type checking of tests with improved driver types --- .../b1500_driver_tests/test_b1517a_smu.py | 44 ++++++++++------ .../b1500_driver_tests/test_b1520a_cmu.py | 52 ++++++++++--------- 2 files changed, 56 insertions(+), 40 deletions(-) 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 4f3f66c29533..b66a514cceb4 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 @@ -88,6 +88,7 @@ def test_getting_voltage_after_calling_v_measure_range_config( smu: KeysightB1517A, ) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "NAV-000.002E-01\r" smu.v_measure_range_config(VMeasRange.FIX_2V) @@ -125,6 +126,7 @@ def test_getting_current_after_calling_i_measure_range_config( smu: KeysightB1517A, ) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "NAI+000.005E-06\r" smu.i_measure_range_config(IMeasRange.MIN_100mA) @@ -151,7 +153,7 @@ def test_force_invalid_current_output_range(smu: KeysightB1517A) -> None: def test_force_voltage_with_autorange(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.source_config(output_range=VOutputRange.AUTO) smu.voltage(10) @@ -160,7 +162,7 @@ def test_force_voltage_with_autorange(smu: KeysightB1517A) -> None: def test_force_voltage_autorange_and_compliance(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.source_config( output_range=VOutputRange.AUTO, compliance=1e-6, @@ -176,7 +178,7 @@ def test_new_source_config_should_invalidate_old_source_config( smu: KeysightB1517A, ) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.source_config( output_range=VOutputRange.AUTO, compliance=1e-6, @@ -192,7 +194,7 @@ def test_new_source_config_should_invalidate_old_source_config( def test_unconfigured_source_defaults_to_autorange_v(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.voltage(10) mainframe.write.assert_called_once_with("DV 1,0,10") @@ -200,7 +202,7 @@ def test_unconfigured_source_defaults_to_autorange_v(smu: KeysightB1517A) -> Non def test_unconfigured_source_defaults_to_autorange_i(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.current(0.2) mainframe.write.assert_called_once_with("DI 1,0,0.2") @@ -208,7 +210,7 @@ def test_unconfigured_source_defaults_to_autorange_i(smu: KeysightB1517A) -> Non def test_force_current_with_autorange(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.source_config(output_range=IOutputRange.AUTO) smu.current(0.1) @@ -235,6 +237,7 @@ def test_raise_warning_output_range_mismatches_output_command( def test_measure_current(smu: KeysightB1517A) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "NAI+000.005E-06\r" assert smu.current.measurement_status is None @@ -245,6 +248,7 @@ def test_measure_current(smu: KeysightB1517A) -> None: def test_measure_voltage(smu: KeysightB1517A) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "NAV+000.123E-06\r" assert smu.voltage.measurement_status is None @@ -258,6 +262,7 @@ def test_measure_voltage(smu: KeysightB1517A) -> None: def test_measure_current_shows_compliance_hit(smu: KeysightB1517A) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "CAI+000.123E-06\r" assert smu.current.measurement_status is None @@ -268,6 +273,7 @@ def test_measure_current_shows_compliance_hit(smu: KeysightB1517A) -> None: def test_measured_voltage_with_V_status_returns_nan(smu: KeysightB1517A) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "VAV+199.999E+99\r" assert smu.voltage.measurement_status is None @@ -278,7 +284,7 @@ def test_measured_voltage_with_V_status_returns_nan(smu: KeysightB1517A) -> None def test_some_voltage_sourcing_and_current_measurement(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.source_config(output_range=VOutputRange.MIN_0V5, compliance=1e-9) smu.i_measure_range_config(IMeasRange.FIX_100nA) @@ -296,7 +302,7 @@ def test_some_voltage_sourcing_and_current_measurement(smu: KeysightB1517A) -> N def test_use_high_resolution_adc(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.use_high_resolution_adc() mainframe.write.assert_called_once_with("AAD 1,1") @@ -304,7 +310,7 @@ def test_use_high_resolution_adc(smu: KeysightB1517A) -> None: def test_use_high_speed_adc(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.use_high_speed_adc() mainframe.write.assert_called_once_with("AAD 1,0") @@ -317,7 +323,7 @@ def test_measurement_mode_at_init(smu: KeysightB1517A) -> None: def test_measurement_mode_to_enum_value(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.measurement_mode(MM.Mode.SAMPLING) mainframe.write.assert_called_once_with("MM 10,1") @@ -327,7 +333,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) mainframe.write.assert_called_once_with("MM 10,1") @@ -337,7 +343,7 @@ def test_measurement_mode_to_int_value(smu: KeysightB1517A) -> None: def test_setting_timing_parameters(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.timing_parameters(0.0, 0.42, 32) mainframe.write.assert_called_once_with("MT 0.0,0.42,32") @@ -349,7 +355,7 @@ def test_setting_timing_parameters(smu: KeysightB1517A) -> None: def test_set_average_samples_for_high_speed_adc(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.set_average_samples_for_high_speed_adc(131, constants.AV.Mode.MANUAL) mainframe.write.assert_called_once_with("AV 131,1") mainframe.reset_mock() @@ -366,7 +372,7 @@ def test_set_average_samples_for_high_speed_adc(smu: KeysightB1517A) -> None: def test_measurement_operation_mode(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.measurement_operation_mode(constants.CMM.Mode.COMPLIANCE_SIDE) mainframe.write.assert_called_once_with("CMM 1,0") @@ -381,7 +387,7 @@ def test_measurement_operation_mode(smu: KeysightB1517A) -> None: def test_current_measurement_range(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.current_measurement_range(constants.IMeasRange.FIX_1A) mainframe.write.assert_called_once_with("RI 1,-20") @@ -394,6 +400,8 @@ def test_current_measurement_range(smu: KeysightB1517A) -> None: def test_get_sweep_mode_range_start_end_steps(smu: KeysightB1517A) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) + mainframe.ask.return_value = "WV1,1,50,+3.0E+00,-3.0E+00,201" sweep_mode = smu.iv_sweep.sweep_mode() @@ -439,6 +447,7 @@ def test_iv_sweep_delay(smu: KeysightB1517A) -> None: def test_iv_sweep_mode_start_end_steps_compliance(smu: KeysightB1517A) -> None: mainframe = smu.parent + assert isinstance(mainframe, MagicMock) smu.iv_sweep.sweep_mode(constants.SweepMode.LINEAR_TWO_WAY) smu.iv_sweep.sweep_range(constants.VOutputRange.MIN_2V) @@ -463,7 +472,7 @@ def test_iv_sweep_mode_start_end_steps_compliance(smu: KeysightB1517A) -> None: def test_set_sweep_auto_abort(smu: KeysightB1517A) -> None: mainframe = smu.parent - + assert isinstance(mainframe, MagicMock) smu.iv_sweep.sweep_auto_abort(constants.Abort.ENABLED) mainframe.write.assert_called_once_with("WM 2") @@ -471,6 +480,7 @@ def test_set_sweep_auto_abort(smu: KeysightB1517A) -> None: def test_get_sweep_auto_abort(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;" condition = smu.iv_sweep.sweep_auto_abort() @@ -479,6 +489,7 @@ def test_get_sweep_auto_abort(smu: KeysightB1517A) -> None: 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) @@ -487,6 +498,7 @@ def test_set_post_sweep_voltage_cond(smu: KeysightB1517A) -> None: def test_get_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" condition = smu.iv_sweep.post_sweep_voltage_condition() diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py index 6122a15d63db..8b432d79106a 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py @@ -36,7 +36,7 @@ def test_force_dc_voltage(cmu: KeysightB1520A) -> None: def test_force_ac_voltage(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.voltage_ac(0.1) mainframe.write.assert_called_once_with("ACV 3,0.1") @@ -44,7 +44,7 @@ def test_force_ac_voltage(cmu: KeysightB1520A) -> None: def test_set_ac_frequency(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.frequency(100e3) mainframe.write.assert_called_once_with("FC 3,100000.0") @@ -52,6 +52,7 @@ def test_set_ac_frequency(cmu: KeysightB1520A) -> None: def test_get_dc_voltage(cmu: KeysightB1520A) -> None: mainframe = cmu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "DCV3,0.000;ACV3,0.0;FC3,1000.000" response = cmu.voltage_dc() assert response == 0.0 @@ -69,6 +70,7 @@ def test_get_dc_voltage(cmu: KeysightB1520A) -> None: def test_get_ac_voltage(cmu: KeysightB1520A) -> None: mainframe = cmu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "DCV3,0.000;ACV3,0.000;FC3,1000.000" response = cmu.voltage_ac() assert response == 0.0 @@ -86,6 +88,7 @@ def test_get_ac_voltage(cmu: KeysightB1520A) -> None: def test_get_frequency(cmu: KeysightB1520A) -> None: mainframe = cmu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "DCV3,0.000;ACV3,0.000;FC3,100000.000" response = cmu.frequency() assert response == 100000.0 @@ -103,7 +106,7 @@ def test_get_frequency(cmu: KeysightB1520A) -> None: def test_get_capacitance(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "NCC-1.45713E-06,NCD-3.05845E-03" assert pytest.approx((-1.45713e-06, -3.05845e-03)) == cmu.capacitance() @@ -117,7 +120,7 @@ def test_get_capacitance(cmu: KeysightB1520A) -> None: def test_raise_error_on_unsupported_result_format(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "NCR-1.1234E-03,NCX-4.5677E-03,NCV+0.14235E-03" with pytest.raises(ValueError): @@ -126,7 +129,7 @@ def test_raise_error_on_unsupported_result_format(cmu: KeysightB1520A) -> None: def test_ranging_mode(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.ranging_mode(constants.RangingMode.AUTO) mainframe.write.assert_called_once_with("RC 3,0") @@ -134,7 +137,7 @@ def test_ranging_mode(cmu: KeysightB1520A) -> None: def test_set_sweep_auto_abort(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.cv_sweep.sweep_auto_abort(constants.Abort.ENABLED) mainframe.write.assert_called_once_with("WMDCV 2") @@ -142,7 +145,7 @@ def test_set_sweep_auto_abort(cmu: KeysightB1520A) -> None: def test_get_sweep_auto_abort(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "WMDCV2,2;WTDCV1.0,0.0,0.0,0.0,0.0" condition = cmu.cv_sweep.sweep_auto_abort() assert condition == constants.Abort.ENABLED @@ -150,6 +153,7 @@ def test_get_sweep_auto_abort(cmu: KeysightB1520A) -> None: def test_set_post_sweep_voltage_cond(cmu: KeysightB1520A) -> None: mainframe = cmu.parent + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "WMDCV2,2;WTDCV1.0,0.0,0.0,0.0,0.0" cmu.cv_sweep.post_sweep_voltage_condition.set(constants.WMDCV.Post.STOP) @@ -158,7 +162,7 @@ def test_set_post_sweep_voltage_cond(cmu: KeysightB1520A) -> None: def test_get_post_sweep_voltage_cond(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "WMDCV2,2;WTDCV1.0,0.0,0.0,0.0,0.0" condition = cmu.cv_sweep.post_sweep_voltage_condition() assert condition == constants.WMDCV.Post.STOP @@ -270,7 +274,7 @@ def test_run_sweep(cmu: KeysightB1520A) -> None: def test_phase_compensation_mode(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.phase_compensation_mode(constants.ADJ.Mode.MANUAL) mainframe.write.assert_called_once_with("ADJ 3,1") @@ -280,7 +284,7 @@ def test_phase_compensation_mode(cmu: KeysightB1520A) -> None: def test_phase_compensation(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = 0 response = cmu.phase_compensation() @@ -292,7 +296,7 @@ def test_phase_compensation(cmu: KeysightB1520A) -> None: def test_phase_compensation_with_mode(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = 0 response = cmu.phase_compensation(constants.ADJQuery.Mode.USE_LAST) @@ -304,7 +308,7 @@ def test_phase_compensation_with_mode(cmu: KeysightB1520A) -> None: def test_enable_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.correction.enable(constants.CalibrationType.OPEN) mainframe.write.assert_called_once_with("CORRST 3,1,1") @@ -321,7 +325,7 @@ def test_enable_correction(cmu: KeysightB1520A) -> None: def test_disable_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.correction.disable(constants.CalibrationType.OPEN) mainframe.write.assert_called_once_with("CORRST 3,1,0") @@ -338,7 +342,7 @@ def test_disable_correction(cmu: KeysightB1520A) -> None: def test_correction_is_enabled(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "1" response = cmu.correction.is_enabled(constants.CalibrationType.SHORT) @@ -347,7 +351,7 @@ def test_correction_is_enabled(cmu: KeysightB1520A) -> None: def test_correction_set_reference_values(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.correction.set_reference_values( constants.CalibrationType.OPEN, constants.DCORR.Mode.Cp_G, 1, 2 ) @@ -356,7 +360,7 @@ def test_correction_set_reference_values(cmu: KeysightB1520A) -> None: def test_correction_get_reference_values(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "100,0.001,2" response = "Mode: Cp_G, Primary Cp: 0.001 F, Secondary G: 2.0 S" assert response == cmu.correction.get_reference_values( @@ -368,7 +372,7 @@ def test_clear_and_set_default_frequency_list_for_correction( cmu: KeysightB1520A, ) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.correction.frequency_list.clear_and_set_default() mainframe.write.assert_called_once_with("CLCORR 3,2") @@ -376,7 +380,7 @@ def test_clear_and_set_default_frequency_list_for_correction( def test_clear_frequency_list_for_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.correction.frequency_list.clear() mainframe.write.assert_called_once_with("CLCORR 3,1") @@ -384,7 +388,7 @@ def test_clear_frequency_list_for_correction(cmu: KeysightB1520A) -> None: def test_add_frequency_for_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) cmu.correction.frequency_list.add(1000) mainframe.write.assert_called_once_with("CORRL 3,1000") @@ -392,7 +396,7 @@ def test_add_frequency_for_correction(cmu: KeysightB1520A) -> None: def test_query_from_frequency_list_for_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "25" assert pytest.approx(25) == cmu.correction.frequency_list.query() @@ -401,7 +405,7 @@ def test_query_from_frequency_list_for_correction(cmu: KeysightB1520A) -> None: def test_query_at_index_from_frequency_list_for_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = "1234.567" assert pytest.approx(1234.567) == cmu.correction.frequency_list.query(index=0) @@ -410,7 +414,7 @@ def test_query_at_index_from_frequency_list_for_correction(cmu: KeysightB1520A) def test_perform_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.return_value = 0 response = cmu.correction.perform(constants.CalibrationType.OPEN) @@ -419,7 +423,7 @@ def test_perform_correction(cmu: KeysightB1520A) -> None: def test_perform_and_enable_correction(cmu: KeysightB1520A) -> None: mainframe = cmu.parent - + assert isinstance(mainframe, MagicMock) mainframe.ask.side_effect = [ "0", # for correction status "1", # for correction state (enabled/disabled) @@ -440,5 +444,5 @@ def test_abort(cmu: KeysightB1520A) -> None: mainframe = cmu.parent cmu.abort() - + assert isinstance(mainframe, MagicMock) mainframe.write.assert_called_once_with("AB")