diff --git a/CHANGELOG.md b/CHANGELOG.md index 6936379c..a68214bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ - ## [Unreleased] ### Added -- Added InOutSingleChannel - Add optional `config_settings` property to quam components indicating that they should be called before/after other components when generating QUA configuration - Added `InOutIQChannel.measure_accumulated/sliced` - Added `StandardReadoutPulse`. All readout pulses can now be created simply by inheriting from the `StandardReadoutPulse` and the non-readout variant. + +## [0.3.0] +### Added +- Added InOutSingleChannel +- Added optional `config_settings` property to quam components indicating that they should be called before/after other components when generating QUA configuration +- Added support for the new Octave API. +- Added support for `Literal` types in QuAM + + ### Changed - Changed `InOutIQChannel.input_offset_I/Q` to `InOutIQChannel.opx_input_offset_I/Q` - Renamed `SingleChannel.output_offset` -> `SingleChannel.opx_output_offset` @@ -22,15 +29,17 @@ - Move `quam.components.superconducting_qubits` to `quam.examples.superconducting_qubits` - Replaced `InOutIQChannel.measure` kwargs `I_var` and `Q_var` by `qua_vars` tuple - `Pulse.id` is now an instance variable instead of a class variable +- Channel frequency converter default types are now `BaseFrequencyConverter` which has fewer attributes than `FrequencyConverter`. This is to make it compatible with the new Octave API. ### Fixed - Don't raise instantiation error when required_type is not a class - Add support for QuAM component sublist type: List[List[...]] -- Channel offsets (e.g. `SingleChannel.opx_output_offset`) are ensured to be unique +- Channel offsets (e.g. `SingleChannel.opx_output_offset`) are ensured to be unique, otherwise a warning is raised - Previously the offset could be overwritten when two channels share the same port - Default values are None, and they're only added if nonzero - If the offset is not specified in config at the end, it's manually added to be 0.0 - JSON serializer doesn't break if an item is added to ignore that isn't part of QuAM +- Allow `QuamDict` keys to be integers ## [0.2.2] - ### Added diff --git a/docs/components/octave.md b/docs/components/octave.md new file mode 100644 index 00000000..c93e60a9 --- /dev/null +++ b/docs/components/octave.md @@ -0,0 +1,374 @@ +# Octave + +An Octave is represented in QuAM through the [quam.components.octave.Octave][] class. +Below we describe the three steps needed to configuring an Octave in QuAM: + +1. Creating the Octave +2. Adding frequency converters +3. Attaching channels + +## :zero: Creating the root QuAM machine +Before we get started, we need a top-level QuAM class that matches our components: + +```python +from typing import Dict +from dataclasses import field +from quam.core import QuamRoot, quam_dataclass +from quam.components import Octave, OctaveUpConverter, OctaveDownConverter, Channel + +@quam_dataclass +class QuAM(QuamRoot): + octave: Octave = None + channels: Dict[str, Channel] = field(default_factory=dict) + +machine = QuAM() +``` + +This will be used later to generate our QUA configuration + +## :one: Creating the Octave +Below we show how an Octave is instantiated using some example arguments: + +```python +octave = Octave( + name="octave1", + ip="127.0.0.1", + port=80, +) +machine.octave = octave +``` + +We can next retrieve the Octave config `QmOctaveConfig`, used to create the `QuantumMachinesManager` +```python +octave_config = octave.get_octave_config() +# The calibration_db and device_info are automatically configured + +qmm = QuantumMachinesManager(host={opx_host}, port={opx_port}, octave=octave_config) +``` + +At this point the channel connectivity of the Octave hasn't yet been configured. +We can do so by adding frequency converters. + +## :two: Adding frequency converters +A frequency converter is a grouping of the components needed to upconvert or downconvert a signal. +These typically consist of a local oscillator, mixer, as well as IF, LO, and RF ports. +For the Octave we have two types of frequency converters: + +- [OctaveUpConverter][quam.components.octave.OctaveUpConverter]: Used to upconvert a pair of IF signals to an RF signal +- [OctaveDownCovnerter][quam.components.octave.OctaveDownConverter]: Used to downconvert an RF signal to a pair of IF signals + +We can add all relevant frequency converters as follows: + +```python +octave.initialize_frequency_converters() + +octave.print_summary() +``` + +/// details | `octave.print_summary()` output +```json +Octave (parent unknown): + name: "octave1" + ip: "127.0.0.1" + port: 80 + calibration_db_path: None + RF_outputs: QuamDict + 1: OctaveUpConverter + id: 1 + channel: None + LO_frequency: None + LO_source: "internal" + gain: 0 + output_mode: "always_off" + input_attenuators: "off" + 2: OctaveUpConverter + id: 2 + channel: None + LO_frequency: None + LO_source: "internal" + gain: 0 + output_mode: "always_off" + input_attenuators: "off" + 3: OctaveUpConverter + id: 3 + channel: None + LO_frequency: None + LO_source: "internal" + gain: 0 + output_mode: "always_off" + input_attenuators: "off" + 4: OctaveUpConverter + id: 4 + channel: None + LO_frequency: None + LO_source: "internal" + gain: 0 + output_mode: "always_off" + input_attenuators: "off" + 5: OctaveUpConverter + id: 5 + channel: None + LO_frequency: None + LO_source: "internal" + gain: 0 + output_mode: "always_off" + input_attenuators: "off" + RF_inputs: QuamDict + 1: OctaveDownConverter + id: 1 + channel: None + LO_frequency: None + LO_source: "internal" + IF_mode_I: "direct" + IF_mode_Q: "direct" + IF_output_I: 1 + IF_output_Q: 2 + 2: OctaveDownConverter + id: 2 + channel: None + LO_frequency: None + LO_source: "internal" + IF_mode_I: "direct" + IF_mode_Q: "direct" + IF_output_I: 1 + IF_output_Q: 2 + loopbacks: QuamList = [] +``` +/// + +We can see five `OctaveUpConverter` elements in `Octave.RF_outputs`, and two `OctaveDownConverter` elements in `Octave.RF_inputs`, matching with the number of RF outputs / inputs, respectively. +It is important to specify the `LO_frequency` of the frequency converters that are used, otherwise they will not add information to the QUA configuration when it is generated. + +At this point, our `Octave` does not yet contain any information on which OPX output / input is connected to each `OctaveUpconverter` / `OctaveDownConverter`. +This is done in the third stage + +## :three: Attaching channels +Once the frequency converters have been setup, it is time to attach the ones that are in use to corresponding channels in QuAM. +In the example below, we connect an `IQChannel` to the `OctaveUpconverter` at `octave.RF_outputs[1]` +```python +from quam.components import IQChannel, InOutIQChannel + +machine.channels["IQ1"] = IQChannel( + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=octave.RF_outputs[1].get_reference() +) +octave.RF_outputs[1].channel = machine.channels["IQ1"].get_reference() +octave.RF_outputs[1].LO_frequency = 2e9 # Remember to set the LO frequency +``` + +Similarly, we can connect an `InOutIQChannel` to a combination of an `OctaveUpConverter` and `OctaveDownConverter` +```python +machine.channels["IQ2"] = InOutIQChannel( + opx_output_I=("con1", 3), + opx_output_Q=("con1", 4), + opx_input_I=("con1", 1), + opx_input_Q=("con1", 2), + frequency_converter_up=octave.RF_outputs[2].get_reference(), + frequency_converter_down=octave.RF_inputs[1].get_reference() +) +octave.RF_outputs[2].channel = machine.channels["IQ2"].get_reference() +octave.RF_inputs[1].channel = machine.channels["IQ2"].get_reference() +octave.RF_outputs[2].LO_frequency = 2e9 +octave.RF_inputs[2].LO_frequency = 2e9 +``` + +## Generating the config +Once everything is setup, we can generate the QUA configuration + +```python +qua_config = machine.generate_config() +``` + +/// details | qua_config +```json +{ + "version": 1, + "controllers": { + "con1": { + "analog_outputs": { + "1": { + "offset": 0.0 + }, + "2": { + "offset": 0.0 + }, + "3": { + "offset": 0.0 + }, + "4": { + "offset": 0.0 + } + }, + "digital_outputs": {}, + "analog_inputs": { + "1": { + "offset": 0.0 + }, + "2": { + "offset": 0.0 + } + } + } + }, + "elements": { + "IQ1": { + "operations": {}, + "intermediate_frequency": 0.0, + "RF_outputs": { + "port": [ + "octave1", + 1 + ] + } + }, + "IQ2": { + "operations": {}, + "intermediate_frequency": 0.0, + "RF_outputs": { + "port": [ + "octave1", + 2 + ] + }, + "smearing": 0, + "time_of_flight": 24, + "RF_inputs": { + "port": [ + "octave1", + 1 + ] + } + } + }, + "pulses": { + "const_pulse": { + "operation": "control", + "length": 1000, + "waveforms": { + "I": "const_wf", + "Q": "zero_wf" + } + } + }, + "waveforms": { + "zero_wf": { + "type": "constant", + "sample": 0.0 + }, + "const_wf": { + "type": "constant", + "sample": 0.1 + } + }, + "digital_waveforms": { + "ON": { + "samples": [ + [ + 1, + 0 + ] + ] + } + }, + "integration_weights": {}, + "mixers": {}, + "oscillators": {}, + "octaves": { + "octave1": { + "RF_outputs": { + "1": { + "LO_frequency": 2000000000.0, + "LO_source": "internal", + "gain": 0, + "output_mode": "always_off", + "input_attenuators": "off", + "I_connection": [ + "con1", + 1 + ], + "Q_connection": [ + "con1", + 2 + ] + }, + "2": { + "LO_frequency": 2000000000.0, + "LO_source": "internal", + "gain": 0, + "output_mode": "always_off", + "input_attenuators": "off", + "I_connection": [ + "con1", + 3 + ], + "Q_connection": [ + "con1", + 4 + ] + } + }, + "IF_outputs": {}, + "RF_inputs": {}, + "loopbacks": [] + } + } +} + +``` +/// + +## Combined example +```python +from typing import Dict +from dataclasses import field +from quam.core import QuamRoot, quam_dataclass +from quam.components import Octave, OctaveUpConverter, OctaveDownConverter, Channel + +@quam_dataclass +class QuAM(QuamRoot): + octave: Octave = None + channels: Dict[str, Channel] = field(default_factory=dict) + +machine = QuAM() + + +octave = Octave( + name="octave1", + ip="127.0.0.1", + port=80, +) +machine.octave = octave + +octave.initialize_frequency_converters() + +octave.print_summary() + + +from quam.components import IQChannel, InOutIQChannel + +machine.channels["IQ1"] = IQChannel( + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=octave.RF_outputs[1].get_reference() +) +octave.RF_outputs[1].channel = machine.channels["IQ1"].get_reference() +octave.RF_outputs[1].LO_frequency = 2e9 + + +machine.channels["IQ2"] = InOutIQChannel( + opx_output_I=("con1", 3), + opx_output_Q=("con1", 4), + opx_input_I=("con1", 1), + opx_input_Q=("con1", 2), + frequency_converter_up=octave.RF_outputs[2].get_reference(), + frequency_converter_down=octave.RF_inputs[1].get_reference() +) +octave.RF_outputs[2].channel = machine.channels["IQ2"].get_reference() +octave.RF_inputs[1].channel = machine.channels["IQ2"].get_reference() +octave.RF_outputs[2].LO_frequency = 2e9 +octave.RF_inputs[2].LO_frequency = 2e9 + + +qua_config = machine.generate_config() +``` diff --git a/docs/demonstration.md b/docs/demonstration.md index 54fd96f7..fe99cf32 100644 --- a/docs/demonstration.md +++ b/docs/demonstration.md @@ -7,7 +7,7 @@ The standard QuAM components can be imported using ```python from quam.components import * -from quam.components.superconducting_qubits import Transmon, QuAM +from quam.examples.superconducting_qubits import Transmon, QuAM ``` Since we're starting from scratch, we will have to instantiate all QuAM components. This has to be done once, after which we will generally save and load QuAM from a file. @@ -62,6 +62,138 @@ for idx in range(num_qubits): ``` This example demonstrates that QuAM follows a tree structure: each component can have a parent and it can have children as attributes. +We can print a summary of QuAM using +```python +machine.print_summary() +``` + +/// details | `machine.print_summary()` output +```json +QuAM: + mixers: QuamList = [] + qubits: QuamList: + 0: Transmon + id: 0 + xy: IQChannel + operations: QuamDict Empty + id: None + digital_outputs: QuamDict Empty + opx_output_I: ('con1', 3) + opx_output_Q: ('con1', 4) + opx_output_offset_I: None + opx_output_offset_Q: None + frequency_converter_up: FrequencyConverter + local_oscillator: LocalOscillator + frequency: 6000000000.0 + power: 10 + mixer: Mixer + local_oscillator_frequency: "#../local_oscillator/frequency" + intermediate_frequency: "#../../intermediate_frequency" + correction_gain: 0 + correction_phase: 0 + gain: None + intermediate_frequency: 0.0 + z: SingleChannel + operations: QuamDict Empty + id: None + digital_outputs: QuamDict Empty + opx_output: ('con1', 5) + filter_fir_taps: None + filter_iir_taps: None + opx_output_offset: None + intermediate_frequency: None + resonator: None + 1: Transmon + id: 1 + xy: IQChannel + operations: QuamDict Empty + id: None + digital_outputs: QuamDict Empty + opx_output_I: ('con1', 6) + opx_output_Q: ('con1', 7) + opx_output_offset_I: None + opx_output_offset_Q: None + frequency_converter_up: FrequencyConverter + local_oscillator: LocalOscillator + frequency: 6000000000.0 + power: 10 + mixer: Mixer + local_oscillator_frequency: "#../local_oscillator/frequency" + intermediate_frequency: "#../../intermediate_frequency" + correction_gain: 0 + correction_phase: 0 + gain: None + intermediate_frequency: 0.0 + z: SingleChannel + operations: QuamDict Empty + id: None + digital_outputs: QuamDict Empty + opx_output: ('con1', 8) + filter_fir_taps: None + filter_iir_taps: None + opx_output_offset: None + intermediate_frequency: None + resonator: None + resonators: QuamList: + 0: InOutIQChannel + operations: QuamDict Empty + id: 0 + digital_outputs: QuamDict Empty + opx_output_I: ('con1', 1) + opx_output_Q: ('con1', 2) + opx_output_offset_I: None + opx_output_offset_Q: None + frequency_converter_up: FrequencyConverter + local_oscillator: LocalOscillator + frequency: 6000000000.0 + power: 10 + mixer: Mixer + local_oscillator_frequency: "#../local_oscillator/frequency" + intermediate_frequency: "#../../intermediate_frequency" + correction_gain: 0 + correction_phase: 0 + gain: None + intermediate_frequency: 0.0 + opx_input_I: ('con1', 1) + opx_input_Q: ('con1', 2) + time_of_flight: 24 + smearing: 0 + opx_input_offset_I: None + opx_input_offset_Q: None + input_gain: None + frequency_converter_down: None + 1: InOutIQChannel + operations: QuamDict Empty + id: 1 + digital_outputs: QuamDict Empty + opx_output_I: ('con1', 1) + opx_output_Q: ('con1', 2) + opx_output_offset_I: None + opx_output_offset_Q: None + frequency_converter_up: FrequencyConverter + local_oscillator: LocalOscillator + frequency: 6000000000.0 + power: 10 + mixer: Mixer + local_oscillator_frequency: "#../local_oscillator/frequency" + intermediate_frequency: "#../../intermediate_frequency" + correction_gain: 0 + correction_phase: 0 + gain: None + intermediate_frequency: 0.0 + opx_input_I: ('con1', 1) + opx_input_Q: ('con1', 2) + time_of_flight: 24 + smearing: 0 + opx_input_offset_I: None + opx_input_offset_Q: None + input_gain: None + frequency_converter_down: None + local_oscillators: QuamList = [] + wiring: QuamDict Empty +``` +/// + ## Saving and loading QuAM diff --git a/docs/examples/QuAM features.ipynb b/docs/examples/QuAM features.ipynb index 95805b38..348a6421 100644 --- a/docs/examples/QuAM features.ipynb +++ b/docs/examples/QuAM features.ipynb @@ -11,19 +11,11 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-12-04 20:47:18,324 - qm - INFO - Starting session: da392830-ef24-41ad-aa85-126cae012a21\n" - ] - } - ], + "outputs": [], "source": [ "import json\n", "from quam.components import *\n", - "from quam.components.superconducting_qubits import *\n", + "from quam.examples.superconducting_qubits import *\n", "\n", "from pathlib import Path\n", "root_folder = Path(\"./output\")\n", @@ -83,7 +75,7 @@ { "data": { "text/plain": [ - "QuAM(mixers=[], qubits=[Transmon(id=0, xy=IQChannel(operations={}, id=None, opx_output_I=('con1', 3), opx_output_Q=('con1', 4), opx_output_offset_I=0.0, opx_output_offset_Q=0.0, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0), z=SingleChannel(operations={}, id=None, opx_output=('con1', 5), filter_fir_taps=None, filter_iir_taps=None, output_offset=0), resonator=None), Transmon(id=1, xy=IQChannel(operations={}, id=None, opx_output_I=('con1', 6), opx_output_Q=('con1', 7), opx_output_offset_I=0.0, opx_output_offset_Q=0.0, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0), z=SingleChannel(operations={}, id=None, opx_output=('con1', 8), filter_fir_taps=None, filter_iir_taps=None, output_offset=0), resonator=None)], resonators=[InOutIQChannel(operations={}, id=0, opx_output_I=('con1', 1), opx_output_Q=('con1', 2), opx_output_offset_I=0.0, opx_output_offset_Q=0.0, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0, opx_input_I=('con1', 1), opx_input_Q=('con1', 2), time_of_flight=24, smearing=0, input_offset_I=0.0, input_offset_Q=0.0, input_gain=None, frequency_converter_down=None), InOutIQChannel(operations={}, id=1, opx_output_I=('con1', 1), opx_output_Q=('con1', 2), opx_output_offset_I=0.0, opx_output_offset_Q=0.0, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0, opx_input_I=('con1', 1), opx_input_Q=('con1', 2), time_of_flight=24, smearing=0, input_offset_I=0.0, input_offset_Q=0.0, input_gain=None, frequency_converter_down=None)], local_oscillators=[], wiring={})" + "QuAM(mixers=[], qubits=[Transmon(id=0, xy=IQChannel(operations={}, id=None, digital_outputs={}, opx_output_I=('con1', 3), opx_output_Q=('con1', 4), opx_output_offset_I=None, opx_output_offset_Q=None, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0), z=SingleChannel(operations={}, id=None, digital_outputs={}, opx_output=('con1', 5), filter_fir_taps=None, filter_iir_taps=None, opx_output_offset=None, intermediate_frequency=None), resonator=None), Transmon(id=1, xy=IQChannel(operations={}, id=None, digital_outputs={}, opx_output_I=('con1', 6), opx_output_Q=('con1', 7), opx_output_offset_I=None, opx_output_offset_Q=None, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0), z=SingleChannel(operations={}, id=None, digital_outputs={}, opx_output=('con1', 8), filter_fir_taps=None, filter_iir_taps=None, opx_output_offset=None, intermediate_frequency=None), resonator=None)], resonators=[InOutIQChannel(operations={}, id=0, digital_outputs={}, opx_output_I=('con1', 1), opx_output_Q=('con1', 2), opx_output_offset_I=None, opx_output_offset_Q=None, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0, opx_input_I=('con1', 1), opx_input_Q=('con1', 2), time_of_flight=24, smearing=0, opx_input_offset_I=None, opx_input_offset_Q=None, input_gain=None, frequency_converter_down=None), InOutIQChannel(operations={}, id=1, digital_outputs={}, opx_output_I=('con1', 1), opx_output_Q=('con1', 2), opx_output_offset_I=None, opx_output_offset_Q=None, frequency_converter_up=FrequencyConverter(local_oscillator=LocalOscillator(frequency=6000000000.0, power=10), mixer=Mixer(local_oscillator_frequency=6000000000.0, intermediate_frequency=0.0, correction_gain=0, correction_phase=0), gain=None), intermediate_frequency=0.0, opx_input_I=('con1', 1), opx_input_Q=('con1', 2), time_of_flight=24, smearing=0, opx_input_offset_I=None, opx_input_offset_Q=None, input_gain=None, frequency_converter_down=None)], local_oscillators=[], wiring={})" ] }, "execution_count": 3, @@ -126,6 +118,13 @@ "machine" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A summary of QuAM can be shown using `machine.print_summary()`" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/mkdocs.yml b/mkdocs.yml index 5a18f3eb..7b04fd0e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ nav: - quam-references.md - "QuAM components": - "components/channels.md" + - "components/octave.md" - custom-components.md plugins: diff --git a/pyproject.toml b/pyproject.toml index 514c3276..6d9ee47c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "quam" -version = "0.2.2" +version = "0.3.0" description = "Quantum Abstract Machine (QuAM) facilitates development of abstraction layers in experiments." authors = [ { name = "Serwan Asaad", email = "serwan.asaad@quantum-machines.co" }, diff --git a/quam/components/channels.py b/quam/components/channels.py index 4cb0beb7..7152a269 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -1,9 +1,11 @@ from dataclasses import field from typing import ClassVar, Dict, List, Optional, Sequence, Tuple, Union +import warnings -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.core.quam_classes import QuamDict from quam.utils import string_reference as str_ref @@ -152,7 +154,17 @@ def name(self) -> str: raise AttributeError( f"{cls_name}.name cannot be determined. " f"Please either set {cls_name}.id to a string or integer, " - f"or {cls_name} should be an attribute of another QuAM component." + f"or {cls_name} should be an attribute of another QuAM component with " + "a name." + ) + if isinstance(self.parent, QuamDict): + return self.parent.get_attr_name(self) + if not hasattr(self.parent, "name"): + raise AttributeError( + f"{cls_name}.name cannot be determined. " + f"Please either set {cls_name}.id to a string or integer, " + f"or {cls_name} should be an attribute of another QuAM component with " + "a name." ) return f"{self.parent.name}{str_ref.DELIMITER}{self.parent.get_attr_name(self)}" @@ -429,20 +441,21 @@ def apply_to_config(self, config: dict): offset = self.opx_output_offset if offset is not None: if abs(analog_output.get("offset", offset) - offset) > 1e-4: - raise ValueError( + warnings.warn( f"Channel {self.name} has conflicting output offsets: " - f"{analog_output['offset']} and {offset}. Multiple channel " - f"elements are trying to set different offsets to port {port}" + f"{analog_output['offset']} V and {offset} V. Multiple channel " + f"elements are trying to set different offsets to port {port}. " + f"Using the last offset {offset} V" ) analog_output["offset"] = offset if self.filter_fir_taps is not None: output_filter = analog_output.setdefault("filter", {}) - output_filter["feedforward"] = self.filter_fir_taps + output_filter["feedforward"] = list(self.filter_fir_taps) if self.filter_iir_taps is not None: output_filter = analog_output.setdefault("filter", {}) - output_filter["feedback"] = self.filter_iir_taps + output_filter["feedback"] = list(self.filter_iir_taps) @quam_dataclass @@ -749,27 +762,23 @@ 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): - if self.frequency_converter_up is None: - return None - 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): - if self.frequency_converter_up is None: - return None - return self.frequency_converter_up.mixer + def mixer(self) -> Optional[Mixer]: + return getattr(self.frequency_converter_up, "mixer", None) @property def rf_frequency(self): - return self.local_oscillator.frequency + self.intermediate_frequency + return self.frequency_converter_up.LO_frequency + self.intermediate_frequency def apply_to_config(self, config: dict): """Adds this IQChannel to the QUA configuration. @@ -791,12 +800,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] @@ -857,7 +889,7 @@ class InOutIQChannel(IQChannel): input_gain: Optional[float] = None - frequency_converter_down: FrequencyConverter = None + frequency_converter_down: BaseFrequencyConverter = None _default_label: ClassVar[str] = "IQ" @@ -874,13 +906,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) @@ -889,10 +942,11 @@ def apply_to_config(self, config: dict): # If no offset specified, it will be added at the end of config generation if offset is not None: if abs(analog_input.get("offset", offset) - offset) > 1e-4: - raise ValueError( + warnings.warn( f"Channel {self.name} has conflicting input offsets: " - f"{analog_input['offset']} and {offset}. Multiple channel " - f"elements are trying to set different offsets to port {port}" + f"{analog_input['offset']} V and {offset} V. Multiple channel " + f"elements are trying to set different offsets to port {port}. " + f"Using the last offset {offset} V" ) analog_input["offset"] = offset diff --git a/quam/components/hardware.py b/quam/components/hardware.py index ca416e6d..cc989c07 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,11 +108,22 @@ 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 + @property + def LO_frequency(self): + return self.local_oscillator.frequency + def configure(self): if self.local_oscillator is not None: self.local_oscillator.configure() diff --git a/quam/components/octave.py b/quam/components/octave.py index af0cefd1..4b2debdf 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -1,20 +1,394 @@ +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()` + calibration_db_path: The path to the calibration database. If not specified, the + current working directory is used. + 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 + calibration_db_path: str = None + + 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_frequency_converters(self): + """Initialize the Octave frequency converterswith 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() + + if self.calibration_db_path is not None: + octave_config.set_calibration_db(self.calibration_db_path) + else: + octave_config.set_calibration_db(os.getcwd()) + + 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. -__all__ = ["OctaveOldFrequencyConverter", "OctaveOld"] + 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": list(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. + + Nothing is added to the config if the `OctaveUpConverter.channel` is not + specified or if the `OctaveUpConverter.LO_frequency` is not specified. + + Args: + config: A dictionary representing a QUA config file. + + Raises: + ValueError: If the LO_frequency is not specified. + 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. + """ + if not isinstance(self.LO_frequency, (int, float)): + if self.channel is None: + return + else: + raise ValueError( + f"Error generating config for Octave upconverter id={self.id}: " + "LO_frequency must be specified." + ) + + 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}"' + ) + + 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. + + Nothing is added to the config if the `OctaveDownConverter.channel` is not + specified or if the `OctaveDownConverter.LO_frequency` is not specified. + + Args: + config: A dictionary representing a QUA config file. + + Raises: + ValueError: If the LO_frequency is not specified. + 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. + """ + if not isinstance(self.LO_frequency, (int, float)): + if self.channel is None: + return + else: + raise ValueError( + f"Error generating config for Octave upconverter id={self.id}: " + "LO_frequency must be specified." + ) + + 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 @@ -24,6 +398,7 @@ class OctaveOld(QuamComponent): port: int qmm_host: str qmm_port: int + connection_headers: Dict[str, str] = None calibration_db: str = None @@ -49,7 +424,10 @@ def _initialize_config(self): def _initialize_qm(self) -> QuantumMachine: qmm = QuantumMachinesManager( - host=self.qmm_host, port=self.qmm_port, octave=self.octave_config + host=self.qmm_host, + port=self.qmm_port, + octave=self.octave_config, + connection_headers=self.connection_headers, ) qm = qmm.open_qm(self._root.generate_config()) return qm @@ -74,6 +452,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) @@ -81,7 +461,7 @@ def configure_octave_settings(self): for elem in self._root.iterate_components(): if not isinstance(elem, InOutIQChannel): continue - if getattr(elem.frequency_converter_down, "octave") is not self: + if getattr(elem.frequency_converter_down, "octave", None) is not self: continue self.octave.set_qua_element_octave_rf_in_port(elem.name, self.name, 1) 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 dbfc6910..ab6dfb93 100644 --- a/quam/core/quam_classes.py +++ b/quam/core/quam_classes.py @@ -73,7 +73,7 @@ def convert_dict_and_list(value, cls_or_obj=None, attr=None): """Convert a dict or list to a QuamDict or QuamList if possible.""" if isinstance(value, dict): value_annotation = _get_value_annotation(cls_or_obj=cls_or_obj, attr=attr) - return QuamDict(**value, value_annotation=value_annotation) + return QuamDict(value, value_annotation=value_annotation) elif type(value) == list: value_annotation = _get_value_annotation(cls_or_obj=cls_or_obj, attr=attr) return QuamList(value, value_annotation=value_annotation) @@ -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 @@ -787,6 +801,28 @@ def get_attrs( # TODO implement reference kwargs return self.data + def get_attr_name(self, attr_val: Any) -> Union[str, int]: + """Get the name of an attribute that matches the value. + + Args: + attr_val: The value of the attribute. + + Returns: + The name of the attribute. This can also be an int depending on the dict key + + Raises: + AttributeError if not found. + """ + for attr_name in self._get_attr_names(): + if attr_name in self and self[attr_name] is attr_val: + return attr_name + else: + raise AttributeError( + "Could not find name corresponding to attribute.\n" + f"attribute: {attr_val}\n" + f"obj: {self}" + ) + def _val_matches_attr_annotation(self, attr: str, val: Any) -> bool: """Check whether the type of an attribute matches the annotation. 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/quam/examples/superconducting_qubits/__init__.py b/quam/examples/superconducting_qubits/__init__.py index e69de29b..dfc3c948 100644 --- a/quam/examples/superconducting_qubits/__init__.py +++ b/quam/examples/superconducting_qubits/__init__.py @@ -0,0 +1 @@ +from .components import * diff --git a/quam/utils/string_reference.py b/quam/utils/string_reference.py index 9240598e..6a7f65b5 100644 --- a/quam/utils/string_reference.py +++ b/quam/utils/string_reference.py @@ -79,10 +79,12 @@ def get_relative_reference_value(obj, string: str) -> Any: except KeyError as e: raise AttributeError(f"Object {obj} has no attribute {next_attr}") from e elif isinstance(obj, (dict, UserDict)): - try: + if next_attr in obj: obj_attr = obj[next_attr] - except KeyError as e: - raise AttributeError(f"Object {obj} has no attribute {next_attr}") from e + elif next_attr.isdigit() and int(next_attr) in obj: + obj_attr = obj[int(next_attr)] + else: + raise AttributeError(f"Object {obj} has no attribute {next_attr}") else: obj_attr = getattr(obj, next_attr) diff --git a/tests/components/channels/test_in_out_IQ_channel.py b/tests/components/channels/test_in_out_IQ_channel.py index 843e6742..65caa52f 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/channels/test_single_channel.py b/tests/components/channels/test_single_channel.py index 5fc67973..a6aeefbc 100644 --- a/tests/components/channels/test_single_channel.py +++ b/tests/components/channels/test_single_channel.py @@ -81,7 +81,7 @@ def test_single_channel_differing_offsets(bare_cfg): cfg = deepcopy(bare_cfg) channel1.apply_to_config(cfg) - with pytest.raises(ValueError): + with pytest.warns(UserWarning): channel2.apply_to_config(cfg) cfg = deepcopy(bare_cfg) diff --git a/tests/components/pulses/test_pulses.py b/tests/components/pulses/test_pulses.py index eb86dd74..35ae069a 100644 --- a/tests/components/pulses/test_pulses.py +++ b/tests/components/pulses/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/components/test_octave.py b/tests/components/test_octave.py new file mode 100644 index 00000000..45414db7 --- /dev/null +++ b/tests/components/test_octave.py @@ -0,0 +1,403 @@ +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_frequency_converters() + + 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_frequency_converters() + + 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() + + +def test_frequency_converter_config_no_LO_frequency(octave): + cfg = {} + converter = octave.RF_outputs[1] = OctaveUpConverter(id=1) + octave.apply_to_config(config=cfg) + + converter.apply_to_config(cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "RF_outputs": {}, + "RF_inputs": {}, + "IF_outputs": {}, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg + + converter.channel = "something" + + with pytest.raises(ValueError): + converter.apply_to_config(cfg) + + converter.channel = None + converter.LO_frequency = 2e9 + + converter.apply_to_config(cfg) + + expected_cfg = { + "octaves": { + "octave1": { + "IF_outputs": {}, + "RF_inputs": {}, + "RF_outputs": { + 1: { + "LO_frequency": 2000000000.0, + "LO_source": "internal", + "gain": 0, + "input_attenuators": "off", + "output_mode": "always_off", + } + }, + "loopbacks": [], + } + } + } + assert cfg == expected_cfg 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/quam_base/referencing/test_referencing_dict.py b/tests/quam_base/referencing/test_referencing_dict.py index 899006ae..1f9a2d5d 100644 --- a/tests/quam_base/referencing/test_referencing_dict.py +++ b/tests/quam_base/referencing/test_referencing_dict.py @@ -46,3 +46,17 @@ def test_referencing_to_dict(): assert quam_root._get_referenced_value("#/quam_dict/a") == 44 assert quam_root.quam_dict._get_referenced_value("#/quam_dict/a") == 44 assert quam_root.quam_dict._get_referenced_value("#./a") == 44 + + +def test_referencing_dict_int_keys(): + quam_root = BareQuamRoot() + quam_root.quam_dict = {"1": 1, 2: 2} + + assert list(quam_root.quam_dict.keys()) == list(quam_root.quam_dict) + assert list(quam_root.quam_dict) == ["1", 2] + + assert quam_root._get_referenced_value("#/quam_dict/1") == 1 + assert quam_root.quam_dict._get_referenced_value("#/quam_dict/1") == 1 + + assert quam_root._get_referenced_value("#/quam_dict/2") == 2 + assert quam_root.quam_dict._get_referenced_value("#/quam_dict/2") == 2 diff --git a/tests/quam_base/test_quam_dict.py b/tests/quam_base/test_quam_dict.py index 78e2c4ef..b7fdea22 100644 --- a/tests/quam_base/test_quam_dict.py +++ b/tests/quam_base/test_quam_dict.py @@ -201,3 +201,31 @@ def test_dict_unreferenced_value(): assert d.val1 == 42 assert d.val2 == 42 assert d.get_unreferenced_value("val1") == "#./val2" + + +def test_quam_dict_int_keys(): + quam_dict = QuamDict({1: 2}) + assert quam_dict.data == {1: 2} + assert quam_dict[1] == 2 + quam_dict.pop(1) + assert quam_dict.data == {} + with pytest.raises(KeyError): + quam_dict[1] + + +def test_quam_dict_get_attr_int(): + quam_dict = QuamDict({1: 2}) + assert quam_dict.get_attr_name(2) == 1 + + +def test_quam_dict_print_summary(): + quam_dict = QuamDict({"a": "b", 1: 2}) + + from contextlib import redirect_stdout + import io + + f = io.StringIO() + with redirect_stdout(f): + quam_dict.print_summary() + s = f.getvalue() + assert s == 'QuamDict (parent unknown):\n a: "b"\n 1: 2\n' 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"])