Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/Add all MW channel properties #72

Merged
merged 11 commits into from
Sep 4, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
### Added
- Added `DragCosinePulse`.
- Added support for sticky channels through the `StickyChannelAddon` (see documentation)
- Added `Channel.thread`, which defaults to None

### Changed
- Added ports for different hardware. As a consequence we now also support the LF-FEM and MW-FEM
- `Channel` is now an abstract base class.
- Moved `intermediate_frequency` to `Channel` from `SingleChannel/IQChannel`.
The default is `None`. A consequence of this is that `SingleChannel` no longer adds
`intermediate_frequency` to the config if it's not set.


## [0.3.4]
Expand Down
151 changes: 105 additions & 46 deletions quam/components/channels.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from abc import ABC
from dataclasses import field
from typing import ClassVar, Dict, List, Optional, Sequence, Literal, Tuple, Union, Any
import warnings
Expand Down Expand Up @@ -200,7 +201,7 @@ def apply_to_config(self, config: dict) -> None:


@quam_dataclass
class Channel(QuamComponent):
class Channel(QuamComponent, ABC):
"""Base QuAM component for a channel, can be output, input or both.

Args:
Expand All @@ -222,6 +223,9 @@ class Channel(QuamComponent):
digital_outputs: Dict[str, DigitalOutputChannel] = field(default_factory=dict)
sticky: Optional[StickyChannelAddon] = None

intermediate_frequency: Optional[float] = None
thread: Optional[str] = None

@property
def name(self) -> str:
cls_name = self.__class__.__name__
Expand Down Expand Up @@ -481,12 +485,12 @@ def _config_add_digital_outputs(self, config: Dict[str, dict]) -> None:
if not self.digital_outputs:
return

element_cfg = config["elements"][self.name]
element_cfg.setdefault("digitalInputs", {})
element_config = config["elements"][self.name]
element_config.setdefault("digitalInputs", {})

for name, digital_output in self.digital_outputs.items():
digital_cfg = digital_output.generate_element_config()
element_cfg["digitalInputs"][name] = digital_cfg
element_config["digitalInputs"][name] = digital_cfg

def apply_to_config(self, config: Dict[str, dict]) -> None:
"""Adds this Channel to the QUA configuration.
Expand All @@ -505,6 +509,13 @@ def apply_to_config(self, config: Dict[str, dict]) -> None:
f"exists. Existing entry: {config['elements'][self.name]}"
)
config["elements"][self.name] = {"operations": self.pulse_mapping}
element_config = config["elements"][self.name]

if self.intermediate_frequency is not None:
element_config["intermediate_frequency"] = self.intermediate_frequency

if self.thread is not None:
element_config["thread"] = self.thread

self._config_add_digital_outputs(config)

Expand Down Expand Up @@ -533,7 +544,6 @@ class SingleChannel(Channel):
filter_iir_taps: List[float] = None

opx_output_offset: float = None
intermediate_frequency: float = None

