From 8bf53d9024da5024f2333b1483f4a10f20c77e32 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 6 Mar 2024 12:43:55 +0100 Subject: [PATCH 01/13] adding new octave --- quam/components/octave.py | 161 +++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 3 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index af0cefd1..bec99dad 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -1,10 +1,11 @@ +from abc import ABC import os -from typing import Union, ClassVar, Dict +from typing import Any, Optional, Union, ClassVar, Dict, List, Tuple, Literal from dataclasses import field from quam.core import QuamComponent, quam_dataclass from quam.components.hardware import FrequencyConverter -from quam.components.channels import InOutIQChannel +from quam.components.channels import Channel, InOutIQChannel from qm import QuantumMachinesManager from qm import QuantumMachine @@ -14,7 +15,161 @@ from qm.octave import QmOctaveConfig, RFOutputMode, ClockType -__all__ = ["OctaveOldFrequencyConverter", "OctaveOld"] +__all__ = [ + "Octave", + "OctaveUpConverter", + "OctaveDownConverter", + "OctaveOldFrequencyConverter", + "OctaveOld", +] + + +@quam_dataclass +class Octave(QuamComponent): + name: str + RF_outputs: Dict[int, "OctaveUpConverter"] = field(default_factory=dict) + IF_outputs: Dict[int, "OctaveDownConverter"] = field(default_factory=dict) + loopbacks: List[Tuple[Tuple[str, str], str]] = field(default_factory=list) + + def initialize_default_connectivity(self): + if self.RF_outputs: + raise ValueError( + "Error initializing Octave with default connectivity. " + "octave.RF_outputs is not empty" + ) + if self.IF_outputs: + raise ValueError( + "Error initializing Octave with default connectivity. " + "octave.IF_outputs is not empty" + ) + + for i in range(1, 6): + self.RF_outputs[i] = OctaveUpConverter(octave=self) + self.IF_outputs[i] = OctaveDownConverter(octave=self, id=i) + + def apply_to_config(self, config: Dict) -> None: + if "octaves" not in config: + config["octaves"] = {} + if self.name in config["octaves"]: + raise KeyError( + f'Error generating config: config["octaves"] already contains an entry ' + f' for Octave "{self.name}"' + ) + + config["octaves"][self.name] = { + "RF_outputs": {}, + "IF_outputs": {}, + "loopbacks": self.loopbacks, + } + + +@quam_dataclass +class OctaveConverter(FrequencyConverter, ABC): + channel: Channel + port_I: Tuple[str, int] + port_Q: Tuple[str, int] + id: Union[int, str] = None + + @property + def octave(self) -> Optional[Octave]: + if self.parent is None: + return None + parent_parent = getattr(self.parent, "parent") + if not isinstance(parent_parent, Octave): + return None + return self.parent + + @property + def name(self) -> Union[str, int]: + if self.id is not None: + return self.id + + if self.octave is None: + raise ValueError( + "Could not determine name for OctaveDownConverter. " + "Either specify id or add to an Octave." + ) + return self.octave.get_attr_name(self) + + @property + def config_settings(self) -> Dict[str, Any]: + return {"after": [self.octave]} + + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) # TODO is this necessary? + + if "octaves" not in config: + raise KeyError('Error generating config: "octaves" entry not found') + + if self.octave.name not in config["octaves"]: + raise KeyError( + 'Error generating config: config["octaves"] does not have Octave' + f' entry config["octaves"]["{self.octave.name}"]' + ) + octave_config = config["octaves"][self.octave.name] + + if self.name in octave_config: + raise KeyError( + f'Error generating config: config["octaves"]["{self.octave.name}"] ' + f'already has an entry for OctaveUpConverter "{self.name}"' + ) + + octave_config[self.name] = { + "I_connection": self.port_I, + "Q_connection": self.port_Q, + } + + +@quam_dataclass +class OctaveUpConverter(OctaveConverter): + gain: float = 0 # range [-20:0.5:20] + LO_source: Literal["internal", "external"] = "internal" + LO_frequency: float # Between 2 and 18 GHz + output_mode: Literal[ + "always_on", "always_off", "triggered", "triggered_reersed" + ] = "always_on" + input_attenuators: Literal["off", "on"] = "off" + + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) + + config["octaves"][self.octave.name]["RF_outputs"][self.name] = { + "I_connection": self.port_I, + "Q_connection": self.port_Q, + "LO_frequency": self.LO_frequency, + "LO_source": self.LO_source, + "gain": self.gain, + "output_mode": self.output_mode, + "input_attenuators": self.input_attenuators, + } + + +@quam_dataclass +class OctaveDownConverter(FrequencyConverter): + RF_source: str = "RF_in" + LO_frequency: float # Between 2 and 18 GHz + LO_source: str = "internal" # default is internal for LO 1, external for LO 2 + IF_mode_I: str = "direct" # direct / envelope / mixer / off + IF_mode_Q: str = "direct" # direct / envelope / mixer / off + + @property + def config_settings(self): + return {"after": self.octave} + + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) + + config["octaves"][self.octave.name]["IF_outputs"][self.name] = { + "RF_source": self.RF_source, + "LO_frequency": self.LO_frequency, + "LO_source": self.LO_source, + "IF_mode_I": self.IF_mode_I, + "IF_mode_Q": self.IF_mode_Q, + "IF_outputs": { + "IF_out1": {"port": self.port_I, "name": "out1"}, # + "IF_out2": {"port": self.port_Q, "name": "out2"}, + }, + } @quam_dataclass From 335b3208754ad34ba28f226c3c335cd274018ec8 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 6 Mar 2024 16:50:31 +0100 Subject: [PATCH 02/13] octave updates --- quam/components/hardware.py | 3 +-- quam/components/octave.py | 23 +++++++++++------- tests/components/test_octave.py | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 tests/components/test_octave.py diff --git a/quam/components/hardware.py b/quam/components/hardware.py index ca416e6d..ee3a93c1 100644 --- a/quam/components/hardware.py +++ b/quam/components/hardware.py @@ -26,8 +26,7 @@ class LocalOscillator(QuamComponent): frequency: float = None power: float = None - def configure(self): - ... + def configure(self): ... @quam_dataclass diff --git a/quam/components/octave.py b/quam/components/octave.py index bec99dad..1d30f5e6 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -44,8 +44,18 @@ def initialize_default_connectivity(self): ) for i in range(1, 6): - self.RF_outputs[i] = OctaveUpConverter(octave=self) - self.IF_outputs[i] = OctaveDownConverter(octave=self, id=i) + self.RF_outputs[i] = OctaveUpConverter( + octave=self, + port_I=("con1", 2 * i - 1), + port_Q=("con1", 2 * i), + LO_frequency=None, # TODO What should default be? + ) + + for i in range(1, 3): + self.IF_outputs[i] = OctaveDownConverter( + octave=self, + + ) def apply_to_config(self, config: Dict) -> None: if "octaves" not in config: @@ -59,15 +69,16 @@ def apply_to_config(self, config: Dict) -> None: config["octaves"][self.name] = { "RF_outputs": {}, "IF_outputs": {}, + "RF_inputs": {}, "loopbacks": self.loopbacks, } @quam_dataclass class OctaveConverter(FrequencyConverter, ABC): - channel: Channel port_I: Tuple[str, int] port_Q: Tuple[str, int] + channel: Channel = None id: Union[int, str] = None @property @@ -159,16 +170,12 @@ def config_settings(self): def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) - config["octaves"][self.octave.name]["IF_outputs"][self.name] = { + config["octaves"][self.octave.name]["RF_outputs"][self.name] = { "RF_source": self.RF_source, "LO_frequency": self.LO_frequency, "LO_source": self.LO_source, "IF_mode_I": self.IF_mode_I, "IF_mode_Q": self.IF_mode_Q, - "IF_outputs": { - "IF_out1": {"port": self.port_I, "name": "out1"}, # - "IF_out2": {"port": self.port_Q, "name": "out2"}, - }, } diff --git a/tests/components/test_octave.py b/tests/components/test_octave.py new file mode 100644 index 00000000..06723e91 --- /dev/null +++ b/tests/components/test_octave.py @@ -0,0 +1,41 @@ +from copy import deepcopy + +from quam.components.octave import Octave, OctaveUpConverter, OctaveDownConverter +from quam.core.qua_config_template import qua_config_template +from quam.core.quam_classes import QuamRoot, quam_dataclass + + +@quam_dataclass +class OctaveQuAM(QuamRoot): + octave: Octave + + +def test_instantiate_octave(): + octave = Octave(name="octave1") + assert octave.RF_outputs == {} + assert octave.IF_outputs == {} + assert octave.loopbacks == [] + + +def test_instantiate_octave_default_connectivity(): + octave = Octave() + octave.initialize_default_connectivity() + + assert list(octave.RF_outputs) == [1, 2, 3, 4, 5] + + +def test_empty_octave_config(): + octave = Octave(name="octave1") + machine = OctaveQuAM(octave=octave) + config = machine.generate_config() + + expected_cfg = deepcopy(qua_config_template) + expected_cfg["octaves"] = { + "octave1": { + "RF_outputs": {}, + "IF_outputs": {}, + "loopbacks": [], + } + } + + assert config == expected_cfg From c5147e15627c6c8db1736be6a95e63b146cce3aa Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 7 Mar 2024 09:33:34 +0100 Subject: [PATCH 03/13] fixes for frequency converter --- quam/components/octave.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index 1d30f5e6..5ac819e1 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -9,6 +9,7 @@ from qm import QuantumMachinesManager from qm import QuantumMachine +from qm.octave import QmOctaveConfig from qm.octave.qm_octave import QmOctave from octave_sdk import RFInputLOSource @@ -27,8 +28,11 @@ @quam_dataclass class Octave(QuamComponent): name: str + ip: str + port: int + RF_outputs: Dict[int, "OctaveUpConverter"] = field(default_factory=dict) - IF_outputs: Dict[int, "OctaveDownConverter"] = field(default_factory=dict) + RF_inputs: Dict[int, "OctaveDownConverter"] = field(default_factory=dict) loopbacks: List[Tuple[Tuple[str, str], str]] = field(default_factory=list) def initialize_default_connectivity(self): @@ -37,7 +41,7 @@ def initialize_default_connectivity(self): "Error initializing Octave with default connectivity. " "octave.RF_outputs is not empty" ) - if self.IF_outputs: + if self.RF_inputs: raise ValueError( "Error initializing Octave with default connectivity. " "octave.IF_outputs is not empty" @@ -52,11 +56,16 @@ def initialize_default_connectivity(self): ) for i in range(1, 3): - self.IF_outputs[i] = OctaveDownConverter( + self.RF_inputs[i] = OctaveDownConverter( octave=self, - ) + def get_octave_config(self) -> QmOctaveConfig: + """Return a QmOctaveConfig object with the current Octave configuration.""" + octave_config = QmOctaveConfig() + octave_config.add_device_info(self.name, self.ip, self.port) + return octave_config + def apply_to_config(self, config: Dict) -> None: if "octaves" not in config: config["octaves"] = {} @@ -117,19 +126,13 @@ def apply_to_config(self, config: Dict) -> None: 'Error generating config: config["octaves"] does not have Octave' f' entry config["octaves"]["{self.octave.name}"]' ) - octave_config = config["octaves"][self.octave.name] - if self.name in octave_config: + if self.name in config["octaves"][self.octave.name]: raise KeyError( f'Error generating config: config["octaves"]["{self.octave.name}"] ' f'already has an entry for OctaveUpConverter "{self.name}"' ) - octave_config[self.name] = { - "I_connection": self.port_I, - "Q_connection": self.port_Q, - } - @quam_dataclass class OctaveUpConverter(OctaveConverter): @@ -170,7 +173,7 @@ def config_settings(self): def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) - config["octaves"][self.octave.name]["RF_outputs"][self.name] = { + config["octaves"][self.octave.name]["RF_inputs"][self.name] = { "RF_source": self.RF_source, "LO_frequency": self.LO_frequency, "LO_source": self.LO_source, From 5364c4d9c96c694fd901d1615305b945d074529e Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 7 Mar 2024 14:06:28 +0100 Subject: [PATCH 04/13] Added support for IF outputs --- quam/components/octave.py | 52 ++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index 5ac819e1..75aa2c82 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -5,7 +5,13 @@ from quam.core import QuamComponent, quam_dataclass from quam.components.hardware import FrequencyConverter -from quam.components.channels import Channel, InOutIQChannel +from quam.components.channels import ( + Channel, + IQChannel, + InOutIQChannel, + InOutSingleChannel, + SingleChannel, +) from qm import QuantumMachinesManager from qm import QuantumMachine @@ -85,10 +91,8 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass class OctaveConverter(FrequencyConverter, ABC): - port_I: Tuple[str, int] - port_Q: Tuple[str, int] channel: Channel = None - id: Union[int, str] = None + id: int = None @property def octave(self) -> Optional[Octave]: @@ -147,24 +151,30 @@ class OctaveUpConverter(OctaveConverter): def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) - config["octaves"][self.octave.name]["RF_outputs"][self.name] = { - "I_connection": self.port_I, - "Q_connection": self.port_Q, + output_config = config["octaves"][self.octave.name]["RF_outputs"][self.name] = { "LO_frequency": self.LO_frequency, "LO_source": self.LO_source, "gain": self.gain, "output_mode": self.output_mode, "input_attenuators": self.input_attenuators, } + if isinstance(self.channel, SingleChannel): + output_config["I_connection"] = self.channel.opx_output + elif isinstance(self.channel, IQChannel): + output_config["I_connection"] = self.channel.opx_output_I + output_config["Q_connection"] = self.channel.opx_output_Q @quam_dataclass class OctaveDownConverter(FrequencyConverter): - RF_source: str = "RF_in" LO_frequency: float # Between 2 and 18 GHz - LO_source: str = "internal" # default is internal for LO 1, external for LO 2 - IF_mode_I: str = "direct" # direct / envelope / mixer / off - IF_mode_Q: str = "direct" # direct / envelope / mixer / off + LO_source: Literal["internal", "external"] = ( + "internal" # default is internal for LO 1, external for LO 2 + ) + IF_mode_I: Literal["direct", "envelope", "mixer", "off"] = "direct" + IF_mode_Q: Literal["direct", "envelope", "mixer", "off"] = "direct" + IF_output_I: Literal[1, 2] = 1 + IF_output_Q: Literal[1, 2] = 2 @property def config_settings(self): @@ -174,13 +184,31 @@ def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) config["octaves"][self.octave.name]["RF_inputs"][self.name] = { - "RF_source": self.RF_source, + "RF_source": "RF_in", "LO_frequency": self.LO_frequency, "LO_source": self.LO_source, "IF_mode_I": self.IF_mode_I, "IF_mode_Q": self.IF_mode_Q, } + if isinstance(self.channel, InOutIQChannel): + IF_channels = [self.IF_output_I, self.IF_output_Q] + opx_channels = [self.channel.opx_output_I, self.channel.opx_output_Q] + elif isinstance(self.channel, InOutSingleChannel): + IF_channels = [self.IF_output_I] + opx_channels = [self.channel.opx_output] + + IF_config = config["octaves"][self.octave.name]["IF_outputs"] + for k, (IF_ch, opx_ch) in enumerate(zip(IF_channels, opx_channels)): + label = f"IF_out{IF_ch}" + IF_config.setdefault(label, {"port": tuple(opx_ch), "name": f"out{k}"}) + if IF_config[label]["port"] != tuple(opx_ch): + raise ValueError( + f"Error generating config for Octave downconverter {self.name}: " + f"Unable to assign {label} to port {opx_ch} because it is already " + f"assigned to port {IF_config[label]['port']} " + ) + @quam_dataclass class OctaveOld(QuamComponent): From 3392658a24c92796c650948dd62b2d15d1415df5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 7 Mar 2024 14:07:53 +0100 Subject: [PATCH 05/13] moved octave_sdk import --- quam/components/octave.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index 75aa2c82..f58e8764 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -15,11 +15,8 @@ from qm import QuantumMachinesManager from qm import QuantumMachine -from qm.octave import QmOctaveConfig -from qm.octave.qm_octave import QmOctave - -from octave_sdk import RFInputLOSource from qm.octave import QmOctaveConfig, RFOutputMode, ClockType +from qm.octave.qm_octave import QmOctave __all__ = [ @@ -267,6 +264,8 @@ def get_portmap(self): return portmap def configure_octave_settings(self): + from octave_sdk import RFInputLOSource + self.octave.set_clock(self.name, ClockType.Internal) for qe in self._channel_to_qe.values(): self.octave.set_rf_output_mode(qe, RFOutputMode.on) From c7cdad29a817fa5372e861faa80aa59d5c2ab9bd Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 7 Mar 2024 15:27:16 +0100 Subject: [PATCH 06/13] Added tests --- quam/components/octave.py | 80 +++++----- quam/core/quam_classes.py | 20 ++- tests/components/test_octave.py | 250 ++++++++++++++++++++++++++++++-- 3 files changed, 295 insertions(+), 55 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index f58e8764..518cf1c5 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -13,8 +13,7 @@ SingleChannel, ) -from qm import QuantumMachinesManager -from qm import QuantumMachine +from qm import QuantumMachinesManager, QuantumMachine from qm.octave import QmOctaveConfig, RFOutputMode, ClockType from qm.octave.qm_octave import QmOctave @@ -50,18 +49,14 @@ def initialize_default_connectivity(self): "octave.IF_outputs is not empty" ) - for i in range(1, 6): - self.RF_outputs[i] = OctaveUpConverter( - octave=self, - port_I=("con1", 2 * i - 1), - port_Q=("con1", 2 * i), + for idx in range(1, 6): + self.RF_outputs[idx] = OctaveUpConverter( + id=idx, LO_frequency=None, # TODO What should default be? ) - for i in range(1, 3): - self.RF_inputs[i] = OctaveDownConverter( - octave=self, - ) + for idx in range(1, 3): + self.RF_inputs[idx] = OctaveDownConverter(id=idx, LO_frequency=None) def get_octave_config(self) -> QmOctaveConfig: """Return a QmOctaveConfig object with the current Octave configuration.""" @@ -87,9 +82,9 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass -class OctaveConverter(FrequencyConverter, ABC): +class OctaveFrequencyConverter(FrequencyConverter, ABC): + id: int channel: Channel = None - id: int = None @property def octave(self) -> Optional[Octave]: @@ -98,19 +93,7 @@ def octave(self) -> Optional[Octave]: parent_parent = getattr(self.parent, "parent") if not isinstance(parent_parent, Octave): return None - return self.parent - - @property - def name(self) -> Union[str, int]: - if self.id is not None: - return self.id - - if self.octave is None: - raise ValueError( - "Could not determine name for OctaveDownConverter. " - "Either specify id or add to an Octave." - ) - return self.octave.get_attr_name(self) + return parent_parent @property def config_settings(self) -> Dict[str, Any]: @@ -122,21 +105,21 @@ def apply_to_config(self, config: Dict) -> None: if "octaves" not in config: raise KeyError('Error generating config: "octaves" entry not found') - if self.octave.name not in config["octaves"]: + if self.octave is None: raise KeyError( - 'Error generating config: config["octaves"] does not have Octave' - f' entry config["octaves"]["{self.octave.name}"]' + f"Error generating config: OctaveConverter with id {self.id} does not " + "have an Octave parent" ) - if self.name in config["octaves"][self.octave.name]: + if self.octave.name not in config["octaves"]: raise KeyError( - f'Error generating config: config["octaves"]["{self.octave.name}"] ' - f'already has an entry for OctaveUpConverter "{self.name}"' + 'Error generating config: config["octaves"] does not have Octave' + f' entry config["octaves"]["{self.octave.name}"]' ) @quam_dataclass -class OctaveUpConverter(OctaveConverter): +class OctaveUpConverter(OctaveFrequencyConverter): gain: float = 0 # range [-20:0.5:20] LO_source: Literal["internal", "external"] = "internal" LO_frequency: float # Between 2 and 18 GHz @@ -148,7 +131,14 @@ class OctaveUpConverter(OctaveConverter): def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) - output_config = config["octaves"][self.octave.name]["RF_outputs"][self.name] = { + if self.id in config["octaves"][self.octave.name]["RF_outputs"]: + raise KeyError( + f"Error generating config: " + f'config["octaves"]["{self.octave.name}"]["RF_inputs"] ' + f'already has an entry for OctaveDownConverter with id "{self.id}"' + ) + + output_config = config["octaves"][self.octave.name]["RF_outputs"][self.id] = { "LO_frequency": self.LO_frequency, "LO_source": self.LO_source, "gain": self.gain, @@ -163,7 +153,7 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass -class OctaveDownConverter(FrequencyConverter): +class OctaveDownConverter(OctaveFrequencyConverter): LO_frequency: float # Between 2 and 18 GHz LO_source: Literal["internal", "external"] = ( "internal" # default is internal for LO 1, external for LO 2 @@ -180,7 +170,14 @@ def config_settings(self): def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) - config["octaves"][self.octave.name]["RF_inputs"][self.name] = { + if self.id in config["octaves"][self.octave.name]["RF_inputs"]: + raise KeyError( + f"Error generating config: " + f'config["octaves"]["{self.octave.name}"]["RF_inputs"] ' + f'already has an entry for OctaveDownConverter with id "{self.id}"' + ) + + config["octaves"][self.octave.name]["RF_inputs"][self.id] = { "RF_source": "RF_in", "LO_frequency": self.LO_frequency, "LO_source": self.LO_source, @@ -190,18 +187,21 @@ def apply_to_config(self, config: Dict) -> None: if isinstance(self.channel, InOutIQChannel): IF_channels = [self.IF_output_I, self.IF_output_Q] - opx_channels = [self.channel.opx_output_I, self.channel.opx_output_Q] + opx_channels = [self.channel.opx_input_I, self.channel.opx_input_Q] elif isinstance(self.channel, InOutSingleChannel): IF_channels = [self.IF_output_I] - opx_channels = [self.channel.opx_output] + opx_channels = [self.channel.opx_input] + else: + IF_channels = [] + opx_channels = [] IF_config = config["octaves"][self.octave.name]["IF_outputs"] - for k, (IF_ch, opx_ch) in enumerate(zip(IF_channels, opx_channels)): + for k, (IF_ch, opx_ch) in enumerate(zip(IF_channels, opx_channels), start=1): label = f"IF_out{IF_ch}" IF_config.setdefault(label, {"port": tuple(opx_ch), "name": f"out{k}"}) if IF_config[label]["port"] != tuple(opx_ch): raise ValueError( - f"Error generating config for Octave downconverter {self.name}: " + f"Error generating config for Octave downconverter id={self.id}: " f"Unable to assign {label} to port {opx_ch} because it is already " f"assigned to port {IF_config[label]['port']} " ) diff --git a/quam/core/quam_classes.py b/quam/core/quam_classes.py index dbfc6910..23d90e2c 100644 --- a/quam/core/quam_classes.py +++ b/quam/core/quam_classes.py @@ -497,7 +497,11 @@ def _get_referenced_value(self, reference: str) -> Any: self, reference, root=self._root ) except ValueError as e: - warnings.warn(str(e)) + try: + ref = f"{self.__class__.__name__}: {self.get_reference()}" + except Exception: + ref = self.__class__.__name__ + warnings.warn(f"Could not get reference {reference} from {ref}.\n{str(e)}") return reference def print_summary(self, indent: int = 0): @@ -743,7 +747,11 @@ def __getattr__(self, key): try: return self[key] except KeyError as e: - raise AttributeError(key) from e + try: + repr = f"{self.__class__.__name__}: {self.get_reference()}" + except Exception: + repr = self.__class__.__name__ + raise AttributeError(f'{repr} has no attribute "{key}"') from e def __setattr__(self, key, value): if key in ["data", "parent", "config_settings", "_initialized"]: @@ -757,7 +765,13 @@ def __getitem__(self, i): try: elem = self._get_referenced_value(elem) except ValueError as e: - raise KeyError(str(e)) from e + try: + repr = f"{self.__class__.__name__}: {self.get_reference()}" + except Exception: + repr = self.__class__.__name__ + raise KeyError( + f"Could not get referenced value {elem} from {repr}" + ) from e return elem # Overriding methods from UserDict diff --git a/tests/components/test_octave.py b/tests/components/test_octave.py index 06723e91..a9827e1e 100644 --- a/tests/components/test_octave.py +++ b/tests/components/test_octave.py @@ -1,5 +1,8 @@ from copy import deepcopy +import pytest +from quam.components.channels import InOutIQChannel, InOutSingleChannel + from quam.components.octave import Octave, OctaveUpConverter, OctaveDownConverter from quam.core.qua_config_template import qua_config_template from quam.core.quam_classes import QuamRoot, quam_dataclass @@ -10,22 +13,21 @@ class OctaveQuAM(QuamRoot): octave: Octave -def test_instantiate_octave(): - octave = Octave(name="octave1") - assert octave.RF_outputs == {} - assert octave.IF_outputs == {} - assert octave.loopbacks == [] - +@pytest.fixture +def octave(): + return Octave(name="octave1", ip="127.0.0.1", port=80) -def test_instantiate_octave_default_connectivity(): - octave = Octave() - octave.initialize_default_connectivity() - assert list(octave.RF_outputs) == [1, 2, 3, 4, 5] +def test_instantiate_octave(octave): + assert octave.name == "octave1" + assert octave.ip == "127.0.0.1" + assert octave.port == 80 + assert octave.RF_outputs == {} + assert octave.RF_inputs == {} + assert octave.loopbacks == [] -def test_empty_octave_config(): - octave = Octave(name="octave1") +def test_empty_octave_config(octave): machine = OctaveQuAM(octave=octave) config = machine.generate_config() @@ -33,9 +35,233 @@ def test_empty_octave_config(): expected_cfg["octaves"] = { "octave1": { "RF_outputs": {}, + "RF_inputs": {}, "IF_outputs": {}, "loopbacks": [], } } assert config == expected_cfg + + +def test_empty_octave_empty_config(octave): + cfg = {} + octave.apply_to_config(config=cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "RF_outputs": {}, + "RF_inputs": {}, + "IF_outputs": {}, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg + + +def test_octave_config_conflicting_entry(octave): + machine = OctaveQuAM(octave=octave) + config = machine.generate_config() + + with pytest.raises(KeyError): + octave.apply_to_config(config) + + +def test_get_octave_config(octave): + octave_config = octave.get_octave_config() + assert list(octave_config.devices) == ["octave1"] + connection_details = octave_config.devices["octave1"] + assert connection_details.host == "127.0.0.1" + assert connection_details.port == 80 + + +def test_frequency_converter_no_octave(): + converter = OctaveUpConverter(id=1, LO_frequency=2e9) + assert converter.octave is None + + +def test_frequency_converter_octave(octave): + converter = octave.RF_outputs[1] = OctaveUpConverter(id=1, LO_frequency=2e9) + assert converter.octave is octave + + +def test_frequency_up_converter_apply_to_config(octave): + converter = octave.RF_outputs[1] = OctaveUpConverter(id=1, LO_frequency=2e9) + cfg = {} + octave.apply_to_config(config=cfg) + converter.apply_to_config(cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "RF_outputs": { + 1: { + "LO_frequency": 2e9, + "LO_source": "internal", + "gain": 0, + "output_mode": "always_on", + "input_attenuators": "off", + } + }, + "RF_inputs": {}, + "IF_outputs": {}, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg + + +def test_frequency_down_converter_apply_to_config(octave): + converter = octave.RF_inputs[1] = OctaveDownConverter(id=1, LO_frequency=2e9) + cfg = {} + octave.apply_to_config(config=cfg) + converter.apply_to_config(cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "RF_outputs": {}, + "RF_inputs": { + 1: { + "LO_frequency": 2e9, + "RF_source": "RF_in", + "LO_source": "internal", + "IF_mode_I": "direct", + "IF_mode_Q": "direct", + } + }, + "IF_outputs": {}, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg + + +def test_frequency_down_converter_with_IQchannel_apply_to_config(octave): + channel = InOutIQChannel( + opx_output_I=("con1", 3), + opx_output_Q=("con1", 4), + opx_input_I=("con1", 1), + opx_input_Q=("con1", 2), + frequency_converter_up=None, + frequency_converter_down=None, + ) + converter = octave.RF_inputs[1] = OctaveDownConverter( + id=1, LO_frequency=2e9, channel=channel + ) + cfg = {} + octave.apply_to_config(config=cfg) + converter.apply_to_config(cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "RF_outputs": {}, + "RF_inputs": { + 1: { + "LO_frequency": 2e9, + "RF_source": "RF_in", + "LO_source": "internal", + "IF_mode_I": "direct", + "IF_mode_Q": "direct", + } + }, + "IF_outputs": { + "IF_out1": {"port": ("con1", 1), "name": "out1"}, + "IF_out2": {"port": ("con1", 2), "name": "out2"}, + }, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg + + +def test_frequency_converter_down_existing_IF_outputs(octave): + channel = InOutIQChannel( + opx_output_I=("con1", 3), + opx_output_Q=("con1", 4), + opx_input_I=("con1", 2), + opx_input_Q=("con1", 1), + frequency_converter_up=None, + frequency_converter_down=None, + ) + converter = octave.RF_inputs[1] = OctaveDownConverter( + id=1, LO_frequency=2e9, channel=channel + ) + cfg = {} + octave.apply_to_config(config=cfg) + cfg["octaves"]["octave1"]["IF_outputs"] = { + "IF_out1": {"port": ("con1", 1), "name": "out1"}, + "IF_out2": {"port": ("con1", 2), "name": "out2"}, + } + + with pytest.raises(ValueError): + converter.apply_to_config(cfg) + + cfg = {} + octave.apply_to_config(config=cfg) + cfg["octaves"]["octave1"]["IF_outputs"] = { + "IF_out1": {"port": ("con1", 2), "name": "out1"}, + "IF_out2": {"port": ("con1", 1), "name": "out2"}, + } + + converter.apply_to_config(cfg) + + assert cfg["octaves"]["octave1"]["IF_outputs"] == { + "IF_out1": {"port": ("con1", 2), "name": "out1"}, + "IF_out2": {"port": ("con1", 1), "name": "out2"}, + } + + +def test_frequency_down_converter_with_single_channel_apply_to_config(octave): + channel = InOutSingleChannel( + opx_output=("con1", 3), + opx_input=("con1", 3), + ) + converter = octave.RF_inputs[1] = OctaveDownConverter( + id=1, LO_frequency=2e9, channel=channel, IF_output_I=2 + ) + cfg = {} + octave.apply_to_config(config=cfg) + converter.apply_to_config(cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "RF_outputs": {}, + "RF_inputs": { + 1: { + "LO_frequency": 2e9, + "RF_source": "RF_in", + "LO_source": "internal", + "IF_mode_I": "direct", + "IF_mode_Q": "direct", + } + }, + "IF_outputs": { + "IF_out2": {"port": ("con1", 3), "name": "out1"}, + }, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg + + +def test_instantiate_octave_default_connectivity(octave): + octave.initialize_default_connectivity() + + assert list(octave.RF_outputs) == [1, 2, 3, 4, 5] + for idx, RF_output in octave.RF_outputs.items(): + assert RF_output.octave == octave + assert RF_output.id == idx + + assert list(octave.RF_inputs) == [1, 2] + for idx, RF_input in octave.RF_inputs.items(): + assert RF_input.octave == octave + assert RF_input.id == idx From e04fa7fcad6bf3233cd7864b85a90e765c19e2f2 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 7 Mar 2024 19:35:45 +0100 Subject: [PATCH 07/13] Added docstrings --- quam/components/octave.py | 159 +++++++++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 4 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index 518cf1c5..b8a06476 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -29,6 +29,30 @@ @quam_dataclass class Octave(QuamComponent): + """QuAM component for the QM Octave. + + The QM Octave is a device that can be used to upconvert and downconvert signals. It + has 5 RF outputs and 2 RF inputs. Each RF_output has an associated + `OctaveUpConverter`, and similarly each RF_input has an `OctaveDownConverter`. + + In many cases the Octave is connected to a single OPX in the default configuration, + i.e. OPX outputs are connected to the corresponding Octave I/Q input, and Octave IF + outputs are connected to the corresponding OPX input. In this case you can configure + the Octave with the correct `FrequencyConverter`s using + `Octave.initialize_default_connectivity()`. + + Args: + name: The name of the Octave. Must be unique + ip: The IP address of the Octave. Used in `Octave.get_octave_config()` + port: The port number of the Octave. Used in `Octave.get_octave_config()` + RF_outputs: A dictionary of `OctaveUpConverter` objects. The keys are the + output numbers (1-5). + RF_inputs: A dictionary of `OctaveDownConverter` objects. The keys are the + input numbers (1-2). + loopbacks: A list of loopback connections, for example to connect a local + oscillator. See the QUA Octave documentation for details. + """ + name: str ip: str port: int @@ -38,6 +62,18 @@ class Octave(QuamComponent): loopbacks: List[Tuple[Tuple[str, str], str]] = field(default_factory=list) def initialize_default_connectivity(self): + """Initialize the Octave with default connectivity. + + This method initializes the Octave with default connectivity, i.e. it connects + the Octave to a single OPX. It creates an `OctaveUpConverter` for each RF output + and an `OctaveDownConverter` for each RF input. The `OctaveUpConverter` objects + are added to `Octave.RF_outputs` and the `OctaveDownConverter` objects are added + to `Octave.RF_inputs`. + + Raises: + ValueError: If the Octave already has RF_outputs or RF_inputs. + + """ if self.RF_outputs: raise ValueError( "Error initializing Octave with default connectivity. " @@ -65,6 +101,16 @@ def get_octave_config(self) -> QmOctaveConfig: return octave_config def apply_to_config(self, config: Dict) -> None: + """Add the Octave configuration to a config dictionary. + + This method is called by the `QuamComponent.generate_config` method. + + Args: + config: A dictionary representing a QUA config file. + + Raises: + KeyError: If the Octave is already in the config. + """ if "octaves" not in config: config["octaves"] = {} if self.name in config["octaves"]: @@ -83,6 +129,15 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass class OctaveFrequencyConverter(FrequencyConverter, ABC): + """Base class for OctaveUpConverter and OctaveDownConverter. + + Args: + id: The id of the converter. Must be unique within the Octave. + For OctaveUpConverter, the id is used as the RF output number. + For OctaveDownConverter, the id is used as the RF input number. + channel: The channel that the converter is connected to. + """ + id: int channel: Channel = None @@ -97,10 +152,22 @@ def octave(self) -> Optional[Octave]: @property def config_settings(self) -> Dict[str, Any]: + """Specifies that the converter will be added to the config after the Octave.""" return {"after": [self.octave]} def apply_to_config(self, config: Dict) -> None: - super().apply_to_config(config) # TODO is this necessary? + """Add information about the frequency converter to the QUA config + + This method is called by the `QuamComponent.generate_config` method. + + Args: + config: A dictionary representing a QUA config file. + + Raises: + KeyError: If the Octave is not in the config, or if config["octaves"] does + not exist. + """ + super().apply_to_config(config) if "octaves" not in config: raise KeyError('Error generating config: "octaves" entry not found') @@ -120,15 +187,54 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass class OctaveUpConverter(OctaveFrequencyConverter): - gain: float = 0 # range [-20:0.5:20] + """A frequency upconverter for the QM Octave. + + The OctaveUpConverter represents a frequency upconverter in the QM Octave. Usually + an IQChannel is connected `OctaveUpconverter.channel`, in which case the two OPX + outputs are connected to the I and Q inputs of the OctaveUpConverter. + The OPX outputs are specified in the `OctaveUpConverter.channel` attribute. + The channel is either an IQChannel or a SingleChannel. + + Args: + id: The RF output id, must be between 1-5. + LO_frequency: The local oscillator frequency in Hz, between 2 and 18 GHz. + LO_source: The local oscillator source, "internal" (default) or "external". + gain: The gain of the output, between -20 and 20 dB in steps of 0.5. + Default is 0 dB. + output_mode: Sets the fast switch's mode of the up converter module. + Can be "always_on" / "always_off" / "triggered" / "triggered_reversed". + The default is "always_off". + - "always_on" - Output is always on + - "always_off" - Output is always off + - "triggered" - The output will play when rising edge is detected in the + octave's digital port. + - "triggered_reversed" - The output will play when falling edge is detected + in the octave's digital port. + input_attenuators: Whether the I and Q ports have a 10 dB attenuator before + entering the mixer. Off by default. + """ + + LO_frequency: float LO_source: Literal["internal", "external"] = "internal" - LO_frequency: float # Between 2 and 18 GHz + gain: float = 0 output_mode: Literal[ "always_on", "always_off", "triggered", "triggered_reersed" - ] = "always_on" + ] = "always_off" input_attenuators: Literal["off", "on"] = "off" def apply_to_config(self, config: Dict) -> None: + """Add information about the frequency up-converter to the QUA config + + This method is called by the `QuamComponent.generate_config` method. + + Args: + config: A dictionary representing a QUA config file. + + Raises: + KeyError: If the Octave is not in the config, or if config["octaves"] does + not exist. + KeyError: If the Octave already has an entry for the OctaveUpConverter. + """ super().apply_to_config(config) if self.id in config["octaves"][self.octave.name]["RF_outputs"]: @@ -154,6 +260,36 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass class OctaveDownConverter(OctaveFrequencyConverter): + """A frequency downconverter for the QM Octave. + + The OctaveDownConverter represents a frequency downconverter in the QM Octave. The + OctaveDownConverter is usually connected to an InOutIQChannel, in which case the + two OPX inputs are connected to the IF outputs of the OctaveDownConverter. The + OPX inputs are specified in the `OctaveDownConverter.channel` attribute. The + channel is either an InOutIQChannel or an InOutSingleChannel. + + Args: + id: The RF input id, must be between 1-2. + LO_frequency: The local oscillator frequency in Hz, between 2 and 18 GHz. + LO_source: The local oscillator source, "internal" or "external. + For down converter 1 "internal" is the default, + for down converter 2 "external" is the default. + IF_mode_I: Sets the mode of the I port of the IF Down Converter module as can be + seen in the octave block diagram (see Octave page in QUA documentation). + Can be "direct" / "envelope" / "mixer" / "off". The default is "direct". + - "direct" - The signal bypasses the IF module. + - "envelope" - The signal passes through an envelope detector. + - "mixer" - The signal passes through a low-frequency mixer. + - "off" - the signal doesn't pass to the output port. + IF_mode_Q: Sets the mode of the Q port of the IF Down Converter module. + IF_output_I: The output port of the IF Down Converter module for the I port. + Can be 1 or 2. The default is 1. This will be 2 if the IF outputs + are connected to the opposite OPX inputs + IF_output_Q: The output port of the IF Down Converter module for the Q port. + Can be 1 or 2. The default is 2. This will be 1 if the IF outputs + are connected to the opposite OPX inputs. + """ + LO_frequency: float # Between 2 and 18 GHz LO_source: Literal["internal", "external"] = ( "internal" # default is internal for LO 1, external for LO 2 @@ -165,9 +301,24 @@ class OctaveDownConverter(OctaveFrequencyConverter): @property def config_settings(self): + """Specifies that the converter will be added to the config after the Octave.""" return {"after": self.octave} def apply_to_config(self, config: Dict) -> None: + """Add information about the frequency down-converter to the QUA config + + This method is called by the `QuamComponent.generate_config` method. + + Args: + config: A dictionary representing a QUA config file. + + Raises: + KeyError: If the Octave is not in the config, or if config["octaves"] does + not exist. + KeyError: If the Octave already has an entry for the OctaveDownConverter. + ValueError: If the IF_output_I and IF_output_Q are already assigned to + other ports. + """ super().apply_to_config(config) if self.id in config["octaves"][self.octave.name]["RF_inputs"]: From c34b40a37bf81bdc8cc30510fcf0c98b1b1b5091 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 7 Mar 2024 19:38:58 +0100 Subject: [PATCH 08/13] fix broken test --- tests/components/test_octave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/test_octave.py b/tests/components/test_octave.py index a9827e1e..1b348492 100644 --- a/tests/components/test_octave.py +++ b/tests/components/test_octave.py @@ -101,7 +101,7 @@ def test_frequency_up_converter_apply_to_config(octave): "LO_frequency": 2e9, "LO_source": "internal", "gain": 0, - "output_mode": "always_on", + "output_mode": "always_off", "input_attenuators": "off", } }, From 9f95a046e55bf85537c077c2b4f40b22ef2e89e6 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 8 Mar 2024 08:37:00 +0100 Subject: [PATCH 09/13] Add element items --- quam/components/channels.py | 72 ++++++++++++++++++++++++++------- tests/components/test_octave.py | 59 ++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index e1489d58..21bd097a 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -1,7 +1,7 @@ from dataclasses import field from typing import ClassVar, Dict, List, Optional, Tuple, Union -from quam.components.hardware import FrequencyConverter +from quam.components.hardware import FrequencyConverter, Mixer, LocalOscillator from quam.components.pulses import Pulse, ReadoutPulse from quam.core import QuamComponent, quam_dataclass from quam.utils import string_reference as str_ref @@ -534,12 +534,12 @@ class IQChannel(Channel): _default_label: ClassVar[str] = "IQ" @property - def local_oscillator(self): - return self.frequency_converter_up.local_oscillator + def local_oscillator(self) -> Optional[LocalOscillator]: + return getattr(self.frequency_converter_up, "local_oscillator", None) @property - def mixer(self): - return self.frequency_converter_up.mixer + def mixer(self) -> Optional[Mixer]: + return getattr(self.frequency_converter_up, "mixer", None) @property def rf_frequency(self): @@ -565,12 +565,35 @@ def apply_to_config(self, config: dict): ) element_cfg = config["elements"][self.name] - element_cfg["mixInputs"] = {**opx_outputs} element_cfg["intermediate_frequency"] = self.intermediate_frequency - if self.mixer is not None: - element_cfg["mixInputs"]["mixer"] = self.mixer.name - if self.local_oscillator is not None: - element_cfg["mixInputs"]["lo_frequency"] = self.local_oscillator.frequency + + from quam.components.octave import OctaveUpConverter + + if isinstance(self.frequency_converter_up, OctaveUpConverter): + octave = self.frequency_converter_up.octave + if octave is None: + raise ValueError( + f"Error generating config: channel {self.name} has an " + f"OctaveUpConverter (id={self.frequency_converter_up.id}) without " + "an attached Octave" + ) + element_cfg["RF_outputs"] = { + "port": (octave.name, self.frequency_converter_up.id) + } + elif str_ref.is_reference(self.frequency_converter_up): + raise ValueError( + f"Error generating config: channel {self.name} could not determine " + f'"frequency_converter_up", it seems to point to a non-existent ' + f"reference: {self.frequency_converter_up}" + ) + else: + element_cfg["mixInputs"] = {**opx_outputs} + if self.mixer is not None: + element_cfg["mixInputs"]["mixer"] = self.mixer.name + if self.local_oscillator is not None: + element_cfg["mixInputs"][ + "lo_frequency" + ] = self.local_oscillator.frequency for I_or_Q in ["I", "Q"]: controller_name, port = opx_outputs[I_or_Q] @@ -648,13 +671,34 @@ def apply_to_config(self, config: dict): # Note outputs instead of inputs because it's w.r.t. the QPU element_cfg = config["elements"][self.name] - element_cfg["outputs"] = { - "out1": tuple(self.opx_input_I), - "out2": tuple(self.opx_input_Q), - } element_cfg["smearing"] = self.smearing element_cfg["time_of_flight"] = self.time_of_flight + from quam.components.octave import OctaveDownConverter + + if isinstance(self.frequency_converter_down, OctaveDownConverter): + octave = self.frequency_converter_down.octave + if octave is None: + raise ValueError( + f"Error generating config: channel {self.name} has an " + f"OctaveDownConverter (id={self.frequency_converter_down.id}) " + "without an attached Octave" + ) + element_cfg["RF_inputs"] = { + "port": (octave.name, self.frequency_converter_down.id) + } + elif str_ref.is_reference(self.frequency_converter_down): + raise ValueError( + f"Error generating config: channel {self.name} could not determine " + f'"frequency_converter_down", it seems to point to a non-existent ' + f"reference: {self.frequency_converter_down}" + ) + else: + element_cfg["outputs"] = { + "out1": tuple(self.opx_input_I), + "out2": tuple(self.opx_input_Q), + } + for I_or_Q in ["I", "Q"]: controller_name, port = opx_inputs[I_or_Q] controller_cfg = self._config_add_controller(config, controller_name) diff --git a/tests/components/test_octave.py b/tests/components/test_octave.py index 1b348492..6a9f52cc 100644 --- a/tests/components/test_octave.py +++ b/tests/components/test_octave.py @@ -1,7 +1,7 @@ from copy import deepcopy import pytest -from quam.components.channels import InOutIQChannel, InOutSingleChannel +from quam.components.channels import IQChannel, InOutIQChannel, InOutSingleChannel from quam.components.octave import Octave, OctaveUpConverter, OctaveDownConverter from quam.core.qua_config_template import qua_config_template @@ -265,3 +265,60 @@ def test_instantiate_octave_default_connectivity(octave): for idx, RF_input in octave.RF_inputs.items(): assert RF_input.octave == octave assert RF_input.id == idx + + +def test_channel_add_RF_outputs(octave): + OctaveQuAM(octave=octave) + octave.RF_outputs[2] = OctaveUpConverter(id=2, LO_frequency=2e9) + + channel = IQChannel( + id="ch", + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up="#/octave/RF_outputs/2", + ) + + cfg = deepcopy(qua_config_template) + channel.apply_to_config(cfg) + + expected_cfg_elements = { + "ch": { + "intermediate_frequency": 0.0, + "RF_outputs": {"port": ("octave1", 2)}, + "operations": {}, + } + } + + assert cfg["elements"] == expected_cfg_elements + + +def test_channel_add_RF_inputs(octave): + OctaveQuAM(octave=octave) + octave.RF_outputs[3] = OctaveUpConverter(id=3, LO_frequency=2e9) + octave.RF_inputs[4] = OctaveDownConverter(id=4, LO_frequency=2e9) + + channel = InOutIQChannel( + id="ch", + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + opx_input_I=("con1", 1), + opx_input_Q=("con1", 2), + frequency_converter_up="#/octave/RF_outputs/3", + frequency_converter_down="#/octave/RF_inputs/4", + ) + + cfg = deepcopy(qua_config_template) + channel.apply_to_config(cfg) + + expected_cfg_elements = { + "ch": { + "intermediate_frequency": 0.0, + "RF_outputs": {"port": ("octave1", 3)}, + "RF_inputs": {"port": ("octave1", 4)}, + "operations": {}, + "smearing": 0, + "time_of_flight": 24, + } + } + + assert cfg["elements"] == expected_cfg_elements From a34ba8b1d165f1301083bfea9db363c5134b35d9 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 8 Mar 2024 10:59:05 +0100 Subject: [PATCH 10/13] Add BaseFrequencyConverter --- quam/components/channels.py | 6 +++--- quam/components/hardware.py | 8 +++++++- quam/components/octave.py | 6 +++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 21bd097a..614dfb44 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -1,7 +1,7 @@ from dataclasses import field from typing import ClassVar, Dict, List, Optional, Tuple, Union -from quam.components.hardware import FrequencyConverter, Mixer, LocalOscillator +from quam.components.hardware import BaseFrequencyConverter, Mixer, LocalOscillator from quam.components.pulses import Pulse, ReadoutPulse from quam.core import QuamComponent, quam_dataclass from quam.utils import string_reference as str_ref @@ -527,7 +527,7 @@ class IQChannel(Channel): opx_output_offset_I: float = None opx_output_offset_Q: float = None - frequency_converter_up: FrequencyConverter + frequency_converter_up: BaseFrequencyConverter intermediate_frequency: float = 0.0 @@ -654,7 +654,7 @@ class InOutIQChannel(IQChannel): input_gain: Optional[float] = None - frequency_converter_down: FrequencyConverter = None + frequency_converter_down: BaseFrequencyConverter = None _default_label: ClassVar[str] = "IQ" diff --git a/quam/components/hardware.py b/quam/components/hardware.py index ee3a93c1..b63023ee 100644 --- a/quam/components/hardware.py +++ b/quam/components/hardware.py @@ -108,7 +108,13 @@ def IQ_imbalance(g: float, phi: float) -> List[float]: @quam_dataclass -class FrequencyConverter(QuamComponent): +class BaseFrequencyConverter(QuamComponent): + """Base class for frequency converters.""" + pass + + +@quam_dataclass +class FrequencyConverter(BaseFrequencyConverter): local_oscillator: LocalOscillator = None mixer: Mixer = None gain: float = None diff --git a/quam/components/octave.py b/quam/components/octave.py index a24fdf09..92f0bbe2 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -4,7 +4,7 @@ from dataclasses import field from quam.core import QuamComponent, quam_dataclass -from quam.components.hardware import FrequencyConverter +from quam.components.hardware import BaseFrequencyConverter, FrequencyConverter from quam.components.channels import ( Channel, IQChannel, @@ -128,7 +128,7 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass -class OctaveFrequencyConverter(FrequencyConverter, ABC): +class OctaveFrequencyConverter(BaseFrequencyConverter, ABC): """Base class for OctaveUpConverter and OctaveDownConverter. Args: @@ -394,7 +394,7 @@ def _initialize_qm(self) -> QuantumMachine: host=self.qmm_host, port=self.qmm_port, octave=self.octave_config, - connection_headers=self.connection_headers + connection_headers=self.connection_headers, ) qm = qmm.open_qm(self._root.generate_config()) return qm From b857b590eac873845fbeed2b91ea45b364f33d88 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 8 Mar 2024 11:22:31 +0100 Subject: [PATCH 11/13] Avoid breaking change for FrequencyConverter --- quam/core/deprecations.py | 46 +++++++++++++++++++ quam/core/quam_instantiation.py | 9 ++++ .../test_instantiation_deprecation.py | 36 +++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 quam/core/deprecations.py create mode 100644 tests/instantiation/test_instantiation_deprecation.py diff --git a/quam/core/deprecations.py b/quam/core/deprecations.py new file mode 100644 index 00000000..d22ee5ca --- /dev/null +++ b/quam/core/deprecations.py @@ -0,0 +1,46 @@ +import warnings +from abc import abstractclassmethod + + +instantiation_deprecations = [] + + +class InstantiationDeprecationRule: + @abstractclassmethod + def match(cls, quam_class, contents): + raise NotImplementedError + + @abstractclassmethod + def apply(cls, quam_class, contents): + raise NotImplementedError + + +class DeprecatedFrequencyConverterInstantiation(InstantiationDeprecationRule): + @classmethod + def match(cls, quam_class, contents): + from quam.components.hardware import BaseFrequencyConverter + + if quam_class != BaseFrequencyConverter: + return False + if "__class__" in contents: + return False + return True + + @classmethod + def apply(cls, quam_class, contents): + from quam.components.hardware import FrequencyConverter + + warnings.warn( + "The default frequency converter for channels is changed to the " + "`BaseFrequencyConverter`. If you want to use `FrequencyConverter`, " + 'Please add {"__class__": "quam.components.hardware.FrequencyConverter"} ' + "to the JSON contents of the frequency converter. This will raise an error " + "in future versions.", + DeprecationWarning, + ) + contents["__class__"] = "quam.components.hardware.FrequencyConverter" + + return FrequencyConverter, contents + + +instantiation_deprecations.append(DeprecatedFrequencyConverterInstantiation) diff --git a/quam/core/quam_instantiation.py b/quam/core/quam_instantiation.py index 9f1457b9..ef02b0ad 100644 --- a/quam/core/quam_instantiation.py +++ b/quam/core/quam_instantiation.py @@ -10,6 +10,7 @@ validate_obj_type, type_is_optional, ) +from .deprecations import instantiation_deprecations if TYPE_CHECKING: from quam.core import QuamBase @@ -336,6 +337,13 @@ def instantiate_quam_class( Returns: QuamBase instance """ + # Add depcrecation checks + for deprecation_rule in instantiation_deprecations: + if deprecation_rule.match(quam_class=quam_class, contents=contents): + quam_class, contents = deprecation_rule.apply( + quam_class=quam_class, contents=contents + ) + if not str_repr: str_repr = quam_class.__name__ # str_repr = f"{str_repr}.{quam_class.__name__}" if str_repr else quam_class.__name__ @@ -348,6 +356,7 @@ def instantiate_quam_class( f"contents must be a dict, not {type(contents)}, could not instantiate" f" {str_repr}. Contents: {contents}" ) + attr_annotations = get_dataclass_attr_annotations(quam_class) instantiated_attrs = instantiate_attrs( diff --git a/tests/instantiation/test_instantiation_deprecation.py b/tests/instantiation/test_instantiation_deprecation.py new file mode 100644 index 00000000..57c4f4a4 --- /dev/null +++ b/tests/instantiation/test_instantiation_deprecation.py @@ -0,0 +1,36 @@ +import pytest + +from quam.components.hardware import FrequencyConverter, BaseFrequencyConverter +from quam.core.quam_instantiation import instantiate_quam_class +from quam.core.deprecations import DeprecatedFrequencyConverterInstantiation + + +def test_deprecation_frequency_converter(): + assert not DeprecatedFrequencyConverterInstantiation.match(FrequencyConverter, {}) + assert DeprecatedFrequencyConverterInstantiation.match(BaseFrequencyConverter, {}) + assert not DeprecatedFrequencyConverterInstantiation.match( + BaseFrequencyConverter, + {"__class__": "quam.components.hardware.FrequencyConverter"}, + ) + + cls, contents = DeprecatedFrequencyConverterInstantiation.apply( + BaseFrequencyConverter, {} + ) + assert cls == FrequencyConverter + assert contents == {"__class__": "quam.components.hardware.FrequencyConverter"} + + cls, contents = DeprecatedFrequencyConverterInstantiation.apply( + BaseFrequencyConverter, + {"__class__": "quam.components.hardware.BaseFrequencyConverter"}, + ) + assert cls == FrequencyConverter + assert contents == {"__class__": "quam.components.hardware.FrequencyConverter"} + + +def test_instantiate_frequency_converter_deprecation(): + contents = {} + with pytest.deprecated_call(): + obj = instantiate_quam_class(BaseFrequencyConverter, contents) + + assert isinstance(obj, FrequencyConverter) + assert contents == {"__class__": "quam.components.hardware.FrequencyConverter"} From 550ef4943702f9e387360fb473a51f253f1f5c30 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 8 Mar 2024 12:51:19 +0100 Subject: [PATCH 12/13] fix frequency converter tests --- tests/components/channels/test_in_out_IQ_channel.py | 2 ++ tests/components/test_pulses.py | 6 +++++- tests/examples/superconducting_qubits/test_transmon.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/components/channels/test_in_out_IQ_channel.py b/tests/components/channels/test_in_out_IQ_channel.py index 0ccef9a3..0ad7fb5c 100644 --- a/tests/components/channels/test_in_out_IQ_channel.py +++ b/tests/components/channels/test_in_out_IQ_channel.py @@ -40,6 +40,7 @@ def test_empty_in_out_IQ_channel(): d = readout_resonator.to_dict() assert d == { "frequency_converter_up": { + "__class__": "quam.components.hardware.FrequencyConverter", "mixer": {}, "local_oscillator": {"frequency": 5000000000.0}, }, @@ -127,6 +128,7 @@ def test_readout_resonator_with_readout(): d = readout_resonator.to_dict() assert d == { "frequency_converter_up": { + "__class__": "quam.components.hardware.FrequencyConverter", "mixer": {}, "local_oscillator": {"frequency": 5000000000.0}, }, diff --git a/tests/components/test_pulses.py b/tests/components/test_pulses.py index eb86dd74..35ae069a 100644 --- a/tests/components/test_pulses.py +++ b/tests/components/test_pulses.py @@ -53,7 +53,11 @@ def test_IQ_channel(): "opx_output_I": 0, "opx_output_Q": 1, "intermediate_frequency": 100e6, - "frequency_converter_up": {"mixer": {}, "local_oscillator": {}}, + "frequency_converter_up": { + "__class__": "quam.components.hardware.FrequencyConverter", + "mixer": {}, + "local_oscillator": {}, + }, } diff --git a/tests/examples/superconducting_qubits/test_transmon.py b/tests/examples/superconducting_qubits/test_transmon.py index fe085704..03f3da8e 100644 --- a/tests/examples/superconducting_qubits/test_transmon.py +++ b/tests/examples/superconducting_qubits/test_transmon.py @@ -97,6 +97,7 @@ def test_transmon_add_pulse(): "opx_output_I": ("con1", 1), "opx_output_Q": ("con1", 2), "frequency_converter_up": { + "__class__": "quam.components.hardware.FrequencyConverter", "mixer": {}, "local_oscillator": {"frequency": 5000000000.0}, }, From e112061194a32b39c0902fc073b0f2f41744a7ab Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 8 Mar 2024 13:00:05 +0100 Subject: [PATCH 13/13] Added test loading Octave --- quam/components/octave.py | 8 +++----- tests/components/test_octave.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index 92f0bbe2..555cb1da 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -214,7 +214,7 @@ class OctaveUpConverter(OctaveFrequencyConverter): entering the mixer. Off by default. """ - LO_frequency: float + LO_frequency: float = None LO_source: Literal["internal", "external"] = "internal" gain: float = 0 output_mode: Literal[ @@ -290,10 +290,8 @@ class OctaveDownConverter(OctaveFrequencyConverter): are connected to the opposite OPX inputs. """ - LO_frequency: float # Between 2 and 18 GHz - LO_source: Literal["internal", "external"] = ( - "internal" # default is internal for LO 1, external for LO 2 - ) + LO_frequency: float = None + LO_source: Literal["internal", "external"] = "internal" IF_mode_I: Literal["direct", "envelope", "mixer", "off"] = "direct" IF_mode_Q: Literal["direct", "envelope", "mixer", "off"] = "direct" IF_output_I: Literal[1, 2] = 1 diff --git a/tests/components/test_octave.py b/tests/components/test_octave.py index 6a9f52cc..93f6f642 100644 --- a/tests/components/test_octave.py +++ b/tests/components/test_octave.py @@ -322,3 +322,32 @@ def test_channel_add_RF_inputs(octave): } assert cfg["elements"] == expected_cfg_elements + + +def test_load_octave(octave): + machine = OctaveQuAM(octave=octave) + octave.initialize_default_connectivity() + + d = machine.to_dict() + + d_expected = { + "__class__": "test_octave.OctaveQuAM", + "octave": { + "RF_inputs": {1: {"id": 1}, 2: {"id": 2}}, + "RF_outputs": { + 1: {"id": 1}, + 2: {"id": 2}, + 3: {"id": 3}, + 4: {"id": 4}, + 5: {"id": 5}, + }, + "ip": "127.0.0.1", + "name": "octave1", + "port": 80, + }, + } + assert d == d_expected + + machine2 = OctaveQuAM.load(d) + + assert d == machine2.to_dict()