From 3a5d913971cc00ba41dea055c4229bdc9f02b6e8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 1 Apr 2026 22:06:21 +0200 Subject: [PATCH] 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