def set_dc_offset(self, offset: QuaNumberType):
"""Set the DC offset of an element's input to the given value.
Expand Down Expand Up @@ -566,9 +576,6 @@ def apply_to_config(self, config: dict):

element_config = config["elements"][self.name]

if self.intermediate_frequency is not None:
element_config["intermediate_frequency"] = self.intermediate_frequency

filter_fir_taps = self.filter_fir_taps
if filter_fir_taps is not None:
filter_fir_taps = list(filter_fir_taps)
Expand Down Expand Up @@ -908,7 +915,6 @@ class IQChannel(Channel):

frequency_converter_up: BaseFrequencyConverter

intermediate_frequency: float = 0.0
LO_frequency: float = "#./frequency_converter_up/LO_frequency"
RF_frequency: float = "#./inferred_RF_frequency"

Expand Down Expand Up @@ -1032,8 +1038,7 @@ def apply_to_config(self, config: dict):
" with a name."
)

element_cfg = config["elements"][self.name]
element_cfg["intermediate_frequency"] = self.intermediate_frequency
element_config = config["elements"][self.name]

from quam.components.octave import OctaveUpConverter

Expand All @@ -1045,7 +1050,7 @@ def apply_to_config(self, config: dict):
f"OctaveUpConverter (id={self.frequency_converter_up.id}) without "
"an attached Octave"
)
element_cfg["RF_inputs"] = {
element_config["RF_inputs"] = {
"port": (octave.name, self.frequency_converter_up.id)
}
elif str_ref.is_reference(self.frequency_converter_up):
Expand All @@ -1056,11 +1061,11 @@ def apply_to_config(self, config: dict):
)
else:

element_cfg["mixInputs"] = {} # To be filled in next section
element_config["mixInputs"] = {} # To be filled in next section
if self.mixer is not None:
element_cfg["mixInputs"]["mixer"] = self.mixer.name
element_config["mixInputs"]["mixer"] = self.mixer.name
if self.local_oscillator is not None:
element_cfg["mixInputs"][
element_config["mixInputs"][
"lo_frequency"
] = self.local_oscillator.frequency

Expand All @@ -1076,32 +1081,33 @@ def apply_to_config(self, config: dict):
opx_port = LFFEMAnalogOutputPort(*opx_output, offset=offset)
opx_port.apply_to_config(config)

if "mixInputs" in element_cfg:
element_cfg["mixInputs"][I_or_Q] = opx_port.port_tuple
if "mixInputs" in element_config:
element_config["mixInputs"][I_or_Q] = opx_port.port_tuple


@quam_dataclass
class InIQChannel(Channel):
"""QuAM component for an IQ input channel

operations (Dict[str, Pulse]): A dictionary of pulses to be played on this
channel. The key is the pulse label (e.g. "readout") and value is a
ReadoutPulse.
id (str, int): The id of the channel, used to generate the name.
Can be a string, or an integer in which case it will add
`Channel._default_label`.
opx_input_I (Tuple[str, int]): Channel I input port from the OPX perspective,
a tuple of (controller_name, port).
opx_input_Q (Tuple[str, int]): Channel Q input port from the OPX perspective,
a tuple of (controller_name, port).
opx_input_offset_I float: The offset of the I channel. Default is 0.
opx_input_offset_Q float: The offset of the Q channel. Default is 0.
frequency_converter_down (Optional[FrequencyConverter]): Frequency converter
QuAM component for the IQ input port. Only needed for the old Octave.
time_of_flight (int): Round-trip signal duration in nanoseconds.
smearing (int): Additional window of ADC integration in nanoseconds.
Used to account for signal smearing.
input_gain (float): The gain of the input channel. Default is None.
Args:
operations (Dict[str, Pulse]): A dictionary of pulses to be played on this
channel. The key is the pulse label (e.g. "readout") and value is a
ReadoutPulse.
id (str, int): The id of the channel, used to generate the name.
Can be a string, or an integer in which case it will add
`Channel._default_label`.
opx_input_I (Tuple[str, int]): Channel I input port from the OPX perspective,
a tuple of (controller_name, port).
opx_input_Q (Tuple[str, int]): Channel Q input port from the OPX perspective,
a tuple of (controller_name, port).
opx_input_offset_I float: The offset of the I channel. Default is 0.
opx_input_offset_Q float: The offset of the Q channel. Default is 0.
frequency_converter_down (Optional[FrequencyConverter]): Frequency converter
QuAM component for the IQ input port. Only needed for the old Octave.
time_of_flight (int): Round-trip signal duration in nanoseconds.
smearing (int): Additional window of ADC integration in nanoseconds.
Used to account for signal smearing.
input_gain (float): The gain of the input channel. Default is None.
"""

opx_input_I: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogInputPort]
Expand All @@ -1128,9 +1134,9 @@ def apply_to_config(self, config: dict):
super().apply_to_config(config)

# Note outputs instead of inputs because it's w.r.t. the QPU
element_cfg = config["elements"][self.name]
element_cfg["smearing"] = self.smearing
element_cfg["time_of_flight"] = self.time_of_flight
element_config = config["elements"][self.name]
element_config["smearing"] = self.smearing
element_config["time_of_flight"] = self.time_of_flight

from quam.components.octave import OctaveDownConverter

Expand All @@ -1142,7 +1148,7 @@ def apply_to_config(self, config: dict):
f"OctaveDownConverter (id={self.frequency_converter_down.id}) "
"without an attached Octave"
)
element_cfg["RF_outputs"] = {
element_config["RF_outputs"] = {
"port": (octave.name, self.frequency_converter_down.id)
}
elif str_ref.is_reference(self.frequency_converter_down):
Expand All @@ -1153,7 +1159,7 @@ def apply_to_config(self, config: dict):
)
else:
# To be filled in next section
element_cfg["outputs"] = {}
element_config["outputs"] = {}

opx_inputs = [self.opx_input_I, self.opx_input_Q]
offsets = [self.opx_input_offset_I, self.opx_input_offset_Q]
Expand All @@ -1172,7 +1178,7 @@ def apply_to_config(self, config: dict):
)
opx_port.apply_to_config(config)
if not isinstance(self.frequency_converter_down, OctaveDownConverter):
element_cfg["outputs"][f"out{k}"] = opx_port.port_tuple
element_config["outputs"][f"out{k}"] = opx_port.port_tuple

def measure(
self,
Expand Down Expand Up @@ -1552,28 +1558,81 @@ class InIQOutSingleChannel(SingleChannel, InIQChannel):

@quam_dataclass
class MWChannel(Channel):
"""QuAM component for a MW FEM output channel

