diff --git a/quam/components/channels.py b/quam/components/channels.py index e1489d58..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 +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,19 +527,19 @@ 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 _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] @@ -631,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" @@ -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/quam/components/hardware.py b/quam/components/hardware.py index ca416e6d..b63023ee 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 @@ -109,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 bcc1a91f..555cb1da 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -1,20 +1,359 @@ +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 qm import QuantumMachinesManager -from qm import QuantumMachine +from quam.components.hardware import BaseFrequencyConverter, FrequencyConverter +from quam.components.channels import ( + Channel, + IQChannel, + InOutIQChannel, + InOutSingleChannel, + SingleChannel, +) + +from qm import QuantumMachinesManager, QuantumMachine +from qm.octave import QmOctaveConfig, RFOutputMode, ClockType from qm.octave.qm_octave import QmOctave -from octave_sdk import RFInputLOSource -from qm.octave import QmOctaveConfig, RFOutputMode, ClockType +__all__ = [ + "Octave", + "OctaveUpConverter", + "OctaveDownConverter", + "OctaveOldFrequencyConverter", + "OctaveOld", +] + + +@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 + + RF_outputs: Dict[int, "OctaveUpConverter"] = 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): + """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. " + "octave.RF_outputs is not empty" + ) + if self.RF_inputs: + raise ValueError( + "Error initializing Octave with default connectivity. " + "octave.IF_outputs is not empty" + ) + + for idx in range(1, 6): + self.RF_outputs[idx] = OctaveUpConverter( + id=idx, + LO_frequency=None, # TODO What should default be? + ) + + 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.""" + 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: + """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"]: + 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": {}, + "RF_inputs": {}, + "loopbacks": self.loopbacks, + } + + +@quam_dataclass +class OctaveFrequencyConverter(BaseFrequencyConverter, 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 + + @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 parent_parent + + @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: + """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') + + if self.octave is None: + raise KeyError( + f"Error generating config: OctaveConverter with id {self.id} does not " + "have an Octave parent" + ) + + 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}"]' + ) + + +@quam_dataclass +class OctaveUpConverter(OctaveFrequencyConverter): + """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 = None + LO_source: Literal["internal", "external"] = "internal" + gain: float = 0 + output_mode: Literal[ + "always_on", "always_off", "triggered", "triggered_reersed" + ] = "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"]: + 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}"' + ) -__all__ = ["OctaveOldFrequencyConverter", "OctaveOld"] + output_config = config["octaves"][self.octave.name]["RF_outputs"][self.id] = { + "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(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 = 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 + IF_output_Q: Literal[1, 2] = 2 + + @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"]: + 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, + "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_input_I, self.channel.opx_input_Q] + elif isinstance(self.channel, InOutSingleChannel): + IF_channels = [self.IF_output_I] + 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), 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 id={self.id}: " + f"Unable to assign {label} to port {opx_ch} because it is already " + f"assigned to port {IF_config[label]['port']} " + ) @quam_dataclass @@ -53,7 +392,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 @@ -78,6 +417,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) 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_classes.py b/quam/core/quam_classes.py index 7e2455c3..5369598f 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/quam/core/quam_instantiation.py b/quam/core/quam_instantiation.py index 9f1457b9..28a8bd12 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 @@ -229,6 +230,8 @@ def instantiate_attr( if isinstance(attr_val, list): attr_val = tuple(attr_val) instantiated_attr = attr_val + elif typing.get_origin(expected_type) == typing.Literal: + instantiated_attr = attr_val elif typing.get_origin(expected_type) is not None and validate_type: raise TypeError( f"Instantiation for type {expected_type} in {str_repr} not implemented" @@ -336,6 +339,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 +358,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/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_octave.py b/tests/components/test_octave.py new file mode 100644 index 00000000..93f6f642 --- /dev/null +++ b/tests/components/test_octave.py @@ -0,0 +1,353 @@ +from copy import deepcopy + +import pytest +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 +from quam.core.quam_classes import QuamRoot, quam_dataclass + + +@quam_dataclass +class OctaveQuAM(QuamRoot): + octave: Octave + + +@pytest.fixture +def octave(): + return Octave(name="octave1", ip="127.0.0.1", port=80) + + +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): + machine = OctaveQuAM(octave=octave) + config = machine.generate_config() + + expected_cfg = deepcopy(qua_config_template) + 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_off", + "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 + + +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 + + +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() 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}, }, diff --git a/tests/instantiation/test_instantiation.py b/tests/instantiation/test_instantiation.py index 2e0c0a44..40b14fab 100644 --- a/tests/instantiation/test_instantiation.py +++ b/tests/instantiation/test_instantiation.py @@ -1,13 +1,10 @@ import pytest -from typing import List, Optional +from typing import List, Literal, Optional from quam.core import QuamRoot, QuamComponent, quam_dataclass from quam.examples.superconducting_qubits.components import Transmon from quam.core.quam_instantiation import * -from quam.utils import ( - get_dataclass_attr_annotations, - validate_obj_type, -) +from quam.utils import get_dataclass_attr_annotations def test_get_dataclass_attributes(): @@ -75,58 +72,6 @@ class TestClass(AbstractClass): } -def test_validate_standard_types(): - validate_obj_type(1, int) - validate_obj_type(1.0, float) - validate_obj_type("hello", str) - validate_obj_type(":reference", str) - validate_obj_type([1, 2, 3], list) - validate_obj_type((1, 2, 3), tuple) - validate_obj_type({"a": 1, "b": 2}, dict) - validate_obj_type(True, bool) - validate_obj_type(None, type(None)) - - with pytest.raises(TypeError): - validate_obj_type(1, str) - with pytest.raises(TypeError): - validate_obj_type("hello", int) - - -def test_validate_type_exceptions(): - validate_obj_type("#/reference", int) - validate_obj_type("#/reference", str) - validate_obj_type("#./reference", int) - validate_obj_type("#./reference", str) - validate_obj_type("#../reference", int) - validate_obj_type("#../reference", str) - - validate_obj_type(None, int) - validate_obj_type(None, str) - - -def test_validate_typing_list(): - validate_obj_type([1, 2, 3], List[int]) - with pytest.raises(TypeError): - validate_obj_type([1, 2, 3], List[str]) - - validate_obj_type([1, 2, 3], List) - validate_obj_type(["a", "b", "c"], List) - validate_obj_type(["a", "b", "c"], List[str]) - with pytest.raises(TypeError): - validate_obj_type(["a", "b", "c"], List[int]) - - -def test_validate_typing_dict(): - validate_obj_type({"a": 1, "b": 2}, dict) - validate_obj_type({"a": 1, "b": 2}, Dict[str, int]) - with pytest.raises(TypeError): - validate_obj_type({"a": 1, "b": 2}, Dict[str, str]) - - validate_obj_type("#/reference", Dict[str, int]) - validate_obj_type("#./reference", Dict[str, int]) - validate_obj_type("#../reference", Dict[str, int]) - - @quam_dataclass class QuamComponentTest(QuamComponent): test_str: str @@ -347,6 +292,28 @@ def test_instantiate_sublist(): class TestQuamSubList(QuamComponent): sublist: List[List[float]] - obj = instantiate_quam_class(TestQuamSubList, {"sublist": [[1,2,3], [4,5,6]]}) + obj = instantiate_quam_class(TestQuamSubList, {"sublist": [[1, 2, 3], [4, 5, 6]]}) + + assert obj.sublist == [[1, 2, 3], [4, 5, 6]] + + +def test_instantiate_attr_literal(): + attr = instantiate_attr( + attr_val="a", + expected_type=Literal["a", "b", "c"], + ) + assert attr == "a" - assert obj.sublist == [[1,2,3], [4,5,6]] \ No newline at end of file + +def test_instance_attr_literal_fail(): + with pytest.raises(TypeError): + instantiate_attr( + attr_val="d", + expected_type=Literal["a", "b", "c"], + ) + + with pytest.raises(TypeError): + instantiate_attr( + attr_val=1, + expected_type=Literal["a", "b", "c"], + ) 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"} diff --git a/tests/utils/test_validate_obj_type.py b/tests/utils/test_validate_obj_type.py new file mode 100644 index 00000000..4f378162 --- /dev/null +++ b/tests/utils/test_validate_obj_type.py @@ -0,0 +1,67 @@ +import pytest +from typing import List, Dict, Literal + +from quam.utils.general import validate_obj_type + + +def test_validate_standard_types(): + validate_obj_type(1, int) + validate_obj_type(1.0, float) + validate_obj_type("hello", str) + validate_obj_type(":reference", str) + validate_obj_type([1, 2, 3], list) + validate_obj_type((1, 2, 3), tuple) + validate_obj_type({"a": 1, "b": 2}, dict) + validate_obj_type(True, bool) + validate_obj_type(None, type(None)) + + with pytest.raises(TypeError): + validate_obj_type(1, str) + with pytest.raises(TypeError): + validate_obj_type("hello", int) + + +def test_validate_type_exceptions(): + validate_obj_type("#/reference", int) + validate_obj_type("#/reference", str) + validate_obj_type("#./reference", int) + validate_obj_type("#./reference", str) + validate_obj_type("#../reference", int) + validate_obj_type("#../reference", str) + + validate_obj_type(None, int) + validate_obj_type(None, str) + + +def test_validate_typing_list(): + validate_obj_type([1, 2, 3], List[int]) + with pytest.raises(TypeError): + validate_obj_type([1, 2, 3], List[str]) + + validate_obj_type([1, 2, 3], List) + validate_obj_type(["a", "b", "c"], List) + validate_obj_type(["a", "b", "c"], List[str]) + with pytest.raises(TypeError): + validate_obj_type(["a", "b", "c"], List[int]) + + +def test_validate_typing_dict(): + validate_obj_type({"a": 1, "b": 2}, dict) + validate_obj_type({"a": 1, "b": 2}, Dict[str, int]) + with pytest.raises(TypeError): + validate_obj_type({"a": 1, "b": 2}, Dict[str, str]) + + validate_obj_type("#/reference", Dict[str, int]) + validate_obj_type("#./reference", Dict[str, int]) + validate_obj_type("#../reference", Dict[str, int]) + + +def test_validate_typing_literal(): + validate_obj_type("a", Literal["a", "b", "c"]) + validate_obj_type("b", Literal["a", "b", "c"]) + + with pytest.raises(TypeError): + validate_obj_type("d", Literal["a", "b", "c"]) + + with pytest.raises(TypeError): + validate_obj_type(123, Literal["b", "c"])