diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f46e16..12186a34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ ### Changed - Allow `QuamBase.get_reference(attr)` to return a reference of one of its attributes +### Fixed +- Fix quam object instantiation error when a parameter type uses pipe operator +- Allow int keys to be serialised / loaded in QuAM using JSONSerialiser + + ## [0.3.3] ### Added - Added the following parameters to `IQChannel`: `RF_frequency`, `LO_frequency`, `intermediate_frequency` diff --git a/docs/installation.md b/docs/installation.md index fab721f9..31bdf4e7 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,7 +4,7 @@ /// tab | For Windows - Windows 10 (build 1809 and later), or Windows 11 -- Python 3.9 or higher, we recommend Python 3.10 or higher +- 3.8 ≤ Python ≤ 3.11, we recommend Python 3.10 or 3.11 - [Git version control system](https://git-scm.com/), or a Git GUI such as [GitHub Desktop](https://desktop.github.com/) or [GitKraken](https://www.gitkraken.com/) diff --git a/quam/core/quam_instantiation.py b/quam/core/quam_instantiation.py index 57680cf4..e1bd0720 100644 --- a/quam/core/quam_instantiation.py +++ b/quam/core/quam_instantiation.py @@ -1,4 +1,6 @@ from __future__ import annotations +import sys +import types import typing from typing import TYPE_CHECKING, Dict, Any from inspect import isclass @@ -16,6 +18,12 @@ from quam.core import QuamBase +if sys.version_info < (3, 10): + union_types = (typing.Union,) +else: + union_types = [typing.Union, types.UnionType] + + def instantiate_attrs_from_dict( attr_dict: dict, required_type: type, @@ -224,7 +232,7 @@ def instantiate_attr( ) if typing.get_origin(expected_type) == dict: expected_type = dict - elif typing.get_origin(expected_type) == typing.Union: + elif typing.get_origin(expected_type) in union_types: for union_type in typing.get_args(expected_type): try: instantiated_attr = instantiate_attr( diff --git a/quam/serialisation/json.py b/quam/serialisation/json.py index 3d7b9306..e2d56655 100644 --- a/quam/serialisation/json.py +++ b/quam/serialisation/json.py @@ -166,7 +166,7 @@ def load( metadata["default_filename"] = path.name with open(path, "r") as f: - contents = json.load(f) + contents = json.load(f, object_hook=convert_int_keys) elif path.is_dir(): metadata["default_foldername"] = str(path) for file in path.iterdir(): @@ -183,3 +183,17 @@ def load( metadata["content_mapping"][file.name] = list(file_contents.keys()) return contents, metadata + + +def convert_int_keys(obj): + """Convert dictionary keys to integers if possible.""" + if not isinstance(obj, dict): + return obj + + new_obj = {} + for key, value in obj.items(): + if key.isdigit(): + key = int(key) + new_obj[key] = value + + return new_obj diff --git a/quam/utils/config.py b/quam/utils/config.py index e46fdaef..8f5619cc 100644 --- a/quam/utils/config.py +++ b/quam/utils/config.py @@ -10,7 +10,16 @@ def generate_config_final_actions(qua_config): Args: qua_config (dict): The generated qua config. """ + # Add default dc offset 0V to all analog outputs and inputs if not set for controller_cfg in qua_config["controllers"].values(): + for fem in controller_cfg.get("fems", {}).values(): + if fem.get("type") != "LF": + continue + for analog_output in fem.get("analog_outputs", {}).values(): + analog_output.setdefault("offset", 0.0) + for analog_input in fem.get("analog_inputs", {}).values(): + analog_input.setdefault("offset", 0.0) + if "analog_outputs" in controller_cfg: for analog_output in controller_cfg["analog_outputs"].values(): analog_output.setdefault("offset", 0.0) diff --git a/tests/config/test_config_final_actions.py b/tests/config/test_config_final_actions.py new file mode 100644 index 00000000..e7d0f538 --- /dev/null +++ b/tests/config/test_config_final_actions.py @@ -0,0 +1,139 @@ +from quam.utils.config import generate_config_final_actions + + +def test_config_no_overwrite_existing_offset(): + cfg = { + "controllers": { + "con1": { + "analog_outputs": {1: {"offset": 0.5}}, + "analog_inputs": {2: {"offset": 0.5}}, + }, + }, + } + + generate_config_final_actions(cfg) + + assert cfg == { + "controllers": { + "con1": { + "analog_outputs": {1: {"offset": 0.5}}, + "analog_inputs": {2: {"offset": 0.5}}, + }, + }, + } + + +def test_config_default_offset(): + cfg = { + "controllers": { + "con1": { + "analog_outputs": {1: {}}, + "analog_inputs": {2: {}}, + }, + }, + } + + generate_config_final_actions(cfg) + + assert cfg == { + "controllers": { + "con1": { + "analog_outputs": {1: {"offset": 0.0}}, + "analog_inputs": {2: {"offset": 0.0}}, + }, + }, + } + + +def test_config_default_offset_LF_FEM(): + cfg = { + "controllers": { + "con1": { + "fems": { + 2: { + "type": "LF", + "analog_outputs": {1: {}}, + "analog_inputs": {2: {}}, + }, + }, + }, + }, + } + + generate_config_final_actions(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 2: { + "type": "LF", + "analog_outputs": {1: {"offset": 0.0}}, + "analog_inputs": {2: {"offset": 0.0}}, + }, + }, + }, + }, + } + + +def test_config_no_overwrite_existing_offset_LF_FEM(): + cfg = { + "controllers": { + "con1": { + "fems": { + 2: { + "type": "LF", + "analog_outputs": {1: {"offset": 0.5}}, + "analog_inputs": {2: {"offset": 0.5}}, + }, + }, + }, + }, + } + + generate_config_final_actions(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 2: { + "type": "LF", + "analog_outputs": {1: {"offset": 0.5}}, + "analog_inputs": {2: {"offset": 0.5}}, + }, + }, + }, + }, + } + + +def test_config_default_offset_no_outputs_inputs_entries(): + cfg = {"controllers": {"con1": {}}} + + generate_config_final_actions(cfg) + + assert cfg == {"controllers": {"con1": {}}} + + +def test_config_default_offset_no_outputs_inputs(): + cfg = { + "controllers": { + "con1": { + "analog_outputs": {}, + "analog_inputs": {}, + } + } + } + + generate_config_final_actions(cfg) + + assert cfg == { + "controllers": { + "con1": { + "analog_outputs": {}, + "analog_inputs": {}, + } + } + } diff --git a/tests/instantiation/test_instantiation.py b/tests/instantiation/test_instantiation.py index 7aa293ff..f6c0da63 100644 --- a/tests/instantiation/test_instantiation.py +++ b/tests/instantiation/test_instantiation.py @@ -1,6 +1,8 @@ import pytest from typing import List, Literal, Optional, Tuple, Union +from pytest_cov.engine import sys + from quam.core import QuamRoot, QuamComponent, quam_dataclass from quam.core.quam_classes import QuamDict from quam.examples.superconducting_qubits.components import Transmon @@ -339,6 +341,7 @@ def test_instantiate_dict_referenced(): assert attrs == {"test_attr": "#./reference"} + @quam_dataclass class TestQuamComponent(QuamComponent): a: int @@ -357,3 +360,19 @@ class TestQuamUnion(QuamComponent): with pytest.raises(TypeError): instantiate_quam_class(TestQuamUnion, {"union_val": {"a": "42"}}) + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") +def test_instantiation_pipe_union_type(): + @quam_dataclass + class TestQuamUnion(QuamComponent): + union_val: int | TestQuamComponent + + obj = instantiate_quam_class(TestQuamUnion, {"union_val": 42}) + assert obj.union_val == 42 + + obj = instantiate_quam_class(TestQuamUnion, {"union_val": {"a": 42}}) + assert obj.union_val.a == 42 + + with pytest.raises(TypeError): + instantiate_quam_class(TestQuamUnion, {"union_val": {"a": "42"}}) diff --git a/tests/serialisation/test_json_serialisation.py b/tests/serialisation/test_json_serialisation.py index b7695e5f..167f4c4d 100644 --- a/tests/serialisation/test_json_serialisation.py +++ b/tests/serialisation/test_json_serialisation.py @@ -1,4 +1,5 @@ import json +from typing import Dict import pytest from quam.serialisation import JSONSerialiser @@ -107,3 +108,28 @@ def test_component_mamping_ignore(tmp_path): "a": 4, } } + + +@quam_dataclass +class QuAMWithIntDict(QuamRoot): + a: int + d: Dict[int, str] + + +def test_serialise_int_dict_keys(tmp_path): + quam_root = QuAMWithIntDict(a=1, d={1: "a", 2: "b"}) + + serialiser = JSONSerialiser() + path = tmp_path / "quam_root.json" + serialiser.save(quam_root, path) + + d, _ = serialiser.load(path) + + assert d == { + "a": 1, + "d": { + 1: "a", + 2: "b", + }, + "__class__": "test_json_serialisation.QuAMWithIntDict", + }