Args:
operations (Dict[str, Pulse]): A dictionary of pulses to be played on this
channel. The key is the pulse label (e.g. "X90") and value is a Pulse.
id (str, int): The id of the channel, used to generate the name.
Can be a string, or an integer in which case it will add
`Channel._default_label`.
opx_output (MWFEMAnalogOutputPort): Channel output port from the OPX perspective.
intermediate_frequency (float): Intermediate frequency of OPX output, default
is None.
upconverter (int): The upconverter to use. Default is 1.
time_of_flight (int): Round-trip signal duration in nanoseconds.
smearing (int): Additional window of ADC integration in nanoseconds.
Used to account for signal smearing.
"""

opx_output: MWFEMAnalogOutputPort
upconverter: int = 1

def apply_to_config(self, config: Dict) -> None:
super().apply_to_config(config)

element_cfg = config["elements"][self.name]
element_cfg["MWInput"] = self.opx_output.port_tuple
element_cfg["upconverter"] = self.upconverter
element_config = config["elements"][self.name]
element_config["MWInput"] = self.opx_output.port_tuple
element_config["upconverter"] = self.upconverter


@quam_dataclass
class InMWChannel(Channel):
"""QuAM component for a MW FEM input channel

Args:
operations (Dict[str, Pulse]): A dictionary of pulses to be played on this
channel. The key is the pulse label (e.g. "X90") and value is a Pulse.
id (str, int): The id of the channel, used to generate the name.
Can be a string, or an integer in which case it will add
`Channel._default_label`.
opx_input (MWFEMAnalogInputPort)): Channel input port from the OPX perspective.
intermediate_frequency (float): Intermediate frequency of OPX output, default
is None.
"""

opx_input: MWFEMAnalogInputPort

time_of_flight: int = 24
smearing: int = 0

def apply_to_config(self, config: Dict) -> None:
super().apply_to_config(config)

element_cfg = config["elements"][self.name]
element_cfg["MWOutput"] = self.opx_input.port_tuple
element_config = config["elements"][self.name]
element_config["MWOutput"] = self.opx_input.port_tuple
element_config["smearing"] = self.smearing
element_config["time_of_flight"] = self.time_of_flight


@quam_dataclass
class InOutMWChannel(MWChannel, InMWChannel):
"""QuAM component for a MW FEM input channel

