diff --git a/docs/changes/newsfragments/7996.improved_driver b/docs/changes/newsfragments/7996.improved_driver new file mode 100644 index 00000000000..387976168ab --- /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. 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 6c51d9290d4..abe47966ccc 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)" ] }, { diff --git a/src/qcodes/instrument/sims/Tektronix_AWG5014C.yaml b/src/qcodes/instrument/sims/Tektronix_AWG5014C.yaml index e0af9f6e714..3176f588cd6 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/src/qcodes/instrument_drivers/tektronix/AWG5014.py b/src/qcodes/instrument_drivers/tektronix/AWG5014.py index 5907bff2730..e1671bfebef 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,50 @@ 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) + return super().__getattr__(name) + # Convenience parser def newlinestripper(self, string: str) -> str: if string.endswith("\n"): @@ -668,13 +812,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 +1169,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 +1180,68 @@ 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()], - ] + addinptrans: dict[str | None, int | None] = { + '"ESIG"': 1, + '"ESIGnal"': 1, + '""': 0, + None: None, + } - # 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 +1857,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 06862d560cc..6a13b926e87 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 cbc516c9f88..56a1ea6efd5 100644 --- a/tests/drivers/test_tektronix_AWG5014C.py +++ b/tests/drivers/test_tektronix_AWG5014C.py @@ -1,11 +1,26 @@ +from __future__ import annotations + +import struct +import warnings +from typing import TYPE_CHECKING + 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 + +if TYPE_CHECKING: + from collections.abc import Generator @pytest.fixture(scope="function") -def awg(): +def awg() -> Generator[TektronixAWG5014, None, None]: awg_sim = TektronixAWG5014( "awg_sim", address="GPIB0::1::INSTR", @@ -18,46 +33,623 @@ def awg(): awg_sim.close() -def test_init_awg(awg) -> None: +# ── Initialisation and structure ────────────────────────────────────── + + +def test_init_awg(awg: TektronixAWG5014) -> None: idn_dict = awg.IDN() assert idn_dict["vendor"] == "QCoDeS" -def test_pack_waveform(awg) -> None: - N = 25 +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: TektronixAWG5014) -> None: + """Each channel should be a TektronixAWG5014Channel.""" + for ch in awg.channels: + assert isinstance(ch, TektronixAWG5014Channel) + + +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: TektronixAWG5014) -> 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: 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 + assert ch.m1.marker == 1 + assert ch.m2.channel == i + assert ch.m2.marker == 2 + + +# ── Helper functions ────────────────────────────────────────────────── + + +def test_strips_quotes_and_whitespace() -> None: + assert parsestr(' "hello" ') == "hello" + + +def test_no_quotes() -> None: + assert parsestr("plain") == "plain" + + +def test_empty_quoted_string() -> None: + assert parsestr('""') == "" + + +def test_strips_only_outer_quotes() -> None: + assert parsestr('"a"b"') == 'a"b' + + +def test_strips_trailing_newline(awg: TektronixAWG5014) -> None: + assert awg.newlinestripper("hello\n") == "hello" + + +def test_no_trailing_newline(awg: TektronixAWG5014) -> None: + assert awg.newlinestripper("hello") == "hello" + + +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) ───────────────────────────── + + +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_ref_source_get(awg: TektronixAWG5014) -> None: + assert awg.ref_source.get() == "INT" + + +def test_trigger_source_get(awg: TektronixAWG5014) -> None: + assert awg.trigger_source.get() == "INT" + + +def test_trigger_level_get(awg: TektronixAWG5014) -> None: + assert awg.trigger_level.get() == 0.0 + + +def test_event_impedance_get(awg: TektronixAWG5014) -> None: + assert awg.event_impedance.get() == 50.0 + + +def test_event_level_get(awg: TektronixAWG5014) -> None: + assert awg.event_level.get() == 0.0 - rng = np.random.default_rng() - waveform = rng.random(N) + +def test_run_mode_get(awg: TektronixAWG5014) -> None: + assert awg.run_mode.get() == "CONT" + + +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) ────────────────────────────────────── + + +def test_channel_state_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].state.get() == 0 + + +def test_channel_amp_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].amp.get() == 0.5 + + +def test_channel_offset_get(awg: TektronixAWG5014) -> None: + assert awg.channels[0].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) ─────────────────────────────────────── + + +def test_marker_delay_get(awg: TektronixAWG5014) -> None: + ch1 = awg.channels[0] + assert ch1.m1.delay.get() == 0.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(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 ──────────────────────────────── + + +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_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 ──────────────────────────────────────────────────── + + +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) - package = awg._pack_waveform(waveform, m1, m2) + result = awg._pack_waveform(wf, m1, m2) - assert package is not None + assert result is not None + assert len(result) == N + assert result.dtype == np.uint16 -def test_make_awg_file(awg) -> None: - N = 25 +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])) - rng = np.random.default_rng() - waveforms = [[rng.random(N)]] + +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 ────────────────────────────────────────────────────── + + +def test_pack_short(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: + 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 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) + assert result["clock_freq"] == clock + assert result["numpoints"] == 3 + + +def test_file_dict_none_clock(awg: TektronixAWG5014) -> 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 ───────────────────────────────────────── + + +def test_valid_1m1() -> None: + result = TektronixAWG5014.parse_marker_channel_name("1M1") + assert result.channel == 1 + 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_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 ───────────────────────────────────────────────────── + + +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)]] - nreps = [1] - trig_waits = [0] - goto_states = [0] - jump_tos = [0] awgfile = awg.make_awg_file( waveforms, m1s, m2s, - nreps, - trig_waits, - goto_states, - jump_tos, + [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 ────────────────────────────────────────────── + + +def test_returns_dict(awg: TektronixAWG5014) -> None: + cfg = awg.generate_channel_cfg() + assert isinstance(cfg, dict) + + +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 + + +# ── generate_sequence_cfg ───────────────────────────────────────────── + + +def test_generate_sequence_cfg(awg: TektronixAWG5014) -> 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 + + +# ── Legacy attribute backward compatibility ─────────────────────────── + + +"""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}" + + +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}"].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(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