From bfae8fa0377a1d8f32ff15383f2147bd68231acf Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 1 Apr 2026 22:06:21 +0200 Subject: [PATCH 1/7] Port awg5014c to use instrunment channels --- .../instrument_drivers/tektronix/AWG5014.py | 537 ++++++++++-------- .../instrument_drivers/tektronix/__init__.py | 4 +- tests/drivers/test_tektronix_AWG5014C.py | 74 +++ 3 files changed, 389 insertions(+), 226 deletions(-) diff --git a/src/qcodes/instrument_drivers/tektronix/AWG5014.py b/src/qcodes/instrument_drivers/tektronix/AWG5014.py index 5907bff2730b..6e996cb5e879 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -4,6 +4,7 @@ import logging import re import struct +import warnings from collections import abc from io import BytesIO from time import localtime, sleep @@ -22,7 +23,14 @@ from typing_extensions import deprecated from qcodes import validators as vals -from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs +from qcodes.instrument import ( + ChannelList, + ChannelTuple, + InstrumentBaseKWArgs, + InstrumentChannel, + VisaInstrument, + VisaInstrumentKWArgs, +) from qcodes.utils.deprecate import QCoDeSDeprecationWarning if TYPE_CHECKING: @@ -44,6 +52,198 @@ def parsestr(v: str) -> str: return v.strip().strip('"') +class TektronixAWG5014Marker(InstrumentChannel["TektronixAWG5014Channel"]): + """ + Class to hold a marker of an AWG5014 channel. + + Each marker has delay, high level, and low level parameters. + """ + + def __init__( + self, + parent: TektronixAWG5014Channel, + name: str, + channel: int, + marker: int, + **kwargs: Unpack[InstrumentBaseKWArgs], + ) -> None: + """ + Args: + parent: The channel instance to which the marker is + to be attached. + name: The name used in the DataSet. + channel: The channel number (1-4). + marker: The marker number (1-2). + **kwargs: Forwarded to base class. + + """ + super().__init__(parent, name, **kwargs) + + self.channel = channel + self.marker = marker + + m_del_cmd = f"SOURce{channel}:MARKer{marker}:DELay" + m_high_cmd = f"SOURce{channel}:MARKer{marker}:VOLTage:LEVel:IMMediate:HIGH" + m_low_cmd = f"SOURce{channel}:MARKer{marker}:VOLTage:LEVel:IMMediate:LOW" + + self.delay: Parameter = self.add_parameter( + "delay", + label=f"Channel {channel} Marker {marker} delay", + unit="ns", + get_cmd=m_del_cmd + "?", + set_cmd=m_del_cmd + " {:.3f}e-9", + vals=vals.Numbers(0, 1), + get_parser=float, + ) + """Parameter delay""" + self.high: Parameter = self.add_parameter( + "high", + label=f"Channel {channel} Marker {marker} high level", + unit="V", + get_cmd=m_high_cmd + "?", + set_cmd=m_high_cmd + " {:.3f}", + vals=vals.Numbers(-0.9, 2.7), + get_parser=float, + ) + """Parameter high""" + self.low: Parameter = self.add_parameter( + "low", + label=f"Channel {channel} Marker {marker} low level", + unit="V", + get_cmd=m_low_cmd + "?", + set_cmd=m_low_cmd + " {:.3f}", + vals=vals.Numbers(-1.0, 2.6), + get_parser=float, + ) + """Parameter low""" + + +class TektronixAWG5014Channel(InstrumentChannel["TektronixAWG5014"]): + """ + Class to hold a channel of the AWG5014. + + Each channel has analog output parameters (amplitude, offset, waveform, + etc.) and two marker sub-channels with delay, high, and low parameters. + """ + + def __init__( + self, + parent: TektronixAWG5014, + name: str, + channel: int, + **kwargs: Unpack[InstrumentBaseKWArgs], + ) -> None: + """ + Args: + parent: The Instrument instance to which the channel is + to be attached. + name: The name used in the DataSet. + channel: The channel number (1-4). + **kwargs: Forwarded to base class. + + """ + super().__init__(parent, name, **kwargs) + + self.channel = channel + + i = channel + amp_cmd = f"SOURce{i}:VOLTage:LEVel:IMMediate:AMPLitude" + offset_cmd = f"SOURce{i}:VOLTage:LEVel:IMMediate:OFFS" + state_cmd = f"OUTPUT{i}:STATE" + waveform_cmd = f"SOURce{i}:WAVeform" + directoutput_cmd = f"AWGControl:DOUTput{i}:STATE" + filter_cmd = f"OUTPut{i}:FILTer:FREQuency" + add_input_cmd = f"SOURce{i}:COMBine:FEED" + dc_out_cmd = f"AWGControl:DC{i}:VOLTage:OFFSet" + + # Set channel first to ensure sensible sorting of pars + self.state: Parameter = self.add_parameter( + "state", + label=f"Status channel {i}", + get_cmd=state_cmd + "?", + set_cmd=state_cmd + " {}", + vals=vals.Ints(0, 1), + get_parser=int, + ) + """Parameter state""" + self.amp: Parameter = self.add_parameter( + "amp", + label=f"Amplitude channel {i}", + unit="Vpp", + get_cmd=amp_cmd + "?", + set_cmd=amp_cmd + " {:.6f}", + vals=vals.Numbers(0.02, 4.5), + get_parser=float, + ) + """Parameter amp""" + self.offset: Parameter = self.add_parameter( + "offset", + label=f"Offset channel {i}", + unit="V", + get_cmd=offset_cmd + "?", + set_cmd=offset_cmd + " {:.3f}", + vals=vals.Numbers(-2.25, 2.25), + get_parser=float, + ) + """Parameter offset""" + self.waveform: Parameter = self.add_parameter( + "waveform", + label=f"Waveform channel {i}", + get_cmd=waveform_cmd + "?", + set_cmd=waveform_cmd + ' "{}"', + vals=vals.Strings(), + get_parser=parsestr, + ) + """Parameter waveform""" + self.direct_output: Parameter = self.add_parameter( + "direct_output", + label=f"Direct output channel {i}", + get_cmd=directoutput_cmd + "?", + set_cmd=directoutput_cmd + " {}", + vals=vals.Ints(0, 1), + ) + """Parameter direct_output""" + self.add_input: Parameter = self.add_parameter( + "add_input", + label=f"Add input channel {i}", + get_cmd=add_input_cmd + "?", + set_cmd=add_input_cmd + " {}", + vals=vals.Enum('"ESIG"', '"ESIGnal"', '""'), + get_parser=self.parent.newlinestripper, + ) + """Parameter add_input""" + self.filter: Parameter = self.add_parameter( + "filter", + label=f"Low pass filter channel {i}", + unit="Hz", + get_cmd=filter_cmd + "?", + set_cmd=filter_cmd + " {}", + vals=vals.Enum(20e6, 100e6, float("inf"), "INF", "INFinity"), + get_parser=self.parent._tek_outofrange_get_parser, + ) + """Parameter filter""" + self.DC_out: Parameter = self.add_parameter( + "DC_out", + label=f"DC output level channel {i}", + unit="V", + get_cmd=dc_out_cmd + "?", + set_cmd=dc_out_cmd + " {}", + vals=vals.Numbers(-3, 5), + get_parser=float, + ) + """Parameter DC_out""" + + # Marker sub-channels + self.m1: TektronixAWG5014Marker = self.add_submodule( + "m1", TektronixAWG5014Marker(self, "m1", i, 1) + ) + """Marker 1 subchannel""" + self.m2: TektronixAWG5014Marker = self.add_submodule( + "m2", TektronixAWG5014Marker(self, "m2", i, 2) + ) + """Marker 2 subchannel""" + + class TektronixAWG5014(VisaInstrument): """ This is the QCoDeS driver for the Tektronix AWG5014 @@ -384,118 +584,18 @@ def __init__( """Parameter setup_filename""" # Channel parameters # + channels = ChannelList( + self, "channels", TektronixAWG5014Channel, snapshotable=True + ) for i in range(1, self.num_channels + 1): - amp_cmd = f"SOURce{i}:VOLTage:LEVel:IMMediate:AMPLitude" - offset_cmd = f"SOURce{i}:VOLTage:LEVel:IMMediate:OFFS" - state_cmd = f"OUTPUT{i}:STATE" - waveform_cmd = f"SOURce{i}:WAVeform" - directoutput_cmd = f"AWGControl:DOUTput{i}:STATE" - filter_cmd = f"OUTPut{i}:FILTer:FREQuency" - add_input_cmd = f"SOURce{i}:COMBine:FEED" - dc_out_cmd = f"AWGControl:DC{i}:VOLTage:OFFSet" - - # Set channel first to ensure sensible sorting of pars - self.add_parameter( - f"ch{i}_state", - label=f"Status channel {i}", - get_cmd=state_cmd + "?", - set_cmd=state_cmd + " {}", - vals=vals.Ints(0, 1), - get_parser=int, - ) - self.add_parameter( - f"ch{i}_amp", - label=f"Amplitude channel {i}", - unit="Vpp", - get_cmd=amp_cmd + "?", - set_cmd=amp_cmd + " {:.6f}", - vals=vals.Numbers(0.02, 4.5), - get_parser=float, - ) - self.add_parameter( - f"ch{i}_offset", - label=f"Offset channel {i}", - unit="V", - get_cmd=offset_cmd + "?", - set_cmd=offset_cmd + " {:.3f}", - vals=vals.Numbers(-2.25, 2.25), - get_parser=float, - ) - self.add_parameter( - f"ch{i}_waveform", - label=f"Waveform channel {i}", - get_cmd=waveform_cmd + "?", - set_cmd=waveform_cmd + ' "{}"', - vals=vals.Strings(), - get_parser=parsestr, - ) - self.add_parameter( - f"ch{i}_direct_output", - label=f"Direct output channel {i}", - get_cmd=directoutput_cmd + "?", - set_cmd=directoutput_cmd + " {}", - vals=vals.Ints(0, 1), - ) - self.add_parameter( - f"ch{i}_add_input", - label="Add input channel {}", - get_cmd=add_input_cmd + "?", - set_cmd=add_input_cmd + " {}", - vals=vals.Enum('"ESIG"', '"ESIGnal"', '""'), - get_parser=self.newlinestripper, - ) - self.add_parameter( - f"ch{i}_filter", - label=f"Low pass filter channel {i}", - unit="Hz", - get_cmd=filter_cmd + "?", - set_cmd=filter_cmd + " {}", - vals=vals.Enum(20e6, 100e6, float("inf"), "INF", "INFinity"), - get_parser=self._tek_outofrange_get_parser, - ) - self.add_parameter( - f"ch{i}_DC_out", - label=f"DC output level channel {i}", - unit="V", - get_cmd=dc_out_cmd + "?", - set_cmd=dc_out_cmd + " {}", - vals=vals.Numbers(-3, 5), - get_parser=float, - ) - - # Marker channels - for j in range(1, 3): - m_del_cmd = f"SOURce{i}:MARKer{j}:DELay" - m_high_cmd = f"SOURce{i}:MARKer{j}:VOLTage:LEVel:IMMediate:HIGH" - m_low_cmd = f"SOURce{i}:MARKer{j}:VOLTage:LEVel:IMMediate:LOW" - - self.add_parameter( - f"ch{i}_m{j}_del", - label=f"Channel {i} Marker {j} delay", - unit="ns", - get_cmd=m_del_cmd + "?", - set_cmd=m_del_cmd + " {:.3f}e-9", - vals=vals.Numbers(0, 1), - get_parser=float, - ) - self.add_parameter( - f"ch{i}_m{j}_high", - label=f"Channel {i} Marker {j} high level", - unit="V", - get_cmd=m_high_cmd + "?", - set_cmd=m_high_cmd + " {:.3f}", - vals=vals.Numbers(-0.9, 2.7), - get_parser=float, - ) - self.add_parameter( - f"ch{i}_m{j}_low", - label=f"Channel {i} Marker {j} low level", - unit="V", - get_cmd=m_low_cmd + "?", - set_cmd=m_low_cmd + " {:.3f}", - vals=vals.Numbers(-1.0, 2.6), - get_parser=float, - ) + channel = TektronixAWG5014Channel(self, f"ch{i}", i) + channels.append(channel) + self.add_submodule(f"ch{i}", channel) + self.channels: ChannelTuple[TektronixAWG5014Channel] = self.add_submodule( + "channels", + channels.to_channel_tuple(), + ) + """The collection of all AWG output channels.""" self.trigger_impedance.set(50) if self.clock_freq.get() != 1e9: @@ -503,6 +603,52 @@ def __init__( self.connect_message() + _LEGACY_CHANNEL_RE = re.compile( + r"^ch(?P[1-4])_(?:(?Pm[12])_)?(?P.+)$" + ) + + def __getattr__(self, name: str) -> Any: + """ + Provide backwards-compatible access to the old flat parameter names + like ``ch1_amp``, ``ch1_m1_high``, etc. + + These now live on channel / marker submodules but are still + reachable via the old names with a deprecation warning. + """ + m = self._LEGACY_CHANNEL_RE.match(name) + if m is not None: + ch_num = int(m.group("ch")) + marker = m.group("marker") + param = m.group("param") + ch = self.submodules.get(f"ch{ch_num}") + if ch is not None: + if marker is not None: + mrk = ch.submodules.get(marker) + if mrk is not None: + # Old marker param names were e.g. m1_del, m1_high; + # new names are delay, high, low + new_param = {"del": "delay"}.get(param, param) + if hasattr(mrk, new_param): + new_name = f"ch{ch_num}.{marker}.{new_param}" + warnings.warn( + f"Accessing '{name}' is deprecated. " + f"Use '{new_name}' instead.", + category=QCoDeSDeprecationWarning, + stacklevel=2, + ) + return getattr(mrk, new_param) + elif hasattr(ch, param): + new_name = f"ch{ch_num}.{param}" + warnings.warn( + f"Accessing '{name}' is deprecated. Use '{new_name}' instead.", + category=QCoDeSDeprecationWarning, + stacklevel=2, + ) + return getattr(ch, param) + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + # Convenience parser def newlinestripper(self, string: str) -> str: if string.endswith("\n"): @@ -668,13 +814,13 @@ def all_channels_on(self) -> None: Set the state of all channels to be ON. Note: only channels with defined waveforms can be ON. """ - for i in range(1, self.num_channels + 1): - self.parameters[f"ch{i}_state"].set(1) + for ch in self.channels: + ch.state.set(1) def all_channels_off(self) -> None: """Set the state of all channels to be OFF.""" - for i in range(1, self.num_channels + 1): - self.parameters[f"ch{i}_state"].set(0) + for ch in self.channels: + ch.state.set(0) ##################### # Sequences section # @@ -1025,13 +1171,6 @@ def generate_channel_cfg(self) -> dict[str, float | None]: """ log.info("Getting channel configurations.") - dirouts = [ - self.ch1_direct_output.get_latest(), - self.ch2_direct_output.get_latest(), - self.ch3_direct_output.get_latest(), - self.ch4_direct_output.get_latest(), - ] - # the return value of the parameter is different from what goes # into the .awg file, so we translate it filtertrans = { @@ -1043,113 +1182,63 @@ def generate_channel_cfg(self) -> dict[str, float | None]: float("inf"): 10, None: None, } - filters = [ - filtertrans[self.ch1_filter.get_latest()], - filtertrans[self.ch2_filter.get_latest()], - filtertrans[self.ch3_filter.get_latest()], - filtertrans[self.ch4_filter.get_latest()], - ] - - amps = [ - self.ch1_amp.get_latest(), - self.ch2_amp.get_latest(), - self.ch3_amp.get_latest(), - self.ch4_amp.get_latest(), - ] - - offsets = [ - self.ch1_offset.get_latest(), - self.ch2_offset.get_latest(), - self.ch3_offset.get_latest(), - self.ch4_offset.get_latest(), - ] - - mrk1highs = [ - self.ch1_m1_high.get_latest(), - self.ch2_m1_high.get_latest(), - self.ch3_m1_high.get_latest(), - self.ch4_m1_high.get_latest(), - ] - - mrk1lows = [ - self.ch1_m1_low.get_latest(), - self.ch2_m1_low.get_latest(), - self.ch3_m1_low.get_latest(), - self.ch4_m1_low.get_latest(), - ] - - mrk2highs = [ - self.ch1_m2_high.get_latest(), - self.ch2_m2_high.get_latest(), - self.ch3_m2_high.get_latest(), - self.ch4_m2_high.get_latest(), - ] - - mrk2lows = [ - self.ch1_m2_low.get_latest(), - self.ch2_m2_low.get_latest(), - self.ch3_m2_low.get_latest(), - self.ch4_m2_low.get_latest(), - ] - # the return value of the parameter is different from what goes - # into the .awg file, so we translate it addinptrans = {'"ESIG"': 1, '""': 0, None: None} - addinputs = [ - addinptrans[self.ch1_add_input.get_latest()], - addinptrans[self.ch2_add_input.get_latest()], - addinptrans[self.ch3_add_input.get_latest()], - addinptrans[self.ch4_add_input.get_latest()], - ] - # the return value of the parameter is different from what goes - # into the .awg file, so we translate it def mrkdeltrans(x: float | None) -> float | None: if x is None: return None else: return x * 1e-9 - mrk1delays = [ - mrkdeltrans(self.ch1_m1_del.get_latest()), - mrkdeltrans(self.ch2_m1_del.get_latest()), - mrkdeltrans(self.ch3_m1_del.get_latest()), - mrkdeltrans(self.ch4_m1_del.get_latest()), - ] - mrk2delays = [ - mrkdeltrans(self.ch1_m2_del.get_latest()), - mrkdeltrans(self.ch2_m2_del.get_latest()), - mrkdeltrans(self.ch3_m2_del.get_latest()), - mrkdeltrans(self.ch4_m2_del.get_latest()), - ] - AWG_channel_cfg: dict[str, float | None] = {} - for chan in range(1, self.num_channels + 1): - if dirouts[chan - 1] is not None: - AWG_channel_cfg.update( - {f"ANALOG_DIRECT_OUTPUT_{chan}": int(dirouts[chan - 1])} - ) - if filters[chan - 1] is not None: - AWG_channel_cfg.update({f"ANALOG_FILTER_{chan}": filters[chan - 1]}) - if amps[chan - 1] is not None: - AWG_channel_cfg.update({f"ANALOG_AMPLITUDE_{chan}": amps[chan - 1]}) - if offsets[chan - 1] is not None: - AWG_channel_cfg.update({f"ANALOG_OFFSET_{chan}": offsets[chan - 1]}) - if mrk1highs[chan - 1] is not None: - AWG_channel_cfg.update({f"MARKER1_HIGH_{chan}": mrk1highs[chan - 1]}) - if mrk1lows[chan - 1] is not None: - AWG_channel_cfg.update({f"MARKER1_LOW_{chan}": mrk1lows[chan - 1]}) - if mrk2highs[chan - 1] is not None: - AWG_channel_cfg.update({f"MARKER2_HIGH_{chan}": mrk2highs[chan - 1]}) - if mrk2lows[chan - 1] is not None: - AWG_channel_cfg.update({f"MARKER2_LOW_{chan}": mrk2lows[chan - 1]}) - if mrk1delays[chan - 1] is not None: - AWG_channel_cfg.update({f"MARKER1_SKEW_{chan}": mrk1delays[chan - 1]}) - if mrk2delays[chan - 1] is not None: - AWG_channel_cfg.update({f"MARKER2_SKEW_{chan}": mrk2delays[chan - 1]}) - if addinputs[chan - 1] is not None: - AWG_channel_cfg.update({f"EXTERNAL_ADD_{chan}": addinputs[chan - 1]}) + for ch in self.channels: + chan = ch.channel + + dirout = ch.direct_output.get_latest() + if dirout is not None: + AWG_channel_cfg[f"ANALOG_DIRECT_OUTPUT_{chan}"] = int(dirout) + + filt = filtertrans[ch.filter.get_latest()] + if filt is not None: + AWG_channel_cfg[f"ANALOG_FILTER_{chan}"] = filt + + amp = ch.amp.get_latest() + if amp is not None: + AWG_channel_cfg[f"ANALOG_AMPLITUDE_{chan}"] = amp + + offset = ch.offset.get_latest() + if offset is not None: + AWG_channel_cfg[f"ANALOG_OFFSET_{chan}"] = offset + + mrk1high = ch.m1.high.get_latest() + if mrk1high is not None: + AWG_channel_cfg[f"MARKER1_HIGH_{chan}"] = mrk1high + + mrk1low = ch.m1.low.get_latest() + if mrk1low is not None: + AWG_channel_cfg[f"MARKER1_LOW_{chan}"] = mrk1low + + mrk2high = ch.m2.high.get_latest() + if mrk2high is not None: + AWG_channel_cfg[f"MARKER2_HIGH_{chan}"] = mrk2high + + mrk2low = ch.m2.low.get_latest() + if mrk2low is not None: + AWG_channel_cfg[f"MARKER2_LOW_{chan}"] = mrk2low + + mrk1del = mrkdeltrans(ch.m1.delay.get_latest()) + if mrk1del is not None: + AWG_channel_cfg[f"MARKER1_SKEW_{chan}"] = mrk1del + + mrk2del = mrkdeltrans(ch.m2.delay.get_latest()) + if mrk2del is not None: + AWG_channel_cfg[f"MARKER2_SKEW_{chan}"] = mrk2del + + addinput = addinptrans[ch.add_input.get_latest()] + if addinput is not None: + AWG_channel_cfg[f"EXTERNAL_ADD_{chan}"] = addinput return AWG_channel_cfg @@ -1765,13 +1854,11 @@ def send_DC_pulse( length (float): The time to wait before resetting (s). """ - DC_channel_number -= 1 - chandcs = [self.ch1_DC_out, self.ch2_DC_out, self.ch3_DC_out, self.ch4_DC_out] - - restore = chandcs[DC_channel_number].get() - chandcs[DC_channel_number].set(set_level) + ch = self.channels[DC_channel_number - 1] + restore = ch.DC_out.get() + ch.DC_out.set(set_level) sleep(length) - chandcs[DC_channel_number].set(restore) + ch.DC_out.set(restore) def is_awg_ready(self) -> bool: """ diff --git a/src/qcodes/instrument_drivers/tektronix/__init__.py b/src/qcodes/instrument_drivers/tektronix/__init__.py index 06862d560cc0..6a13b926e878 100644 --- a/src/qcodes/instrument_drivers/tektronix/__init__.py +++ b/src/qcodes/instrument_drivers/tektronix/__init__.py @@ -1,4 +1,4 @@ -from .AWG5014 import TektronixAWG5014 +from .AWG5014 import TektronixAWG5014, TektronixAWG5014Channel, TektronixAWG5014Marker from .AWG5208 import TektronixAWG5208 from .AWG70000A import Tektronix70000AWGChannel, TektronixAWG70000Base from .AWG70002A import TektronixAWG70002A @@ -28,6 +28,8 @@ __all__ = [ "Tektronix70000AWGChannel", "TektronixAWG5014", + "TektronixAWG5014Channel", + "TektronixAWG5014Marker", "TektronixAWG5208", "TektronixAWG70000Base", "TektronixAWG70001A", diff --git a/tests/drivers/test_tektronix_AWG5014C.py b/tests/drivers/test_tektronix_AWG5014C.py index cbc516c9f884..8b6e77d41720 100644 --- a/tests/drivers/test_tektronix_AWG5014C.py +++ b/tests/drivers/test_tektronix_AWG5014C.py @@ -1,7 +1,10 @@ +import warnings + import numpy as np import pytest from qcodes.instrument_drivers.tektronix import TektronixAWG5014 +from qcodes.utils.deprecate import QCoDeSDeprecationWarning @pytest.fixture(scope="function") @@ -61,3 +64,74 @@ def test_make_awg_file(awg) -> None: ) assert len(awgfile) > 0 + + +class TestLegacyChannelAttributes: + """Tests that the old flat ch{i}_* attribute names still work + but emit a QCoDeSDeprecationWarning.""" + + CHANNEL_PARAMS = ( + "state", + "amp", + "offset", + "waveform", + "direct_output", + "add_input", + "filter", + "DC_out", + ) + MARKER_PARAMS = (("del", "delay"), ("high", "high"), ("low", "low")) + + def test_legacy_channel_param_exists(self, awg) -> None: + """All old ch{i}_{param} names resolve to the correct parameter.""" + for i in range(1, 5): + for param in self.CHANNEL_PARAMS: + old_name = f"ch{i}_{param}" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", QCoDeSDeprecationWarning) + old_attr = getattr(awg, old_name) + new_attr = getattr(awg.submodules[f"ch{i}"], param) + assert old_attr is new_attr, ( + f"{old_name} did not resolve to ch{i}.{param}" + ) + + def test_legacy_marker_param_exists(self, awg) -> None: + """All old ch{i}_m{j}_{param} names resolve to the correct parameter.""" + for i in range(1, 5): + for j in (1, 2): + for old_suffix, new_name in self.MARKER_PARAMS: + old_name = f"ch{i}_m{j}_{old_suffix}" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", QCoDeSDeprecationWarning) + old_attr = getattr(awg, old_name) + new_attr = getattr( + awg.submodules[f"ch{i}"].submodules[f"m{j}"], new_name + ) + assert old_attr is new_attr, ( + f"{old_name} did not resolve to ch{i}.m{j}.{new_name}" + ) + + def test_legacy_channel_param_warns(self, awg) -> None: + """Accessing an old channel param name emits QCoDeSDeprecationWarning.""" + with pytest.warns(QCoDeSDeprecationWarning, match="ch1_amp.*ch1.amp"): + _ = awg.ch1_amp + + def test_legacy_marker_param_warns(self, awg) -> None: + """Accessing an old marker param name emits QCoDeSDeprecationWarning.""" + with pytest.warns(QCoDeSDeprecationWarning, match="ch2_m1_high.*ch2.m1.high"): + _ = awg.ch2_m1_high + + def test_legacy_marker_del_warns(self, awg) -> None: + """The renamed 'del' -> 'delay' param emits a correct warning.""" + with pytest.warns(QCoDeSDeprecationWarning, match="ch3_m2_del.*ch3.m2.delay"): + _ = awg.ch3_m2_del + + def test_nonexistent_attr_raises(self, awg) -> None: + """An attribute that doesn't match any legacy name still raises.""" + with pytest.raises(AttributeError, match="no_such_attr"): + _ = awg.no_such_attr + + def test_nonexistent_legacy_style_raises(self, awg) -> None: + """A ch{i}_* name that doesn't map to a real param still raises.""" + with pytest.raises(AttributeError): + _ = awg.ch1_bogus_param From 0569fd272e626b181a6d46eff14f3bcfd1983832 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 7 Apr 2026 13:22:40 +0200 Subject: [PATCH 2/7] Expand AWG5014C test coverage from 10 to 78 tests Add tests for the new channel/marker submodule structure, instrument-level parameters, helper functions (parsestr, newlinestripper, _tek_outofrange_get_parser), _pack_waveform edge cases, _pack_record, _file_dict, parse_marker_channel_name, make_awg_file variants, generate_channel_cfg, generate_sequence_cfg, and all_channels_on/off. Extend the pyvisa sim file with properties for all 4 channels, 8 markers, and additional instrument-level parameters to support the new tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../instrument/sims/Tektronix_AWG5014C.yaml | 539 ++++++++++++++++++ tests/drivers/test_tektronix_AWG5014C.py | 527 ++++++++++++++++- 2 files changed, 1036 insertions(+), 30 deletions(-) diff --git a/src/qcodes/instrument/sims/Tektronix_AWG5014C.yaml b/src/qcodes/instrument/sims/Tektronix_AWG5014C.yaml index e0af9f6e714e..3176f588cd62 100644 --- a/src/qcodes/instrument/sims/Tektronix_AWG5014C.yaml +++ b/src/qcodes/instrument/sims/Tektronix_AWG5014C.yaml @@ -86,6 +86,545 @@ devices: setter: q: "EVENt:LEVel {}" + # Channel 1 parameters + ch1 state: + default: 0 + getter: + q: "OUTPUT1:STATE?" + r: "{}" + setter: + q: "OUTPUT1:STATE {}" + + ch1 amp: + default: 0.5 + getter: + q: "SOURce1:VOLTage:LEVel:IMMediate:AMPLitude?" + r: "{}" + setter: + q: "SOURce1:VOLTage:LEVel:IMMediate:AMPLitude {}" + + ch1 offset: + default: 0.0 + getter: + q: "SOURce1:VOLTage:LEVel:IMMediate:OFFS?" + r: "{}" + setter: + q: "SOURce1:VOLTage:LEVel:IMMediate:OFFS {}" + + ch1 waveform: + default: '""' + getter: + q: "SOURce1:WAVeform?" + r: "{}" + setter: + q: "SOURce1:WAVeform {}" + + ch1 direct output: + default: 0 + getter: + q: "AWGControl:DOUTput1:STATE?" + r: "{}" + setter: + q: "AWGControl:DOUTput1:STATE {}" + + ch1 filter: + default: 9.9e37 + getter: + q: "OUTPut1:FILTer:FREQuency?" + r: "{}" + setter: + q: "OUTPut1:FILTer:FREQuency {}" + + ch1 add input: + default: '""' + getter: + q: "SOURce1:COMBine:FEED?" + r: "{}" + setter: + q: "SOURce1:COMBine:FEED {}" + + ch1 dc out: + default: 0.0 + getter: + q: "AWGControl:DC1:VOLTage:OFFSet?" + r: "{}" + setter: + q: "AWGControl:DC1:VOLTage:OFFSet {}" + + # Channel 1 Marker 1 parameters + ch1 m1 delay: + default: 0.0 + getter: + q: "SOURce1:MARKer1:DELay?" + r: "{}" + setter: + q: "SOURce1:MARKer1:DELay {}" + + ch1 m1 high: + default: 1.0 + getter: + q: "SOURce1:MARKer1:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce1:MARKer1:VOLTage:LEVel:IMMediate:HIGH {}" + + ch1 m1 low: + default: 0.0 + getter: + q: "SOURce1:MARKer1:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce1:MARKer1:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 1 Marker 2 parameters + ch1 m2 delay: + default: 0.0 + getter: + q: "SOURce1:MARKer2:DELay?" + r: "{}" + setter: + q: "SOURce1:MARKer2:DELay {}" + + ch1 m2 high: + default: 1.0 + getter: + q: "SOURce1:MARKer2:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce1:MARKer2:VOLTage:LEVel:IMMediate:HIGH {}" + + ch1 m2 low: + default: 0.0 + getter: + q: "SOURce1:MARKer2:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce1:MARKer2:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 2 parameters + ch2 state: + default: 0 + getter: + q: "OUTPUT2:STATE?" + r: "{}" + setter: + q: "OUTPUT2:STATE {}" + + ch2 amp: + default: 0.5 + getter: + q: "SOURce2:VOLTage:LEVel:IMMediate:AMPLitude?" + r: "{}" + setter: + q: "SOURce2:VOLTage:LEVel:IMMediate:AMPLitude {}" + + ch2 offset: + default: 0.0 + getter: + q: "SOURce2:VOLTage:LEVel:IMMediate:OFFS?" + r: "{}" + setter: + q: "SOURce2:VOLTage:LEVel:IMMediate:OFFS {}" + + ch2 waveform: + default: '""' + getter: + q: "SOURce2:WAVeform?" + r: "{}" + setter: + q: "SOURce2:WAVeform {}" + + ch2 direct output: + default: 0 + getter: + q: "AWGControl:DOUTput2:STATE?" + r: "{}" + setter: + q: "AWGControl:DOUTput2:STATE {}" + + ch2 filter: + default: 9.9e37 + getter: + q: "OUTPut2:FILTer:FREQuency?" + r: "{}" + setter: + q: "OUTPut2:FILTer:FREQuency {}" + + ch2 add input: + default: '""' + getter: + q: "SOURce2:COMBine:FEED?" + r: "{}" + setter: + q: "SOURce2:COMBine:FEED {}" + + ch2 dc out: + default: 0.0 + getter: + q: "AWGControl:DC2:VOLTage:OFFSet?" + r: "{}" + setter: + q: "AWGControl:DC2:VOLTage:OFFSet {}" + + # Channel 2 Marker 1 parameters + ch2 m1 delay: + default: 0.0 + getter: + q: "SOURce2:MARKer1:DELay?" + r: "{}" + setter: + q: "SOURce2:MARKer1:DELay {}" + + ch2 m1 high: + default: 1.0 + getter: + q: "SOURce2:MARKer1:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce2:MARKer1:VOLTage:LEVel:IMMediate:HIGH {}" + + ch2 m1 low: + default: 0.0 + getter: + q: "SOURce2:MARKer1:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce2:MARKer1:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 2 Marker 2 parameters + ch2 m2 delay: + default: 0.0 + getter: + q: "SOURce2:MARKer2:DELay?" + r: "{}" + setter: + q: "SOURce2:MARKer2:DELay {}" + + ch2 m2 high: + default: 1.0 + getter: + q: "SOURce2:MARKer2:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce2:MARKer2:VOLTage:LEVel:IMMediate:HIGH {}" + + ch2 m2 low: + default: 0.0 + getter: + q: "SOURce2:MARKer2:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce2:MARKer2:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 3 parameters + ch3 state: + default: 0 + getter: + q: "OUTPUT3:STATE?" + r: "{}" + setter: + q: "OUTPUT3:STATE {}" + + ch3 amp: + default: 0.5 + getter: + q: "SOURce3:VOLTage:LEVel:IMMediate:AMPLitude?" + r: "{}" + setter: + q: "SOURce3:VOLTage:LEVel:IMMediate:AMPLitude {}" + + ch3 offset: + default: 0.0 + getter: + q: "SOURce3:VOLTage:LEVel:IMMediate:OFFS?" + r: "{}" + setter: + q: "SOURce3:VOLTage:LEVel:IMMediate:OFFS {}" + + ch3 waveform: + default: '""' + getter: + q: "SOURce3:WAVeform?" + r: "{}" + setter: + q: "SOURce3:WAVeform {}" + + ch3 direct output: + default: 0 + getter: + q: "AWGControl:DOUTput3:STATE?" + r: "{}" + setter: + q: "AWGControl:DOUTput3:STATE {}" + + ch3 filter: + default: 9.9e37 + getter: + q: "OUTPut3:FILTer:FREQuency?" + r: "{}" + setter: + q: "OUTPut3:FILTer:FREQuency {}" + + ch3 add input: + default: '""' + getter: + q: "SOURce3:COMBine:FEED?" + r: "{}" + setter: + q: "SOURce3:COMBine:FEED {}" + + ch3 dc out: + default: 0.0 + getter: + q: "AWGControl:DC3:VOLTage:OFFSet?" + r: "{}" + setter: + q: "AWGControl:DC3:VOLTage:OFFSet {}" + + # Channel 3 Marker 1 parameters + ch3 m1 delay: + default: 0.0 + getter: + q: "SOURce3:MARKer1:DELay?" + r: "{}" + setter: + q: "SOURce3:MARKer1:DELay {}" + + ch3 m1 high: + default: 1.0 + getter: + q: "SOURce3:MARKer1:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce3:MARKer1:VOLTage:LEVel:IMMediate:HIGH {}" + + ch3 m1 low: + default: 0.0 + getter: + q: "SOURce3:MARKer1:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce3:MARKer1:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 3 Marker 2 parameters + ch3 m2 delay: + default: 0.0 + getter: + q: "SOURce3:MARKer2:DELay?" + r: "{}" + setter: + q: "SOURce3:MARKer2:DELay {}" + + ch3 m2 high: + default: 1.0 + getter: + q: "SOURce3:MARKer2:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce3:MARKer2:VOLTage:LEVel:IMMediate:HIGH {}" + + ch3 m2 low: + default: 0.0 + getter: + q: "SOURce3:MARKer2:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce3:MARKer2:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 4 parameters + ch4 state: + default: 0 + getter: + q: "OUTPUT4:STATE?" + r: "{}" + setter: + q: "OUTPUT4:STATE {}" + + ch4 amp: + default: 0.5 + getter: + q: "SOURce4:VOLTage:LEVel:IMMediate:AMPLitude?" + r: "{}" + setter: + q: "SOURce4:VOLTage:LEVel:IMMediate:AMPLitude {}" + + ch4 offset: + default: 0.0 + getter: + q: "SOURce4:VOLTage:LEVel:IMMediate:OFFS?" + r: "{}" + setter: + q: "SOURce4:VOLTage:LEVel:IMMediate:OFFS {}" + + ch4 waveform: + default: '""' + getter: + q: "SOURce4:WAVeform?" + r: "{}" + setter: + q: "SOURce4:WAVeform {}" + + ch4 direct output: + default: 0 + getter: + q: "AWGControl:DOUTput4:STATE?" + r: "{}" + setter: + q: "AWGControl:DOUTput4:STATE {}" + + ch4 filter: + default: 9.9e37 + getter: + q: "OUTPut4:FILTer:FREQuency?" + r: "{}" + setter: + q: "OUTPut4:FILTer:FREQuency {}" + + ch4 add input: + default: '""' + getter: + q: "SOURce4:COMBine:FEED?" + r: "{}" + setter: + q: "SOURce4:COMBine:FEED {}" + + ch4 dc out: + default: 0.0 + getter: + q: "AWGControl:DC4:VOLTage:OFFSet?" + r: "{}" + setter: + q: "AWGControl:DC4:VOLTage:OFFSet {}" + + # Channel 4 Marker 1 parameters + ch4 m1 delay: + default: 0.0 + getter: + q: "SOURce4:MARKer1:DELay?" + r: "{}" + setter: + q: "SOURce4:MARKer1:DELay {}" + + ch4 m1 high: + default: 1.0 + getter: + q: "SOURce4:MARKer1:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce4:MARKer1:VOLTage:LEVel:IMMediate:HIGH {}" + + ch4 m1 low: + default: 0.0 + getter: + q: "SOURce4:MARKer1:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce4:MARKer1:VOLTage:LEVel:IMMediate:LOW {}" + + # Channel 4 Marker 2 parameters + ch4 m2 delay: + default: 0.0 + getter: + q: "SOURce4:MARKer2:DELay?" + r: "{}" + setter: + q: "SOURce4:MARKer2:DELay {}" + + ch4 m2 high: + default: 1.0 + getter: + q: "SOURce4:MARKer2:VOLTage:LEVel:IMMediate:HIGH?" + r: "{}" + setter: + q: "SOURce4:MARKer2:VOLTage:LEVel:IMMediate:HIGH {}" + + ch4 m2 low: + default: 0.0 + getter: + q: "SOURce4:MARKer2:VOLTage:LEVel:IMMediate:LOW?" + r: "{}" + setter: + q: "SOURce4:MARKer2:VOLTage:LEVel:IMMediate:LOW {}" + + # Additional instrument-level properties + run mode: + default: "CONT" + getter: + q: "AWGControl:RMODe?" + r: "{}" + setter: + q: "AWGControl:RMODe {}" + + run state: + default: 0 + getter: + q: "AWGControl:RSTATe?" + r: "{}" + setter: + q: "AWGControl:RSTATe {}" + + trigger slope: + default: "POS" + getter: + q: "TRIGger:SLOPe?" + r: "{}" + setter: + q: "TRIGger:SLOPe {}" + + trigger polarity: + default: "POS" + getter: + q: "TRIGger:POLarity?" + r: "{}" + setter: + q: "TRIGger:POLarity {}" + + event polarity: + default: "POS" + getter: + q: "EVENt:POL?" + r: "{}" + setter: + q: "EVENt:POL {}" + + event jump timing: + default: "SYNC" + getter: + q: "EVENt:JTIMing?" + r: "{}" + setter: + q: "EVENt:JTIMing {}" + + dc output state: + default: 0 + getter: + q: "AWGControl:DC:STATe?" + r: "{}" + setter: + q: "AWGControl:DC:STATe {}" + + sequence length: + default: 0 + getter: + q: "SEQuence:LENGth?" + r: "{}" + setter: + q: "SEQuence:LENGth {}" + + sequence position: + default: 1 + getter: + q: "AWGControl:SEQuencer:POSition?" + r: "{}" + setter: + q: "SEQuence:JUMP:IMMediate {}" + + setup filename: + default: '"setup.awg","C:\\Users\\OEM\\Documents"' + getter: + q: "AWGControl:SNAMe?" + r: "{}" + resources: GPIB::1::INSTR: device: device 1 diff --git a/tests/drivers/test_tektronix_AWG5014C.py b/tests/drivers/test_tektronix_AWG5014C.py index 8b6e77d41720..21ae7537790a 100644 --- a/tests/drivers/test_tektronix_AWG5014C.py +++ b/tests/drivers/test_tektronix_AWG5014C.py @@ -1,9 +1,15 @@ +import struct import warnings import numpy as np import pytest -from qcodes.instrument_drivers.tektronix import TektronixAWG5014 +from qcodes.instrument_drivers.tektronix import ( + TektronixAWG5014, + TektronixAWG5014Channel, + TektronixAWG5014Marker, +) +from qcodes.instrument_drivers.tektronix.AWG5014 import parsestr from qcodes.utils.deprecate import QCoDeSDeprecationWarning @@ -21,49 +27,510 @@ def awg(): awg_sim.close() +# ── Initialisation and structure ────────────────────────────────────── + + def test_init_awg(awg) -> None: idn_dict = awg.IDN() assert idn_dict["vendor"] == "QCoDeS" -def test_pack_waveform(awg) -> None: - N = 25 +def test_channel_count(awg) -> None: + """The instrument should have exactly 4 output channels.""" + assert len(awg.channels) == 4 - rng = np.random.default_rng() - waveform = rng.random(N) - m1 = rng.integers(0, 2, N) - m2 = rng.integers(0, 2, N) - package = awg._pack_waveform(waveform, m1, m2) +def test_channel_type(awg) -> None: + """Each channel should be a TektronixAWG5014Channel.""" + for ch in awg.channels: + assert isinstance(ch, TektronixAWG5014Channel) - assert package is not None +def test_marker_subchannels(awg) -> None: + """Each channel should have two marker submodules.""" + for ch in awg.channels: + assert isinstance(ch.m1, TektronixAWG5014Marker) + assert isinstance(ch.m2, TektronixAWG5014Marker) -def test_make_awg_file(awg) -> None: - N = 25 - rng = np.random.default_rng() - waveforms = [[rng.random(N)]] - m1s = [[rng.integers(0, 2, N)]] - m2s = [[rng.integers(0, 2, N)]] - nreps = [1] - trig_waits = [0] - goto_states = [0] - jump_tos = [0] +def test_channel_submodule_names(awg) -> None: + """Channels should be accessible as submodules ch1..ch4.""" + for i in range(1, 5): + name = f"ch{i}" + assert name in awg.submodules + assert awg.submodules[name].channel == i + + +def test_marker_channel_and_marker_numbers(awg) -> None: + """Markers should record their parent channel and marker number.""" + for i, ch in enumerate(awg.channels, start=1): + assert ch.m1.channel == i + assert ch.m1.marker == 1 + assert ch.m2.channel == i + assert ch.m2.marker == 2 + + +# ── Helper functions ────────────────────────────────────────────────── + + +class TestParsestr: + def test_strips_quotes_and_whitespace(self) -> None: + assert parsestr(' "hello" ') == "hello" + + def test_no_quotes(self) -> None: + assert parsestr("plain") == "plain" + + def test_empty_quoted_string(self) -> None: + assert parsestr('""') == "" + + def test_strips_only_outer_quotes(self) -> None: + assert parsestr('"a"b"') == 'a"b' + + +class TestNewlinestripper: + def test_strips_trailing_newline(self, awg) -> None: + assert awg.newlinestripper("hello\n") == "hello" + + def test_no_trailing_newline(self, awg) -> None: + assert awg.newlinestripper("hello") == "hello" + + def test_empty_string(self, awg) -> None: + assert awg.newlinestripper("") == "" + + def test_only_newline(self, awg) -> None: + assert awg.newlinestripper("\n") == "" + + +class TestTekOutofrangeGetParser: + def test_normal_value(self, awg) -> None: + assert awg._tek_outofrange_get_parser("1.5") == 1.5 + + def test_out_of_range_value(self, awg) -> None: + assert awg._tek_outofrange_get_parser("9.9e37") == float("INF") + + def test_zero(self, awg) -> None: + assert awg._tek_outofrange_get_parser("0") == 0.0 + + +# ── Instrument-level parameters (via sim) ───────────────────────────── + + +class TestInstrumentParameters: + def test_clock_freq_get(self, awg) -> None: + val = awg.clock_freq.get() + assert val == 1e9 + + def test_trigger_impedance_get(self, awg) -> None: + val = awg.trigger_impedance.get() + assert val == 50.0 + + def test_clock_source_get(self, awg) -> None: + assert awg.clock_source.get() == "INT" + + def test_ref_source_get(self, awg) -> None: + assert awg.ref_source.get() == "INT" + + def test_trigger_source_get(self, awg) -> None: + assert awg.trigger_source.get() == "INT" + + def test_trigger_level_get(self, awg) -> None: + assert awg.trigger_level.get() == 0.0 + + def test_event_impedance_get(self, awg) -> None: + assert awg.event_impedance.get() == 50.0 + + def test_event_level_get(self, awg) -> None: + assert awg.event_level.get() == 0.0 + + def test_run_mode_get(self, awg) -> None: + assert awg.run_mode.get() == "CONT" + + def test_trigger_slope_get(self, awg) -> None: + assert awg.trigger_slope.get() == "POS" + + def test_event_polarity_get(self, awg) -> None: + assert awg.event_polarity.get() == "POS" + + def test_event_jump_timing_get(self, awg) -> None: + assert awg.event_jump_timing.get() == "SYNC" + + def test_DC_output_get(self, awg) -> None: + assert awg.DC_output.get() == 0 + + def test_sequence_length_get(self, awg) -> None: + assert awg.sequence_length.get() == 0 + + def test_get_state(self, awg) -> None: + assert awg.get_state() == "Idle" + + +# ── Channel parameters (via sim) ────────────────────────────────────── + + +class TestChannelParameters: + def test_channel_state_get(self, awg) -> None: + assert awg.channels[0].state.get() == 0 + + def test_channel_amp_get(self, awg) -> None: + assert awg.channels[0].amp.get() == 0.5 + + def test_channel_offset_get(self, awg) -> None: + assert awg.channels[0].offset.get() == 0.0 + + def test_channel_dc_out_get(self, awg) -> None: + assert awg.channels[0].DC_out.get() == 0.0 + + def test_channel_filter_get(self, awg) -> None: + # sim returns 9.9e37 → parsed to inf + assert awg.channels[0].filter.get() == float("inf") + + def test_all_channels_have_consistent_defaults(self, awg) -> None: + """All four channels should return the same default values.""" + for ch in awg.channels: + assert ch.state.get() == 0 + assert ch.amp.get() == 0.5 + assert ch.offset.get() == 0.0 + + +# ── Marker parameters (via sim) ─────────────────────────────────────── + + +class TestMarkerParameters: + def test_marker_delay_get(self, awg) -> None: + ch1 = awg.channels[0] + assert ch1.m1.delay.get() == 0.0 + + def test_marker_high_get(self, awg) -> None: + ch1 = awg.channels[0] + assert ch1.m1.high.get() == 1.0 + + def test_marker_low_get(self, awg) -> None: + ch1 = awg.channels[0] + assert ch1.m1.low.get() == 0.0 + + def test_all_markers_have_consistent_defaults(self, awg) -> None: + """All 8 markers should share the same defaults.""" + for ch in awg.channels: + for mrk in (ch.m1, ch.m2): + assert mrk.delay.get() == 0.0 + assert mrk.high.get() == 1.0 + assert mrk.low.get() == 0.0 + + +# ── all_channels_on / all_channels_off ──────────────────────────────── + + +class TestAllChannelsOnOff: + def test_all_channels_off(self, awg) -> None: + awg.all_channels_off() + for ch in awg.channels: + assert ch.state.get() == 0 + + def test_all_channels_on(self, awg) -> None: + awg.all_channels_on() + for ch in awg.channels: + assert ch.state.get() == 1 + + def test_all_channels_on_then_off(self, awg) -> None: + awg.all_channels_on() + awg.all_channels_off() + for ch in awg.channels: + assert ch.state.get() == 0 + + +# ── _pack_waveform ──────────────────────────────────────────────────── + + +class TestPackWaveform: + def test_basic_pack(self, awg) -> None: + N = 25 + rng = np.random.default_rng(42) + wf = rng.random(N) * 2 - 1 # values in [-1, 1] + m1 = rng.integers(0, 2, N) + m2 = rng.integers(0, 2, N) + + result = awg._pack_waveform(wf, m1, m2) + + assert result is not None + assert len(result) == N + assert result.dtype == np.uint16 + + def test_known_values(self, awg) -> None: + """Verify encoding of specific known inputs.""" + wf = np.array([0.0]) + m1 = np.array([0]) + m2 = np.array([0]) + result = awg._pack_waveform(wf, m1, m2) + # wf=0 → 0*8191+8191.5 = 8191.5, trunc → 8191 + assert result[0] == 8191 + + def test_markers_set_correct_bits(self, awg) -> None: + """Marker bits should be at positions 14 and 15.""" + wf = np.array([0.0]) + # m1 only + r_m1 = awg._pack_waveform(wf, np.array([1]), np.array([0])) + assert r_m1[0] & 0x4000 # bit 14 + # m2 only + r_m2 = awg._pack_waveform(wf, np.array([0]), np.array([1])) + assert r_m2[0] & 0x8000 # bit 15 + # both + r_both = awg._pack_waveform(wf, np.array([1]), np.array([1])) + assert r_both[0] & 0xC000 == 0xC000 + + def test_mismatched_lengths_raises(self, awg) -> None: + with pytest.raises(Exception, match=r"sizes.*do not match"): + awg._pack_waveform(np.array([0.0, 0.0]), np.array([0]), np.array([0])) + + def test_waveform_out_of_bounds_raises(self, awg) -> None: + with pytest.raises(TypeError, match="Waveform values out of bo"): + awg._pack_waveform(np.array([1.5]), np.array([0]), np.array([0])) + + def test_waveform_below_bounds_raises(self, awg) -> None: + with pytest.raises(TypeError, match="Waveform values out of bo"): + awg._pack_waveform(np.array([-1.5]), np.array([0]), np.array([0])) + + def test_invalid_marker1_raises(self, awg) -> None: + with pytest.raises(TypeError, match="Marker 1"): + awg._pack_waveform(np.array([0.0]), np.array([2]), np.array([0])) + + def test_invalid_marker2_raises(self, awg) -> None: + with pytest.raises(TypeError, match="Marker 2"): + awg._pack_waveform(np.array([0.0]), np.array([0]), np.array([3])) + + +# ── _pack_record ────────────────────────────────────────────────────── + + +class TestPackRecord: + def test_pack_short(self, awg) -> None: + """Pack a 16-bit integer record.""" + result = awg._pack_record("MAGIC", 5000, "h") + # name = "MAGIC\0" (6 bytes), data = 2 bytes (short) + name_size, data_size = struct.unpack_from(" None: + """Pack a 64-bit float record.""" + result = awg._pack_record("SAMPLING_RATE", 1e9, "d") + name_size, data_size = struct.unpack_from(" None: + """Pack a string record.""" + result = awg._pack_record("TEST_NAME", "hello\x00", "6s") + name_size, data_size = struct.unpack_from(" None: + """Pack a numpy array as unsigned 16-bit integers.""" + data = np.array([1, 2, 3], dtype=np.uint16) + result = awg._pack_record("WF_DATA", data, "3H") + _name_size, data_size = struct.unpack_from(" None: + wf = np.array([0.0, 0.5, -0.5]) + m1 = np.array([0, 1, 0]) + m2 = np.array([1, 0, 1]) + clock = 1e9 + + result = awg._file_dict(wf, m1, m2, clock) + + assert np.array_equal(result["w"], wf) + assert np.array_equal(result["m1"], m1) + assert np.array_equal(result["m2"], m2) + assert result["clock_freq"] == clock + assert result["numpoints"] == 3 + + +def test_file_dict_none_clock(awg) -> None: + wf = np.array([0.0]) + m1 = np.array([0]) + m2 = np.array([0]) + + result = awg._file_dict(wf, m1, m2, None) + assert result["clock_freq"] is None + + +# ── parse_marker_channel_name ───────────────────────────────────────── + + +class TestParseMarkerChannelName: + def test_valid_1m1(self) -> None: + result = TektronixAWG5014.parse_marker_channel_name("1M1") + assert result.channel == 1 + assert result.marker == 1 + + def test_valid_3m2(self) -> None: + result = TektronixAWG5014.parse_marker_channel_name("3M2") + assert result.channel == 3 + assert result.marker == 2 + + def test_valid_4m1(self) -> None: + result = TektronixAWG5014.parse_marker_channel_name("4M1") + assert result.channel == 4 + assert result.marker == 1 + + def test_invalid_raises(self) -> None: + with pytest.raises(AssertionError): + TektronixAWG5014.parse_marker_channel_name("bad") + + +# ── make_awg_file ───────────────────────────────────────────────────── + + +class TestMakeAwgFile: + def test_basic_awg_file(self, awg) -> None: + N = 25 + rng = np.random.default_rng(42) + waveforms = [[rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + assert isinstance(awgfile, bytes) + + def test_multi_segment_awg_file(self, awg) -> None: + """File with two sequence elements on one channel.""" + N = 10 + rng = np.random.default_rng(123) + waveforms = [[rng.random(N) * 2 - 1, rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N), rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N), rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1, 1], + [0, 0], + [0, 0], + [0, 0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + def test_multi_channel_awg_file(self, awg) -> None: + """File with one element each on two channels.""" + N = 10 + rng = np.random.default_rng(456) + waveforms = [[rng.random(N) * 2 - 1], [rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + def test_specific_channels(self, awg) -> None: + """Using the channels parameter to target channels 2 and 4.""" + N = 10 + rng = np.random.default_rng(789) + waveforms = [[rng.random(N) * 2 - 1], [rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + channels=[2, 4], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + def test_flat_input_format(self, awg) -> None: + """make_awg_file also accepts flat (non-nested) waveform lists.""" + N = 10 + rng = np.random.default_rng(111) + waveforms = [rng.random(N) * 2 - 1] + m1s = [rng.integers(0, 2, N)] + m2s = [rng.integers(0, 2, N)] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + +# ── generate_channel_cfg ────────────────────────────────────────────── + + +class TestGenerateChannelCfg: + def test_returns_dict(self, awg) -> None: + cfg = awg.generate_channel_cfg() + assert isinstance(cfg, dict) + + def test_contains_settings_after_get(self, awg) -> None: + """After getting channel params, they should appear in the config.""" + ch1 = awg.channels[0] + ch1.amp.get() + ch1.offset.get() + ch1.m1.high.get() + ch1.m1.low.get() + + cfg = awg.generate_channel_cfg() + assert "ANALOG_AMPLITUDE_1" in cfg + assert "ANALOG_OFFSET_1" in cfg + assert "MARKER1_HIGH_1" in cfg + assert "MARKER1_LOW_1" in cfg + + +# ── generate_sequence_cfg ───────────────────────────────────────────── + + +def test_generate_sequence_cfg(awg) -> None: + cfg = awg.generate_sequence_cfg() + assert isinstance(cfg, dict) + assert cfg["SAMPLING_RATE"] == 1e9 + assert cfg["CLOCK_SOURCE"] == 1 # INT + assert cfg["REFERENCE_SOURCE"] == 1 # INT + assert cfg["RUN_MODE"] == 4 # Sequence - awgfile = awg.make_awg_file( - waveforms, - m1s, - m2s, - nreps, - trig_waits, - goto_states, - jump_tos, - preservechannelsettings=False, - ) - assert len(awgfile) > 0 +# ── Legacy attribute backward compatibility ─────────────────────────── class TestLegacyChannelAttributes: From 1a2877759c5e9f1ee0b55f16bf9d7e2cf223b4ea Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 8 Apr 2026 11:31:11 +0200 Subject: [PATCH 3/7] Update AWG5014C example notebook for channel submodules Use the new channel-based parameter access (e.g. awg1.ch2.offset instead of awg1.ch2_offset) and update the parameter listing to show the top-level, channel, and marker parameter hierarchy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...odes example with Tektronix AWG5014C.ipynb | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb b/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb index 6c51d9290d40..abe47966ccc7 100644 --- a/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb @@ -81,17 +81,17 @@ "metadata": {}, "outputs": [], "source": [ - "print(awg1.ch3_state.get())\n", - "print(awg1.ch2_offset.get())\n", - "awg1.ch2_offset.set(0.1)\n", - "print(awg1.ch2_offset.get())" + "print(awg1.ch3.state.get())\n", + "print(awg1.ch2.offset.get())\n", + "awg1.ch2.offset.set(0.1)\n", + "print(awg1.ch2.offset.get())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "A list of all available parameters can be found in the following manner:" + "The instrument has top-level parameters as well as per-channel parameters accessible via `awg1.ch1`, `awg1.ch2`, etc. Each channel also has marker submodules `m1` and `m2`." ] }, { @@ -100,9 +100,19 @@ "metadata": {}, "outputs": [], "source": [ - "pars = np.sort(list(awg1.parameters.keys()))\n", - "for param in pars:\n", - " print(param, \": \", awg1.parameters[param].label)" + "# Top-level parameters\n", + "for name in sorted(awg1.parameters):\n", + " print(name, \": \", awg1.parameters[name].label)\n", + "\n", + "# Channel parameters (e.g. ch1)\n", + "print(\"\\nChannel 1 parameters:\")\n", + "for name in sorted(awg1.ch1.parameters):\n", + " print(f\" ch1.{name}: \", awg1.ch1.parameters[name].label)\n", + "\n", + "# Marker parameters (e.g. ch1.m1)\n", + "print(\"\\nChannel 1 Marker 1 parameters:\")\n", + "for name in sorted(awg1.ch1.m1.parameters):\n", + " print(f\" ch1.m1.{name}: \", awg1.ch1.m1.parameters[name].label)" ] }, { From 68e7bc78a7093241b3ac0397a4e66d89693c691c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 8 Apr 2026 11:33:37 +0200 Subject: [PATCH 4/7] Add newsfragment for AWG5014 channel submodule refactor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/changes/newsfragments/7996.improved_driver | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changes/newsfragments/7996.improved_driver diff --git a/docs/changes/newsfragments/7996.improved_driver b/docs/changes/newsfragments/7996.improved_driver new file mode 100644 index 000000000000..387976168ab4 --- /dev/null +++ b/docs/changes/newsfragments/7996.improved_driver @@ -0,0 +1,6 @@ +The ``TektronixAWG5014`` driver has been refactored to use ``InstrumentChannel`` +submodules. Per-channel parameters (e.g. ``amp``, ``offset``, ``state``) are now +accessed via ``awg.ch1.amp`` instead of ``awg.ch1_amp``, and marker parameters +via ``awg.ch1.m1.high`` instead of ``awg.ch1_m1_high``. The old flat attribute +names still work but emit a deprecation warning. The example notebook has been +updated accordingly. From b16263df2102aed3d8153c12990b0ae010a16c02 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 8 Apr 2026 11:40:51 +0200 Subject: [PATCH 5/7] Add type annotations to AWG5014C tests Annotate all awg fixture parameters with TektronixAWG5014 and add Generator return type to the fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/drivers/test_tektronix_AWG5014C.py | 148 ++++++++++++----------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/tests/drivers/test_tektronix_AWG5014C.py b/tests/drivers/test_tektronix_AWG5014C.py index 21ae7537790a..6baa0c0434fa 100644 --- a/tests/drivers/test_tektronix_AWG5014C.py +++ b/tests/drivers/test_tektronix_AWG5014C.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import struct import warnings +from typing import TYPE_CHECKING import numpy as np import pytest @@ -12,9 +15,12 @@ from qcodes.instrument_drivers.tektronix.AWG5014 import parsestr from qcodes.utils.deprecate import QCoDeSDeprecationWarning +if TYPE_CHECKING: + from collections.abc import Generator + @pytest.fixture(scope="function") -def awg(): +def awg() -> Generator[TektronixAWG5014]: awg_sim = TektronixAWG5014( "awg_sim", address="GPIB0::1::INSTR", @@ -30,31 +36,31 @@ def awg(): # ── Initialisation and structure ────────────────────────────────────── -def test_init_awg(awg) -> None: +def test_init_awg(awg: TektronixAWG5014) -> None: idn_dict = awg.IDN() assert idn_dict["vendor"] == "QCoDeS" -def test_channel_count(awg) -> None: +def test_channel_count(awg: TektronixAWG5014) -> None: """The instrument should have exactly 4 output channels.""" assert len(awg.channels) == 4 -def test_channel_type(awg) -> None: +def test_channel_type(awg: TektronixAWG5014) -> None: """Each channel should be a TektronixAWG5014Channel.""" for ch in awg.channels: assert isinstance(ch, TektronixAWG5014Channel) -def test_marker_subchannels(awg) -> None: +def test_marker_subchannels(awg: TektronixAWG5014) -> None: """Each channel should have two marker submodules.""" for ch in awg.channels: assert isinstance(ch.m1, TektronixAWG5014Marker) assert isinstance(ch.m2, TektronixAWG5014Marker) -def test_channel_submodule_names(awg) -> None: +def test_channel_submodule_names(awg: TektronixAWG5014) -> None: """Channels should be accessible as submodules ch1..ch4.""" for i in range(1, 5): name = f"ch{i}" @@ -62,7 +68,7 @@ def test_channel_submodule_names(awg) -> None: assert awg.submodules[name].channel == i -def test_marker_channel_and_marker_numbers(awg) -> None: +def test_marker_channel_and_marker_numbers(awg: TektronixAWG5014) -> None: """Markers should record their parent channel and marker number.""" for i, ch in enumerate(awg.channels, start=1): assert ch.m1.channel == i @@ -89,27 +95,27 @@ def test_strips_only_outer_quotes(self) -> None: class TestNewlinestripper: - def test_strips_trailing_newline(self, awg) -> None: + def test_strips_trailing_newline(self, awg: TektronixAWG5014) -> None: assert awg.newlinestripper("hello\n") == "hello" - def test_no_trailing_newline(self, awg) -> None: + def test_no_trailing_newline(self, awg: TektronixAWG5014) -> None: assert awg.newlinestripper("hello") == "hello" - def test_empty_string(self, awg) -> None: + def test_empty_string(self, awg: TektronixAWG5014) -> None: assert awg.newlinestripper("") == "" - def test_only_newline(self, awg) -> None: + def test_only_newline(self, awg: TektronixAWG5014) -> None: assert awg.newlinestripper("\n") == "" class TestTekOutofrangeGetParser: - def test_normal_value(self, awg) -> None: + def test_normal_value(self, awg: TektronixAWG5014) -> None: assert awg._tek_outofrange_get_parser("1.5") == 1.5 - def test_out_of_range_value(self, awg) -> None: + def test_out_of_range_value(self, awg: TektronixAWG5014) -> None: assert awg._tek_outofrange_get_parser("9.9e37") == float("INF") - def test_zero(self, awg) -> None: + def test_zero(self, awg: TektronixAWG5014) -> None: assert awg._tek_outofrange_get_parser("0") == 0.0 @@ -117,51 +123,51 @@ def test_zero(self, awg) -> None: class TestInstrumentParameters: - def test_clock_freq_get(self, awg) -> None: + def test_clock_freq_get(self, awg: TektronixAWG5014) -> None: val = awg.clock_freq.get() assert val == 1e9 - def test_trigger_impedance_get(self, awg) -> None: + def test_trigger_impedance_get(self, awg: TektronixAWG5014) -> None: val = awg.trigger_impedance.get() assert val == 50.0 - def test_clock_source_get(self, awg) -> None: + def test_clock_source_get(self, awg: TektronixAWG5014) -> None: assert awg.clock_source.get() == "INT" - def test_ref_source_get(self, awg) -> None: + def test_ref_source_get(self, awg: TektronixAWG5014) -> None: assert awg.ref_source.get() == "INT" - def test_trigger_source_get(self, awg) -> None: + def test_trigger_source_get(self, awg: TektronixAWG5014) -> None: assert awg.trigger_source.get() == "INT" - def test_trigger_level_get(self, awg) -> None: + def test_trigger_level_get(self, awg: TektronixAWG5014) -> None: assert awg.trigger_level.get() == 0.0 - def test_event_impedance_get(self, awg) -> None: + def test_event_impedance_get(self, awg: TektronixAWG5014) -> None: assert awg.event_impedance.get() == 50.0 - def test_event_level_get(self, awg) -> None: + def test_event_level_get(self, awg: TektronixAWG5014) -> None: assert awg.event_level.get() == 0.0 - def test_run_mode_get(self, awg) -> None: + def test_run_mode_get(self, awg: TektronixAWG5014) -> None: assert awg.run_mode.get() == "CONT" - def test_trigger_slope_get(self, awg) -> None: + def test_trigger_slope_get(self, awg: TektronixAWG5014) -> None: assert awg.trigger_slope.get() == "POS" - def test_event_polarity_get(self, awg) -> None: + def test_event_polarity_get(self, awg: TektronixAWG5014) -> None: assert awg.event_polarity.get() == "POS" - def test_event_jump_timing_get(self, awg) -> None: + def test_event_jump_timing_get(self, awg: TektronixAWG5014) -> None: assert awg.event_jump_timing.get() == "SYNC" - def test_DC_output_get(self, awg) -> None: + def test_DC_output_get(self, awg: TektronixAWG5014) -> None: assert awg.DC_output.get() == 0 - def test_sequence_length_get(self, awg) -> None: + def test_sequence_length_get(self, awg: TektronixAWG5014) -> None: assert awg.sequence_length.get() == 0 - def test_get_state(self, awg) -> None: + def test_get_state(self, awg: TektronixAWG5014) -> None: assert awg.get_state() == "Idle" @@ -169,23 +175,23 @@ def test_get_state(self, awg) -> None: class TestChannelParameters: - def test_channel_state_get(self, awg) -> None: + def test_channel_state_get(self, awg: TektronixAWG5014) -> None: assert awg.channels[0].state.get() == 0 - def test_channel_amp_get(self, awg) -> None: + def test_channel_amp_get(self, awg: TektronixAWG5014) -> None: assert awg.channels[0].amp.get() == 0.5 - def test_channel_offset_get(self, awg) -> None: + def test_channel_offset_get(self, awg: TektronixAWG5014) -> None: assert awg.channels[0].offset.get() == 0.0 - def test_channel_dc_out_get(self, awg) -> None: + def test_channel_dc_out_get(self, awg: TektronixAWG5014) -> None: assert awg.channels[0].DC_out.get() == 0.0 - def test_channel_filter_get(self, awg) -> None: + def test_channel_filter_get(self, awg: TektronixAWG5014) -> None: # sim returns 9.9e37 → parsed to inf assert awg.channels[0].filter.get() == float("inf") - def test_all_channels_have_consistent_defaults(self, awg) -> None: + def test_all_channels_have_consistent_defaults(self, awg: TektronixAWG5014) -> None: """All four channels should return the same default values.""" for ch in awg.channels: assert ch.state.get() == 0 @@ -197,19 +203,19 @@ def test_all_channels_have_consistent_defaults(self, awg) -> None: class TestMarkerParameters: - def test_marker_delay_get(self, awg) -> None: + def test_marker_delay_get(self, awg: TektronixAWG5014) -> None: ch1 = awg.channels[0] assert ch1.m1.delay.get() == 0.0 - def test_marker_high_get(self, awg) -> None: + def test_marker_high_get(self, awg: TektronixAWG5014) -> None: ch1 = awg.channels[0] assert ch1.m1.high.get() == 1.0 - def test_marker_low_get(self, awg) -> None: + def test_marker_low_get(self, awg: TektronixAWG5014) -> None: ch1 = awg.channels[0] assert ch1.m1.low.get() == 0.0 - def test_all_markers_have_consistent_defaults(self, awg) -> None: + def test_all_markers_have_consistent_defaults(self, awg: TektronixAWG5014) -> None: """All 8 markers should share the same defaults.""" for ch in awg.channels: for mrk in (ch.m1, ch.m2): @@ -222,17 +228,17 @@ def test_all_markers_have_consistent_defaults(self, awg) -> None: class TestAllChannelsOnOff: - def test_all_channels_off(self, awg) -> None: + def test_all_channels_off(self, awg: TektronixAWG5014) -> None: awg.all_channels_off() for ch in awg.channels: assert ch.state.get() == 0 - def test_all_channels_on(self, awg) -> None: + def test_all_channels_on(self, awg: TektronixAWG5014) -> None: awg.all_channels_on() for ch in awg.channels: assert ch.state.get() == 1 - def test_all_channels_on_then_off(self, awg) -> None: + def test_all_channels_on_then_off(self, awg: TektronixAWG5014) -> None: awg.all_channels_on() awg.all_channels_off() for ch in awg.channels: @@ -243,7 +249,7 @@ def test_all_channels_on_then_off(self, awg) -> None: class TestPackWaveform: - def test_basic_pack(self, awg) -> None: + def test_basic_pack(self, awg: TektronixAWG5014) -> None: N = 25 rng = np.random.default_rng(42) wf = rng.random(N) * 2 - 1 # values in [-1, 1] @@ -256,7 +262,7 @@ def test_basic_pack(self, awg) -> None: assert len(result) == N assert result.dtype == np.uint16 - def test_known_values(self, awg) -> None: + def test_known_values(self, awg: TektronixAWG5014) -> None: """Verify encoding of specific known inputs.""" wf = np.array([0.0]) m1 = np.array([0]) @@ -265,7 +271,7 @@ def test_known_values(self, awg) -> None: # wf=0 → 0*8191+8191.5 = 8191.5, trunc → 8191 assert result[0] == 8191 - def test_markers_set_correct_bits(self, awg) -> None: + def test_markers_set_correct_bits(self, awg: TektronixAWG5014) -> None: """Marker bits should be at positions 14 and 15.""" wf = np.array([0.0]) # m1 only @@ -278,23 +284,23 @@ def test_markers_set_correct_bits(self, awg) -> None: r_both = awg._pack_waveform(wf, np.array([1]), np.array([1])) assert r_both[0] & 0xC000 == 0xC000 - def test_mismatched_lengths_raises(self, awg) -> None: + def test_mismatched_lengths_raises(self, awg: TektronixAWG5014) -> None: with pytest.raises(Exception, match=r"sizes.*do not match"): awg._pack_waveform(np.array([0.0, 0.0]), np.array([0]), np.array([0])) - def test_waveform_out_of_bounds_raises(self, awg) -> None: + def test_waveform_out_of_bounds_raises(self, awg: TektronixAWG5014) -> None: with pytest.raises(TypeError, match="Waveform values out of bo"): awg._pack_waveform(np.array([1.5]), np.array([0]), np.array([0])) - def test_waveform_below_bounds_raises(self, awg) -> None: + def test_waveform_below_bounds_raises(self, awg: TektronixAWG5014) -> None: with pytest.raises(TypeError, match="Waveform values out of bo"): awg._pack_waveform(np.array([-1.5]), np.array([0]), np.array([0])) - def test_invalid_marker1_raises(self, awg) -> None: + def test_invalid_marker1_raises(self, awg: TektronixAWG5014) -> None: with pytest.raises(TypeError, match="Marker 1"): awg._pack_waveform(np.array([0.0]), np.array([2]), np.array([0])) - def test_invalid_marker2_raises(self, awg) -> None: + def test_invalid_marker2_raises(self, awg: TektronixAWG5014) -> None: with pytest.raises(TypeError, match="Marker 2"): awg._pack_waveform(np.array([0.0]), np.array([0]), np.array([3])) @@ -303,7 +309,7 @@ def test_invalid_marker2_raises(self, awg) -> None: class TestPackRecord: - def test_pack_short(self, awg) -> None: + def test_pack_short(self, awg: TektronixAWG5014) -> None: """Pack a 16-bit integer record.""" result = awg._pack_record("MAGIC", 5000, "h") # name = "MAGIC\0" (6 bytes), data = 2 bytes (short) @@ -314,7 +320,7 @@ def test_pack_short(self, awg) -> None: data_val = struct.unpack_from(" None: + def test_pack_double(self, awg: TektronixAWG5014) -> None: """Pack a 64-bit float record.""" result = awg._pack_record("SAMPLING_RATE", 1e9, "d") name_size, data_size = struct.unpack_from(" None: data_val = struct.unpack_from(" None: + def test_pack_string(self, awg: TektronixAWG5014) -> None: """Pack a string record.""" result = awg._pack_record("TEST_NAME", "hello\x00", "6s") name_size, data_size = struct.unpack_from(" None: data_str = result[8 + name_size : 8 + name_size + data_size] assert data_str == b"hello\x00" - def test_pack_array_of_unsigned_shorts(self, awg) -> None: + def test_pack_array_of_unsigned_shorts(self, awg: TektronixAWG5014) -> None: """Pack a numpy array as unsigned 16-bit integers.""" data = np.array([1, 2, 3], dtype=np.uint16) result = awg._pack_record("WF_DATA", data, "3H") @@ -341,7 +347,7 @@ def test_pack_array_of_unsigned_shorts(self, awg) -> None: # ── _file_dict ──────────────────────────────────────────────────────── -def test_file_dict(awg) -> None: +def test_file_dict(awg: TektronixAWG5014) -> None: wf = np.array([0.0, 0.5, -0.5]) m1 = np.array([0, 1, 0]) m2 = np.array([1, 0, 1]) @@ -356,7 +362,7 @@ def test_file_dict(awg) -> None: assert result["numpoints"] == 3 -def test_file_dict_none_clock(awg) -> None: +def test_file_dict_none_clock(awg: TektronixAWG5014) -> None: wf = np.array([0.0]) m1 = np.array([0]) m2 = np.array([0]) @@ -393,7 +399,7 @@ def test_invalid_raises(self) -> None: class TestMakeAwgFile: - def test_basic_awg_file(self, awg) -> None: + def test_basic_awg_file(self, awg: TektronixAWG5014) -> None: N = 25 rng = np.random.default_rng(42) waveforms = [[rng.random(N) * 2 - 1]] @@ -413,7 +419,7 @@ def test_basic_awg_file(self, awg) -> None: assert len(awgfile) > 0 assert isinstance(awgfile, bytes) - def test_multi_segment_awg_file(self, awg) -> None: + def test_multi_segment_awg_file(self, awg: TektronixAWG5014) -> None: """File with two sequence elements on one channel.""" N = 10 rng = np.random.default_rng(123) @@ -433,7 +439,7 @@ def test_multi_segment_awg_file(self, awg) -> None: ) assert len(awgfile) > 0 - def test_multi_channel_awg_file(self, awg) -> None: + def test_multi_channel_awg_file(self, awg: TektronixAWG5014) -> None: """File with one element each on two channels.""" N = 10 rng = np.random.default_rng(456) @@ -453,7 +459,7 @@ def test_multi_channel_awg_file(self, awg) -> None: ) assert len(awgfile) > 0 - def test_specific_channels(self, awg) -> None: + def test_specific_channels(self, awg: TektronixAWG5014) -> None: """Using the channels parameter to target channels 2 and 4.""" N = 10 rng = np.random.default_rng(789) @@ -474,7 +480,7 @@ def test_specific_channels(self, awg) -> None: ) assert len(awgfile) > 0 - def test_flat_input_format(self, awg) -> None: + def test_flat_input_format(self, awg: TektronixAWG5014) -> None: """make_awg_file also accepts flat (non-nested) waveform lists.""" N = 10 rng = np.random.default_rng(111) @@ -499,11 +505,11 @@ def test_flat_input_format(self, awg) -> None: class TestGenerateChannelCfg: - def test_returns_dict(self, awg) -> None: + def test_returns_dict(self, awg: TektronixAWG5014) -> None: cfg = awg.generate_channel_cfg() assert isinstance(cfg, dict) - def test_contains_settings_after_get(self, awg) -> None: + def test_contains_settings_after_get(self, awg: TektronixAWG5014) -> None: """After getting channel params, they should appear in the config.""" ch1 = awg.channels[0] ch1.amp.get() @@ -521,7 +527,7 @@ def test_contains_settings_after_get(self, awg) -> None: # ── generate_sequence_cfg ───────────────────────────────────────────── -def test_generate_sequence_cfg(awg) -> None: +def test_generate_sequence_cfg(awg: TektronixAWG5014) -> None: cfg = awg.generate_sequence_cfg() assert isinstance(cfg, dict) assert cfg["SAMPLING_RATE"] == 1e9 @@ -549,7 +555,7 @@ class TestLegacyChannelAttributes: ) MARKER_PARAMS = (("del", "delay"), ("high", "high"), ("low", "low")) - def test_legacy_channel_param_exists(self, awg) -> None: + def test_legacy_channel_param_exists(self, awg: TektronixAWG5014) -> None: """All old ch{i}_{param} names resolve to the correct parameter.""" for i in range(1, 5): for param in self.CHANNEL_PARAMS: @@ -562,7 +568,7 @@ def test_legacy_channel_param_exists(self, awg) -> None: f"{old_name} did not resolve to ch{i}.{param}" ) - def test_legacy_marker_param_exists(self, awg) -> None: + def test_legacy_marker_param_exists(self, awg: TektronixAWG5014) -> None: """All old ch{i}_m{j}_{param} names resolve to the correct parameter.""" for i in range(1, 5): for j in (1, 2): @@ -578,27 +584,27 @@ def test_legacy_marker_param_exists(self, awg) -> None: f"{old_name} did not resolve to ch{i}.m{j}.{new_name}" ) - def test_legacy_channel_param_warns(self, awg) -> None: + def test_legacy_channel_param_warns(self, awg: TektronixAWG5014) -> None: """Accessing an old channel param name emits QCoDeSDeprecationWarning.""" with pytest.warns(QCoDeSDeprecationWarning, match="ch1_amp.*ch1.amp"): _ = awg.ch1_amp - def test_legacy_marker_param_warns(self, awg) -> None: + def test_legacy_marker_param_warns(self, awg: TektronixAWG5014) -> None: """Accessing an old marker param name emits QCoDeSDeprecationWarning.""" with pytest.warns(QCoDeSDeprecationWarning, match="ch2_m1_high.*ch2.m1.high"): _ = awg.ch2_m1_high - def test_legacy_marker_del_warns(self, awg) -> None: + def test_legacy_marker_del_warns(self, awg: TektronixAWG5014) -> None: """The renamed 'del' -> 'delay' param emits a correct warning.""" with pytest.warns(QCoDeSDeprecationWarning, match="ch3_m2_del.*ch3.m2.delay"): _ = awg.ch3_m2_del - def test_nonexistent_attr_raises(self, awg) -> None: + def test_nonexistent_attr_raises(self, awg: TektronixAWG5014) -> None: """An attribute that doesn't match any legacy name still raises.""" with pytest.raises(AttributeError, match="no_such_attr"): _ = awg.no_such_attr - def test_nonexistent_legacy_style_raises(self, awg) -> None: + def test_nonexistent_legacy_style_raises(self, awg: TektronixAWG5014) -> None: """A ch{i}_* name that doesn't map to a real param still raises.""" with pytest.raises(AttributeError): _ = awg.ch1_bogus_param From 0b828c54c08e9fa3412b64da683b5b5e36180c1d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 8 Apr 2026 13:53:02 +0200 Subject: [PATCH 6/7] Address CI review feedback - Fix __getattr__ to delegate to super().__getattr__ instead of raising AttributeError directly, preserving InstrumentBase attribute delegation for submodules, parameters, and functions. - Add '"ESIGnal"' to addinptrans dict in generate_channel_cfg to handle the alternate valid value accepted by the add_input validator. - Remove unnecessary test classes; convert to plain functions since self was unused (classes were only for grouping). - Fix Generator fixture return type to use 3 type arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../instrument_drivers/tektronix/AWG5014.py | 11 +- tests/drivers/test_tektronix_AWG5014C.py | 824 +++++++++--------- 2 files changed, 440 insertions(+), 395 deletions(-) diff --git a/src/qcodes/instrument_drivers/tektronix/AWG5014.py b/src/qcodes/instrument_drivers/tektronix/AWG5014.py index 6e996cb5e879..e1671bfebef9 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -645,9 +645,7 @@ def __getattr__(self, name: str) -> Any: stacklevel=2, ) return getattr(ch, param) - raise AttributeError( - f"'{type(self).__name__}' object has no attribute '{name}'" - ) + return super().__getattr__(name) # Convenience parser def newlinestripper(self, string: str) -> str: @@ -1183,7 +1181,12 @@ def generate_channel_cfg(self) -> dict[str, float | None]: None: None, } - addinptrans = {'"ESIG"': 1, '""': 0, None: None} + addinptrans: dict[str | None, int | None] = { + '"ESIG"': 1, + '"ESIGnal"': 1, + '""': 0, + None: None, + } def mrkdeltrans(x: float | None) -> float | None: if x is None: diff --git a/tests/drivers/test_tektronix_AWG5014C.py b/tests/drivers/test_tektronix_AWG5014C.py index 6baa0c0434fa..26b401c16b2d 100644 --- a/tests/drivers/test_tektronix_AWG5014C.py +++ b/tests/drivers/test_tektronix_AWG5014C.py @@ -20,7 +20,7 @@ @pytest.fixture(scope="function") -def awg() -> Generator[TektronixAWG5014]: +def awg() -> Generator[TektronixAWG5014, None, None]: awg_sim = TektronixAWG5014( "awg_sim", address="GPIB0::1::INSTR", @@ -80,268 +80,301 @@ def test_marker_channel_and_marker_numbers(awg: TektronixAWG5014) -> None: # ── Helper functions ────────────────────────────────────────────────── -class TestParsestr: - def test_strips_quotes_and_whitespace(self) -> None: - assert parsestr(' "hello" ') == "hello" +def test_strips_quotes_and_whitespace() -> None: + assert parsestr(' "hello" ') == "hello" - def test_no_quotes(self) -> None: - assert parsestr("plain") == "plain" - def test_empty_quoted_string(self) -> None: - assert parsestr('""') == "" +def test_no_quotes() -> None: + assert parsestr("plain") == "plain" - def test_strips_only_outer_quotes(self) -> None: - assert parsestr('"a"b"') == 'a"b' +def test_empty_quoted_string() -> None: + assert parsestr('""') == "" -class TestNewlinestripper: - def test_strips_trailing_newline(self, awg: TektronixAWG5014) -> None: - assert awg.newlinestripper("hello\n") == "hello" - def test_no_trailing_newline(self, awg: TektronixAWG5014) -> None: - assert awg.newlinestripper("hello") == "hello" +def test_strips_only_outer_quotes() -> None: + assert parsestr('"a"b"') == 'a"b' - def test_empty_string(self, awg: TektronixAWG5014) -> None: - assert awg.newlinestripper("") == "" - def test_only_newline(self, awg: TektronixAWG5014) -> None: - assert awg.newlinestripper("\n") == "" +def test_strips_trailing_newline(awg: TektronixAWG5014) -> None: + assert awg.newlinestripper("hello\n") == "hello" -class TestTekOutofrangeGetParser: - def test_normal_value(self, awg: TektronixAWG5014) -> None: - assert awg._tek_outofrange_get_parser("1.5") == 1.5 +def test_no_trailing_newline(awg: TektronixAWG5014) -> None: + assert awg.newlinestripper("hello") == "hello" - def test_out_of_range_value(self, awg: TektronixAWG5014) -> None: - assert awg._tek_outofrange_get_parser("9.9e37") == float("INF") - def test_zero(self, awg: TektronixAWG5014) -> None: - assert awg._tek_outofrange_get_parser("0") == 0.0 +def test_empty_string(awg: TektronixAWG5014) -> None: + assert awg.newlinestripper("") == "" + + +def test_only_newline(awg: TektronixAWG5014) -> None: + assert awg.newlinestripper("\n") == "" + + +def test_normal_value(awg: TektronixAWG5014) -> None: + assert awg._tek_outofrange_get_parser("1.5") == 1.5 + + +def test_out_of_range_value(awg: TektronixAWG5014) -> None: + assert awg._tek_outofrange_get_parser("9.9e37") == float("INF") + + +def test_zero(awg: TektronixAWG5014) -> None: + assert awg._tek_outofrange_get_parser("0") == 0.0 # ── Instrument-level parameters (via sim) ───────────────────────────── -class TestInstrumentParameters: - def test_clock_freq_get(self, awg: TektronixAWG5014) -> None: - val = awg.clock_freq.get() - assert val == 1e9 +def test_clock_freq_get(awg: TektronixAWG5014) -> None: + val = awg.clock_freq.get() + assert val == 1e9 + + +def test_trigger_impedance_get(awg: TektronixAWG5014) -> None: + val = awg.trigger_impedance.get() + assert val == 50.0 + + +def test_clock_source_get(awg: TektronixAWG5014) -> None: + assert awg.clock_source.get() == "INT" - def test_trigger_impedance_get(self, awg: TektronixAWG5014) -> None: - val = awg.trigger_impedance.get() - assert val == 50.0 - def test_clock_source_get(self, awg: TektronixAWG5014) -> None: - assert awg.clock_source.get() == "INT" +def test_ref_source_get(awg: TektronixAWG5014) -> None: + assert awg.ref_source.get() == "INT" - def test_ref_source_get(self, awg: TektronixAWG5014) -> None: - assert awg.ref_source.get() == "INT" - def test_trigger_source_get(self, awg: TektronixAWG5014) -> None: - assert awg.trigger_source.get() == "INT" +def test_trigger_source_get(awg: TektronixAWG5014) -> None: + assert awg.trigger_source.get() == "INT" - def test_trigger_level_get(self, awg: TektronixAWG5014) -> None: - assert awg.trigger_level.get() == 0.0 - def test_event_impedance_get(self, awg: TektronixAWG5014) -> None: - assert awg.event_impedance.get() == 50.0 +def test_trigger_level_get(awg: TektronixAWG5014) -> None: + assert awg.trigger_level.get() == 0.0 - def test_event_level_get(self, awg: TektronixAWG5014) -> None: - assert awg.event_level.get() == 0.0 - def test_run_mode_get(self, awg: TektronixAWG5014) -> None: - assert awg.run_mode.get() == "CONT" +def test_event_impedance_get(awg: TektronixAWG5014) -> None: + assert awg.event_impedance.get() == 50.0 - def test_trigger_slope_get(self, awg: TektronixAWG5014) -> None: - assert awg.trigger_slope.get() == "POS" - def test_event_polarity_get(self, awg: TektronixAWG5014) -> None: - assert awg.event_polarity.get() == "POS" +def test_event_level_get(awg: TektronixAWG5014) -> None: + assert awg.event_level.get() == 0.0 - def test_event_jump_timing_get(self, awg: TektronixAWG5014) -> None: - assert awg.event_jump_timing.get() == "SYNC" - def test_DC_output_get(self, awg: TektronixAWG5014) -> None: - assert awg.DC_output.get() == 0 +def test_run_mode_get(awg: TektronixAWG5014) -> None: + assert awg.run_mode.get() == "CONT" - def test_sequence_length_get(self, awg: TektronixAWG5014) -> None: - assert awg.sequence_length.get() == 0 - def test_get_state(self, awg: TektronixAWG5014) -> None: - assert awg.get_state() == "Idle" +def test_trigger_slope_get(awg: TektronixAWG5014) -> None: + assert awg.trigger_slope.get() == "POS" + + +def test_event_polarity_get(awg: TektronixAWG5014) -> None: + assert awg.event_polarity.get() == "POS" + + +def test_event_jump_timing_get(awg: TektronixAWG5014) -> None: + assert awg.event_jump_timing.get() == "SYNC" + + +def test_DC_output_get(awg: TektronixAWG5014) -> None: + assert awg.DC_output.get() == 0 + + +def test_sequence_length_get(awg: TektronixAWG5014) -> None: + assert awg.sequence_length.get() == 0 + + +def test_get_state(awg: TektronixAWG5014) -> None: + assert awg.get_state() == "Idle" # ── Channel parameters (via sim) ────────────────────────────────────── -class TestChannelParameters: - def test_channel_state_get(self, awg: TektronixAWG5014) -> None: - assert awg.channels[0].state.get() == 0 +def test_channel_state_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].state.get() == 0 + - def test_channel_amp_get(self, awg: TektronixAWG5014) -> None: - assert awg.channels[0].amp.get() == 0.5 +def test_channel_amp_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].amp.get() == 0.5 - def test_channel_offset_get(self, awg: TektronixAWG5014) -> None: - assert awg.channels[0].offset.get() == 0.0 - def test_channel_dc_out_get(self, awg: TektronixAWG5014) -> None: - assert awg.channels[0].DC_out.get() == 0.0 +def test_channel_offset_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].offset.get() == 0.0 - def test_channel_filter_get(self, awg: TektronixAWG5014) -> None: - # sim returns 9.9e37 → parsed to inf - assert awg.channels[0].filter.get() == float("inf") - def test_all_channels_have_consistent_defaults(self, awg: TektronixAWG5014) -> None: - """All four channels should return the same default values.""" - for ch in awg.channels: - assert ch.state.get() == 0 - assert ch.amp.get() == 0.5 - assert ch.offset.get() == 0.0 +def test_channel_dc_out_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].DC_out.get() == 0.0 + + +def test_channel_filter_get(awg: TektronixAWG5014) -> None: + # sim returns 9.9e37 → parsed to inf + assert awg.channels[0].filter.get() == float("inf") + + +def test_all_channels_have_consistent_defaults(awg: TektronixAWG5014) -> None: + """All four channels should return the same default values.""" + for ch in awg.channels: + assert ch.state.get() == 0 + assert ch.amp.get() == 0.5 + assert ch.offset.get() == 0.0 # ── Marker parameters (via sim) ─────────────────────────────────────── -class TestMarkerParameters: - def test_marker_delay_get(self, awg: TektronixAWG5014) -> None: - ch1 = awg.channels[0] - assert ch1.m1.delay.get() == 0.0 +def test_marker_delay_get(awg: TektronixAWG5014) -> None: + ch1 = awg.channels[0] + assert ch1.m1.delay.get() == 0.0 + - def test_marker_high_get(self, awg: TektronixAWG5014) -> None: - ch1 = awg.channels[0] - assert ch1.m1.high.get() == 1.0 +def test_marker_high_get(awg: TektronixAWG5014) -> None: + ch1 = awg.channels[0] + assert ch1.m1.high.get() == 1.0 - def test_marker_low_get(self, awg: TektronixAWG5014) -> None: - ch1 = awg.channels[0] - assert ch1.m1.low.get() == 0.0 - def test_all_markers_have_consistent_defaults(self, awg: TektronixAWG5014) -> None: - """All 8 markers should share the same defaults.""" - for ch in awg.channels: - for mrk in (ch.m1, ch.m2): - assert mrk.delay.get() == 0.0 - assert mrk.high.get() == 1.0 - assert mrk.low.get() == 0.0 +def test_marker_low_get(awg: TektronixAWG5014) -> None: + ch1 = awg.channels[0] + assert ch1.m1.low.get() == 0.0 + + +def test_all_markers_have_consistent_defaults(awg: TektronixAWG5014) -> None: + """All 8 markers should share the same defaults.""" + for ch in awg.channels: + for mrk in (ch.m1, ch.m2): + assert mrk.delay.get() == 0.0 + assert mrk.high.get() == 1.0 + assert mrk.low.get() == 0.0 # ── all_channels_on / all_channels_off ──────────────────────────────── -class TestAllChannelsOnOff: - def test_all_channels_off(self, awg: TektronixAWG5014) -> None: - awg.all_channels_off() - for ch in awg.channels: - assert ch.state.get() == 0 +def test_all_channels_off(awg: TektronixAWG5014) -> None: + awg.all_channels_off() + for ch in awg.channels: + assert ch.state.get() == 0 + + +def test_all_channels_on(awg: TektronixAWG5014) -> None: + awg.all_channels_on() + for ch in awg.channels: + assert ch.state.get() == 1 - def test_all_channels_on(self, awg: TektronixAWG5014) -> None: - awg.all_channels_on() - for ch in awg.channels: - assert ch.state.get() == 1 - def test_all_channels_on_then_off(self, awg: TektronixAWG5014) -> None: - awg.all_channels_on() - awg.all_channels_off() - for ch in awg.channels: - assert ch.state.get() == 0 +def test_all_channels_on_then_off(awg: TektronixAWG5014) -> None: + awg.all_channels_on() + awg.all_channels_off() + for ch in awg.channels: + assert ch.state.get() == 0 # ── _pack_waveform ──────────────────────────────────────────────────── -class TestPackWaveform: - def test_basic_pack(self, awg: TektronixAWG5014) -> None: - N = 25 - rng = np.random.default_rng(42) - wf = rng.random(N) * 2 - 1 # values in [-1, 1] - m1 = rng.integers(0, 2, N) - m2 = rng.integers(0, 2, N) - - result = awg._pack_waveform(wf, m1, m2) - - assert result is not None - assert len(result) == N - assert result.dtype == np.uint16 - - def test_known_values(self, awg: TektronixAWG5014) -> None: - """Verify encoding of specific known inputs.""" - wf = np.array([0.0]) - m1 = np.array([0]) - m2 = np.array([0]) - result = awg._pack_waveform(wf, m1, m2) - # wf=0 → 0*8191+8191.5 = 8191.5, trunc → 8191 - assert result[0] == 8191 - - def test_markers_set_correct_bits(self, awg: TektronixAWG5014) -> None: - """Marker bits should be at positions 14 and 15.""" - wf = np.array([0.0]) - # m1 only - r_m1 = awg._pack_waveform(wf, np.array([1]), np.array([0])) - assert r_m1[0] & 0x4000 # bit 14 - # m2 only - r_m2 = awg._pack_waveform(wf, np.array([0]), np.array([1])) - assert r_m2[0] & 0x8000 # bit 15 - # both - r_both = awg._pack_waveform(wf, np.array([1]), np.array([1])) - assert r_both[0] & 0xC000 == 0xC000 - - def test_mismatched_lengths_raises(self, awg: TektronixAWG5014) -> None: - with pytest.raises(Exception, match=r"sizes.*do not match"): - awg._pack_waveform(np.array([0.0, 0.0]), np.array([0]), np.array([0])) - - def test_waveform_out_of_bounds_raises(self, awg: TektronixAWG5014) -> None: - with pytest.raises(TypeError, match="Waveform values out of bo"): - awg._pack_waveform(np.array([1.5]), np.array([0]), np.array([0])) - - def test_waveform_below_bounds_raises(self, awg: TektronixAWG5014) -> None: - with pytest.raises(TypeError, match="Waveform values out of bo"): - awg._pack_waveform(np.array([-1.5]), np.array([0]), np.array([0])) - - def test_invalid_marker1_raises(self, awg: TektronixAWG5014) -> None: - with pytest.raises(TypeError, match="Marker 1"): - awg._pack_waveform(np.array([0.0]), np.array([2]), np.array([0])) - - def test_invalid_marker2_raises(self, awg: TektronixAWG5014) -> None: - with pytest.raises(TypeError, match="Marker 2"): - awg._pack_waveform(np.array([0.0]), np.array([0]), np.array([3])) +def test_basic_pack(awg: TektronixAWG5014) -> None: + N = 25 + rng = np.random.default_rng(42) + wf = rng.random(N) * 2 - 1 # values in [-1, 1] + m1 = rng.integers(0, 2, N) + m2 = rng.integers(0, 2, N) + + result = awg._pack_waveform(wf, m1, m2) + + assert result is not None + assert len(result) == N + assert result.dtype == np.uint16 + + +def test_known_values(awg: TektronixAWG5014) -> None: + """Verify encoding of specific known inputs.""" + wf = np.array([0.0]) + m1 = np.array([0]) + m2 = np.array([0]) + result = awg._pack_waveform(wf, m1, m2) + # wf=0 → 0*8191+8191.5 = 8191.5, trunc → 8191 + assert result[0] == 8191 + + +def test_markers_set_correct_bits(awg: TektronixAWG5014) -> None: + """Marker bits should be at positions 14 and 15.""" + wf = np.array([0.0]) + # m1 only + r_m1 = awg._pack_waveform(wf, np.array([1]), np.array([0])) + assert r_m1[0] & 0x4000 # bit 14 + # m2 only + r_m2 = awg._pack_waveform(wf, np.array([0]), np.array([1])) + assert r_m2[0] & 0x8000 # bit 15 + # both + r_both = awg._pack_waveform(wf, np.array([1]), np.array([1])) + assert r_both[0] & 0xC000 == 0xC000 + + +def test_mismatched_lengths_raises(awg: TektronixAWG5014) -> None: + with pytest.raises(Exception, match=r"sizes.*do not match"): + awg._pack_waveform(np.array([0.0, 0.0]), np.array([0]), np.array([0])) + + +def test_waveform_out_of_bounds_raises(awg: TektronixAWG5014) -> None: + with pytest.raises(TypeError, match="Waveform values out of bo"): + awg._pack_waveform(np.array([1.5]), np.array([0]), np.array([0])) + + +def test_waveform_below_bounds_raises(awg: TektronixAWG5014) -> None: + with pytest.raises(TypeError, match="Waveform values out of bo"): + awg._pack_waveform(np.array([-1.5]), np.array([0]), np.array([0])) + + +def test_invalid_marker1_raises(awg: TektronixAWG5014) -> None: + with pytest.raises(TypeError, match="Marker 1"): + awg._pack_waveform(np.array([0.0]), np.array([2]), np.array([0])) + + +def test_invalid_marker2_raises(awg: TektronixAWG5014) -> None: + with pytest.raises(TypeError, match="Marker 2"): + awg._pack_waveform(np.array([0.0]), np.array([0]), np.array([3])) # ── _pack_record ────────────────────────────────────────────────────── -class TestPackRecord: - def test_pack_short(self, awg: TektronixAWG5014) -> None: - """Pack a 16-bit integer record.""" - result = awg._pack_record("MAGIC", 5000, "h") - # name = "MAGIC\0" (6 bytes), data = 2 bytes (short) - name_size, data_size = struct.unpack_from(" None: - """Pack a 64-bit float record.""" - result = awg._pack_record("SAMPLING_RATE", 1e9, "d") - name_size, data_size = struct.unpack_from(" None: - """Pack a string record.""" - result = awg._pack_record("TEST_NAME", "hello\x00", "6s") - name_size, data_size = struct.unpack_from(" None: - """Pack a numpy array as unsigned 16-bit integers.""" - data = np.array([1, 2, 3], dtype=np.uint16) - result = awg._pack_record("WF_DATA", data, "3H") - _name_size, data_size = struct.unpack_from(" None: + """Pack a 16-bit integer record.""" + result = awg._pack_record("MAGIC", 5000, "h") + # name = "MAGIC\0" (6 bytes), data = 2 bytes (short) + name_size, data_size = struct.unpack_from(" None: + """Pack a 64-bit float record.""" + result = awg._pack_record("SAMPLING_RATE", 1e9, "d") + name_size, data_size = struct.unpack_from(" None: + """Pack a string record.""" + result = awg._pack_record("TEST_NAME", "hello\x00", "6s") + name_size, data_size = struct.unpack_from(" None: + """Pack a numpy array as unsigned 16-bit integers.""" + data = np.array([1, 2, 3], dtype=np.uint16) + result = awg._pack_record("WF_DATA", data, "3H") + _name_size, data_size = struct.unpack_from(" None: # ── parse_marker_channel_name ───────────────────────────────────────── -class TestParseMarkerChannelName: - def test_valid_1m1(self) -> None: - result = TektronixAWG5014.parse_marker_channel_name("1M1") - assert result.channel == 1 - assert result.marker == 1 +def test_valid_1m1() -> None: + result = TektronixAWG5014.parse_marker_channel_name("1M1") + assert result.channel == 1 + assert result.marker == 1 - def test_valid_3m2(self) -> None: - result = TektronixAWG5014.parse_marker_channel_name("3M2") - assert result.channel == 3 - assert result.marker == 2 - def test_valid_4m1(self) -> None: - result = TektronixAWG5014.parse_marker_channel_name("4M1") - assert result.channel == 4 - assert result.marker == 1 +def test_valid_3m2() -> None: + result = TektronixAWG5014.parse_marker_channel_name("3M2") + assert result.channel == 3 + assert result.marker == 2 - def test_invalid_raises(self) -> None: - with pytest.raises(AssertionError): - TektronixAWG5014.parse_marker_channel_name("bad") + +def test_valid_4m1() -> None: + result = TektronixAWG5014.parse_marker_channel_name("4M1") + assert result.channel == 4 + assert result.marker == 1 + + +def test_invalid_raises() -> None: + with pytest.raises(AssertionError): + TektronixAWG5014.parse_marker_channel_name("bad") # ── make_awg_file ───────────────────────────────────────────────────── -class TestMakeAwgFile: - def test_basic_awg_file(self, awg: TektronixAWG5014) -> None: - N = 25 - rng = np.random.default_rng(42) - waveforms = [[rng.random(N) * 2 - 1]] - m1s = [[rng.integers(0, 2, N)]] - m2s = [[rng.integers(0, 2, N)]] - - awgfile = awg.make_awg_file( - waveforms, - m1s, - m2s, - [1], - [0], - [0], - [0], - preservechannelsettings=False, - ) - assert len(awgfile) > 0 - assert isinstance(awgfile, bytes) - - def test_multi_segment_awg_file(self, awg: TektronixAWG5014) -> None: - """File with two sequence elements on one channel.""" - N = 10 - rng = np.random.default_rng(123) - waveforms = [[rng.random(N) * 2 - 1, rng.random(N) * 2 - 1]] - m1s = [[rng.integers(0, 2, N), rng.integers(0, 2, N)]] - m2s = [[rng.integers(0, 2, N), rng.integers(0, 2, N)]] - - awgfile = awg.make_awg_file( - waveforms, - m1s, - m2s, - [1, 1], - [0, 0], - [0, 0], - [0, 0], - preservechannelsettings=False, - ) - assert len(awgfile) > 0 - - def test_multi_channel_awg_file(self, awg: TektronixAWG5014) -> None: - """File with one element each on two channels.""" - N = 10 - rng = np.random.default_rng(456) - waveforms = [[rng.random(N) * 2 - 1], [rng.random(N) * 2 - 1]] - m1s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] - m2s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] - - awgfile = awg.make_awg_file( - waveforms, - m1s, - m2s, - [1], - [0], - [0], - [0], - preservechannelsettings=False, - ) - assert len(awgfile) > 0 - - def test_specific_channels(self, awg: TektronixAWG5014) -> None: - """Using the channels parameter to target channels 2 and 4.""" - N = 10 - rng = np.random.default_rng(789) - waveforms = [[rng.random(N) * 2 - 1], [rng.random(N) * 2 - 1]] - m1s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] - m2s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] - - awgfile = awg.make_awg_file( - waveforms, - m1s, - m2s, - [1], - [0], - [0], - [0], - channels=[2, 4], - preservechannelsettings=False, - ) - assert len(awgfile) > 0 - - def test_flat_input_format(self, awg: TektronixAWG5014) -> None: - """make_awg_file also accepts flat (non-nested) waveform lists.""" - N = 10 - rng = np.random.default_rng(111) - waveforms = [rng.random(N) * 2 - 1] - m1s = [rng.integers(0, 2, N)] - m2s = [rng.integers(0, 2, N)] - - awgfile = awg.make_awg_file( - waveforms, - m1s, - m2s, - [1], - [0], - [0], - [0], - preservechannelsettings=False, - ) - assert len(awgfile) > 0 +def test_basic_awg_file(awg: TektronixAWG5014) -> None: + N = 25 + rng = np.random.default_rng(42) + waveforms = [[rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + assert isinstance(awgfile, bytes) + + +def test_multi_segment_awg_file(awg: TektronixAWG5014) -> None: + """File with two sequence elements on one channel.""" + N = 10 + rng = np.random.default_rng(123) + waveforms = [[rng.random(N) * 2 - 1, rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N), rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N), rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1, 1], + [0, 0], + [0, 0], + [0, 0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + +def test_multi_channel_awg_file(awg: TektronixAWG5014) -> None: + """File with one element each on two channels.""" + N = 10 + rng = np.random.default_rng(456) + waveforms = [[rng.random(N) * 2 - 1], [rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + +def test_specific_channels(awg: TektronixAWG5014) -> None: + """Using the channels parameter to target channels 2 and 4.""" + N = 10 + rng = np.random.default_rng(789) + waveforms = [[rng.random(N) * 2 - 1], [rng.random(N) * 2 - 1]] + m1s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + m2s = [[rng.integers(0, 2, N)], [rng.integers(0, 2, N)]] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + channels=[2, 4], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 + + +def test_flat_input_format(awg: TektronixAWG5014) -> None: + """make_awg_file also accepts flat (non-nested) waveform lists.""" + N = 10 + rng = np.random.default_rng(111) + waveforms = [rng.random(N) * 2 - 1] + m1s = [rng.integers(0, 2, N)] + m2s = [rng.integers(0, 2, N)] + + awgfile = awg.make_awg_file( + waveforms, + m1s, + m2s, + [1], + [0], + [0], + [0], + preservechannelsettings=False, + ) + assert len(awgfile) > 0 # ── generate_channel_cfg ────────────────────────────────────────────── -class TestGenerateChannelCfg: - def test_returns_dict(self, awg: TektronixAWG5014) -> None: - cfg = awg.generate_channel_cfg() - assert isinstance(cfg, dict) +def test_returns_dict(awg: TektronixAWG5014) -> None: + cfg = awg.generate_channel_cfg() + assert isinstance(cfg, dict) + - def test_contains_settings_after_get(self, awg: TektronixAWG5014) -> None: - """After getting channel params, they should appear in the config.""" - ch1 = awg.channels[0] - ch1.amp.get() - ch1.offset.get() - ch1.m1.high.get() - ch1.m1.low.get() +def test_contains_settings_after_get(awg: TektronixAWG5014) -> None: + """After getting channel params, they should appear in the config.""" + ch1 = awg.channels[0] + ch1.amp.get() + ch1.offset.get() + ch1.m1.high.get() + ch1.m1.low.get() - cfg = awg.generate_channel_cfg() - assert "ANALOG_AMPLITUDE_1" in cfg - assert "ANALOG_OFFSET_1" in cfg - assert "MARKER1_HIGH_1" in cfg - assert "MARKER1_LOW_1" in cfg + cfg = awg.generate_channel_cfg() + assert "ANALOG_AMPLITUDE_1" in cfg + assert "ANALOG_OFFSET_1" in cfg + assert "MARKER1_HIGH_1" in cfg + assert "MARKER1_LOW_1" in cfg # ── generate_sequence_cfg ───────────────────────────────────────────── @@ -539,72 +577,76 @@ def test_generate_sequence_cfg(awg: TektronixAWG5014) -> None: # ── Legacy attribute backward compatibility ─────────────────────────── -class TestLegacyChannelAttributes: - """Tests that the old flat ch{i}_* attribute names still work - but emit a QCoDeSDeprecationWarning.""" +"""Tests that the old flat ch{i}_* attribute names still work +but emit a QCoDeSDeprecationWarning.""" + +CHANNEL_PARAMS = ( + "state", + "amp", + "offset", + "waveform", + "direct_output", + "add_input", + "filter", + "DC_out", +) +MARKER_PARAMS = (("del", "delay"), ("high", "high"), ("low", "low")) + + +def test_legacy_channel_param_exists(awg: TektronixAWG5014) -> None: + """All old ch{i}_{param} names resolve to the correct parameter.""" + for i in range(1, 5): + for param in CHANNEL_PARAMS: + old_name = f"ch{i}_{param}" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", QCoDeSDeprecationWarning) + old_attr = getattr(awg, old_name) + new_attr = getattr(awg.submodules[f"ch{i}"], param) + assert old_attr is new_attr, f"{old_name} did not resolve to ch{i}.{param}" - CHANNEL_PARAMS = ( - "state", - "amp", - "offset", - "waveform", - "direct_output", - "add_input", - "filter", - "DC_out", - ) - MARKER_PARAMS = (("del", "delay"), ("high", "high"), ("low", "low")) - def test_legacy_channel_param_exists(self, awg: TektronixAWG5014) -> None: - """All old ch{i}_{param} names resolve to the correct parameter.""" - for i in range(1, 5): - for param in self.CHANNEL_PARAMS: - old_name = f"ch{i}_{param}" +def test_legacy_marker_param_exists(awg: TektronixAWG5014) -> None: + """All old ch{i}_m{j}_{param} names resolve to the correct parameter.""" + for i in range(1, 5): + for j in (1, 2): + for old_suffix, new_name in MARKER_PARAMS: + old_name = f"ch{i}_m{j}_{old_suffix}" with warnings.catch_warnings(): warnings.simplefilter("ignore", QCoDeSDeprecationWarning) old_attr = getattr(awg, old_name) - new_attr = getattr(awg.submodules[f"ch{i}"], param) + new_attr = getattr( + awg.submodules[f"ch{i}"].submodules[f"m{j}"], new_name + ) assert old_attr is new_attr, ( - f"{old_name} did not resolve to ch{i}.{param}" + f"{old_name} did not resolve to ch{i}.m{j}.{new_name}" ) - def test_legacy_marker_param_exists(self, awg: TektronixAWG5014) -> None: - """All old ch{i}_m{j}_{param} names resolve to the correct parameter.""" - for i in range(1, 5): - for j in (1, 2): - for old_suffix, new_name in self.MARKER_PARAMS: - old_name = f"ch{i}_m{j}_{old_suffix}" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", QCoDeSDeprecationWarning) - old_attr = getattr(awg, old_name) - new_attr = getattr( - awg.submodules[f"ch{i}"].submodules[f"m{j}"], new_name - ) - assert old_attr is new_attr, ( - f"{old_name} did not resolve to ch{i}.m{j}.{new_name}" - ) - - def test_legacy_channel_param_warns(self, awg: TektronixAWG5014) -> None: - """Accessing an old channel param name emits QCoDeSDeprecationWarning.""" - with pytest.warns(QCoDeSDeprecationWarning, match="ch1_amp.*ch1.amp"): - _ = awg.ch1_amp - - def test_legacy_marker_param_warns(self, awg: TektronixAWG5014) -> None: - """Accessing an old marker param name emits QCoDeSDeprecationWarning.""" - with pytest.warns(QCoDeSDeprecationWarning, match="ch2_m1_high.*ch2.m1.high"): - _ = awg.ch2_m1_high - - def test_legacy_marker_del_warns(self, awg: TektronixAWG5014) -> None: - """The renamed 'del' -> 'delay' param emits a correct warning.""" - with pytest.warns(QCoDeSDeprecationWarning, match="ch3_m2_del.*ch3.m2.delay"): - _ = awg.ch3_m2_del - - def test_nonexistent_attr_raises(self, awg: TektronixAWG5014) -> None: - """An attribute that doesn't match any legacy name still raises.""" - with pytest.raises(AttributeError, match="no_such_attr"): - _ = awg.no_such_attr - - def test_nonexistent_legacy_style_raises(self, awg: TektronixAWG5014) -> None: - """A ch{i}_* name that doesn't map to a real param still raises.""" - with pytest.raises(AttributeError): - _ = awg.ch1_bogus_param + +def test_legacy_channel_param_warns(awg: TektronixAWG5014) -> None: + """Accessing an old channel param name emits QCoDeSDeprecationWarning.""" + with pytest.warns(QCoDeSDeprecationWarning, match="ch1_amp.*ch1.amp"): + _ = awg.ch1_amp + + +def test_legacy_marker_param_warns(awg: TektronixAWG5014) -> None: + """Accessing an old marker param name emits QCoDeSDeprecationWarning.""" + with pytest.warns(QCoDeSDeprecationWarning, match="ch2_m1_high.*ch2.m1.high"): + _ = awg.ch2_m1_high + + +def test_legacy_marker_del_warns(awg: TektronixAWG5014) -> None: + """The renamed 'del' -> 'delay' param emits a correct warning.""" + with pytest.warns(QCoDeSDeprecationWarning, match="ch3_m2_del.*ch3.m2.delay"): + _ = awg.ch3_m2_del + + +def test_nonexistent_attr_raises(awg: TektronixAWG5014) -> None: + """An attribute that doesn't match any legacy name still raises.""" + with pytest.raises(AttributeError, match="no_such_attr"): + _ = awg.no_such_attr + + +def test_nonexistent_legacy_style_raises(awg: TektronixAWG5014) -> None: + """A ch{i}_* name that doesn't map to a real param still raises.""" + with pytest.raises(AttributeError): + _ = awg.ch1_bogus_param From af5d1eb19ecfa233d5431abf39df75511f5fa155 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 8 Apr 2026 14:18:40 +0200 Subject: [PATCH 7/7] Fix pyright errors in AWG5014C tests Add not-None assertions before np.array_equal calls in test_file_dict to narrow the union type returned by _file_dict. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/drivers/test_tektronix_AWG5014C.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/drivers/test_tektronix_AWG5014C.py b/tests/drivers/test_tektronix_AWG5014C.py index 26b401c16b2d..56a1ea6efd53 100644 --- a/tests/drivers/test_tektronix_AWG5014C.py +++ b/tests/drivers/test_tektronix_AWG5014C.py @@ -388,6 +388,9 @@ def test_file_dict(awg: TektronixAWG5014) -> None: result = awg._file_dict(wf, m1, m2, clock) + assert result["w"] is not None + assert result["m1"] is not None + assert result["m2"] is not None assert np.array_equal(result["w"], wf) assert np.array_equal(result["m1"], m1) assert np.array_equal(result["m2"], m2)