Args:
operations (Dict[str, Pulse]): A dictionary of pulses to be played on this
channel. The key is the pulse label (e.g. "X90") and value is a Pulse.
id (str, int): The id of the channel, used to generate the name.
Can be a string, or an integer in which case it will add
`Channel._default_label`.
opx_output (MWFEMAnalogOutputPort): Channel output port from the OPX perspective.
opx_input (MWFEMAnalogInputPort)): Channel input port from the OPX perspective.
intermediate_frequency (float): Intermediate frequency of OPX output, default
is None.
upconverter (int): The upconverter to use. Default is 1.
time_of_flight (int): Round-trip signal duration in nanoseconds.
smearing (int): Additional window of ADC integration in nanoseconds.
Used to account for signal smearing.
"""

pass
4 changes: 1 addition & 3 deletions tests/components/channels/test_IQ_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_IQ_channel_inferred_RF_frequency():
frequency_converter_up=None,
)

assert channel.intermediate_frequency == 0.0
assert channel.intermediate_frequency is None
assert channel.LO_frequency == "#./frequency_converter_up/LO_frequency"
assert channel.RF_frequency == "#./inferred_RF_frequency"
with pytest.raises(AttributeError):
Expand Down Expand Up @@ -112,7 +112,6 @@ def test_generate_config(qua_config):

assert qua_config["elements"] == {
"out_channel": {
"intermediate_frequency": 0.0,
"mixInputs": {
"I": ("con1", 1),
"Q": ("con1", 2),
Expand Down Expand Up @@ -145,7 +144,6 @@ def test_generate_config_ports(qua_config):

assert qua_config["elements"] == {
"out_channel": {
"intermediate_frequency": 0.0,
"mixInputs": {
"I": ("con1", 1),
"Q": ("con1", 2),
Expand Down
1 change: 1 addition & 0 deletions tests/components/channels/test_in_IQ_out_single_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def test_in_single_channel_attr_annotations():
"opx_input_offset_I",
"opx_input_offset_Q",
"frequency_converter_down",
"thread",
}


Expand Down
2 changes: 2 additions & 0 deletions tests/components/channels/test_in_single_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ def test_in_single_channel_attr_annotations():
attr_annotations = get_dataclass_attr_annotations(InSingleChannel)
assert set(attr_annotations["required"]) == {"opx_input"}
assert set(attr_annotations["optional"]) == {
"intermediate_frequency",
"operations",
"sticky",
"id",
"digital_outputs",
"opx_input_offset",
"time_of_flight",
"smearing",
"thread",
}


Expand Down
4 changes: 2 additions & 2 deletions tests/components/channels/test_in_single_out_IQ_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def test_in_single_channel_attr_annotations():
"opx_input_offset",
"time_of_flight",
"smearing",
"thread"
}


Expand Down Expand Up @@ -85,7 +86,7 @@ def test_generate_config(qua_config):
"analog_inputs": {
1: {"gain_db": 0, "shareable": False},
},
"analog_outputs": {
"analog_outputs": {
1: {"delay": 0, "shareable": False},
2: {"delay": 0, "shareable": False},
},
Expand All @@ -94,7 +95,6 @@ def test_generate_config(qua_config):

assert qua_config["elements"] == {
"in_out_channel": {
"intermediate_frequency": 0.0,
"mixInputs": {
"I": ("con1", 1),
"Q": ("con1", 2),
Expand Down
2 changes: 0 additions & 2 deletions tests/components/test_octave.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,6 @@ def test_channel_add_RF_outputs(octave, qua_config):

expected_cfg_elements = {
"ch": {
"intermediate_frequency": 0.0,
"RF_inputs": {"port": ("octave1", 2)},
"operations": {},
}
Expand Down Expand Up @@ -308,7 +307,6 @@ def test_channel_add_RF_inputs(octave, qua_config):

expected_cfg_elements = {
"ch": {
"intermediate_frequency": 0.0,
"RF_inputs": {"port": ("octave1", 3)},
"RF_outputs": {"port": ("octave1", 4)},
"operations": {},
Expand Down