diff --git a/CHANGELOG.md b/CHANGELOG.md index 5563f014..2e2818d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ ## [Unreleased] +### Added +- Added `QuamBase.set_at_reference` to set a value at a reference +- Added `string_reference.get_parent_reference` to get the parent reference of a string reference + ### Changed - `Pulse.integration_weights` now defaults to `#./default_integration_weights`, which returns [(1, pulse.length)] + ## [0.3.8] ### Added - Added time tagging to channels diff --git a/quam/core/quam_classes.py b/quam/core/quam_classes.py index 296c3e91..db2eb46a 100644 --- a/quam/core/quam_classes.py +++ b/quam/core/quam_classes.py @@ -542,6 +542,42 @@ def print_summary(self, indent: int = 0): else: print(" " * (indent + 2) + f"{attr}: {val}") + def set_at_reference(self, attr: str, value: Any): + """Follow the reference of an attribute and set the value at the reference + + Args: + attr: The attribute to set the value at the reference of. + value: The value to set. + + Raises: + ValueError: If the attribute is not a reference. + ValueError: If the reference is invalid, e.g. "#./" since it has no + attribute. + """ + raw_value = self.get_unreferenced_value(attr) + if not string_reference.is_reference(raw_value): + raise ValueError( + f"Cannot set at reference because attr '{attr}' is not a reference. " + f"'{attr}' = {raw_value}" + ) + + parent_reference, ref_attr = string_reference.split_reference(raw_value) + if not ref_attr: + raise ValueError( + f"Unsuccessful attempt to set the value at reference {raw_value} for " + f"attribute {attr} because the reference is invalid as it has no " + "attribute" + ) + + parent_obj = self._get_referenced_value(parent_reference) + raw_referenced_value = parent_obj.get_unreferenced_value(ref_attr) + if string_reference.is_reference(raw_referenced_value) and isinstance( + parent_obj, QuamBase + ): + parent_obj.set_at_reference(ref_attr, value) + else: + setattr(parent_obj, ref_attr, value) + # Type annotation for QuamRoot, can be replaced by typing.Self from Python 3.11 QuamRootType = TypeVar("QuamRootType", bound="QuamRoot") @@ -682,8 +718,8 @@ def generate_config(self) -> Dict[str, Any]: return qua_config - def get_unreferenced_value(self, attr: str): - return getattr(self, attr) + # def get_unreferenced_value(self, attr: str): + # return getattr(self, attr) class QuamComponent(QuamBase): diff --git a/quam/utils/reference_class.py b/quam/utils/reference_class.py index 58bf0de6..9a624a0e 100644 --- a/quam/utils/reference_class.py +++ b/quam/utils/reference_class.py @@ -27,8 +27,8 @@ def _is_reference(self, attr: str) -> bool: """ raise NotImplementedError - def get_unreferenced_value(self, attr: str) -> bool: - """Check if an attribute is a reference""" + def get_unreferenced_value(self, attr: str) -> Any: + """Get the raw value of an attribute, returning the reference if it is one""" return super().__getattribute__(attr) def __getattribute__(self, attr: str) -> Any: diff --git a/quam/utils/string_reference.py b/quam/utils/string_reference.py index 6a7f65b5..fd703e5e 100644 --- a/quam/utils/string_reference.py +++ b/quam/utils/string_reference.py @@ -118,3 +118,36 @@ def get_referenced_value(obj, string: str, root=None) -> Any: return get_relative_reference_value(obj, string) except (AttributeError, KeyError) as e: raise ValueError(f"String {string} is not a valid reference, Error: {e}") from e + + +def split_reference(string: str) -> Tuple[str, str]: + """Split a string reference into its parent reference and attribute + + Args: + string: The reference string + + Returns: + A tuple containing the parent reference string and the attribute + + Raises: + ValueError: If the string is not a valid reference + ValueError: If the string equals "#/" + + Examples: + split_reference("#/a/b/c") == ("#/a/b", "c") + split_reference("#/a/b") == ("#/a", "b") + split_reference("#/a") == ("#/", "a") + """ + if not is_reference(string): + raise ValueError(f"String {string} is not a reference") + if string == "#/": + raise ValueError(f"String {string} has no parent") + if string == "#./": + return "#../", "" + if string == "#../": + return "#../../", "" + + parent_reference, attr = string.rsplit("/", 1) + if parent_reference in ("#", "#.", "#.."): + parent_reference += "/" + return parent_reference, attr diff --git a/tests/quam_base/test_set_at_reference.py b/tests/quam_base/test_set_at_reference.py new file mode 100644 index 00000000..e422a3d0 --- /dev/null +++ b/tests/quam_base/test_set_at_reference.py @@ -0,0 +1,157 @@ +import pytest +from quam.core.quam_classes import QuamBase, QuamRoot, quam_dataclass +from typing import Optional + + +@quam_dataclass +class ChildQuam(QuamBase): + value: int = 0 + + +@quam_dataclass +class ParentQuam(QuamBase): + child: ChildQuam = None + ref_value: str = "#./child/value" + normal_value: int = 42 + + +@quam_dataclass +class RootQuam(QuamRoot): + parent: ParentQuam = None + abs_ref: str = "#/parent/child/value" + + +def test_set_at_reference(): + """Test setting a value through a reference""" + parent = ParentQuam(child=ChildQuam()) + + # Set value through reference + parent.set_at_reference("ref_value", 123) + + # Check that the value was set correctly + assert parent.child.value == 123 + # Reference string should remain unchanged + assert parent.get_unreferenced_value("ref_value") == "#./child/value" + + +def test_set_at_reference_non_reference(): + """Test that setting a non-reference attribute raises ValueError""" + parent = ParentQuam(child=ChildQuam()) + + with pytest.raises(ValueError, match="is not a reference"): + parent.set_at_reference("normal_value", 123) + + +def test_set_at_reference_invalid_reference(): + """Test handling of invalid references""" + parent = ParentQuam(child=ChildQuam()) + parent.ref_value = "#./nonexistent/value" + + with pytest.raises(AttributeError): + parent.set_at_reference("ref_value", 123) + + +def test_unreferenced_value(): + root = RootQuam(parent=ParentQuam(child=ChildQuam())) + assert root.get_unreferenced_value("abs_ref") == "#/parent/child/value" + assert root.parent.get_unreferenced_value("ref_value") == "#./child/value" + + +def test_set_at_absolute_reference(): + """Test setting a value through an absolute reference""" + root = RootQuam(parent=ParentQuam(child=ChildQuam())) + + # Set value through absolute reference + root.set_at_reference("abs_ref", 456) + + # Check that the value was set correctly + assert root.parent.child.value == 456 + # Reference string should remain unchanged + assert root.get_unreferenced_value("abs_ref") == "#/parent/child/value" + + +def test_set_at_absolute_reference_invalid(): + """Test handling of invalid absolute references""" + root = RootQuam(parent=ParentQuam(child=ChildQuam())) + root.abs_ref = "#/nonexistent/path" + + with pytest.raises(AttributeError): + root.set_at_reference("abs_ref", 456) + + +@quam_dataclass +class DoubleChildQuam(ChildQuam): + value: int = 0 + child: Optional[ChildQuam] = None + +def test_set_double_reference(): + """Test setting a value through a double reference""" + double_child = DoubleChildQuam(child=ChildQuam(value=42), value="#./child/value") + parent = ParentQuam(child=double_child, ref_value="#./child/value") + + assert parent.ref_value == 42 + assert parent.get_unreferenced_value("ref_value") == "#./child/value" + assert parent.child.get_unreferenced_value("value") == "#./child/value" + + # Set value through double reference + parent.set_at_reference("ref_value", 789) + + # Check that the value was set correctly in the nested child + assert double_child.child.value == 789 + assert double_child.value == 789 + assert parent.ref_value == 789 + + # Reference string should remain unchanged + assert parent.get_unreferenced_value("ref_value") == "#./child/value" + assert double_child.get_unreferenced_value("value") == "#./child/value" + + +def test_set_nonexistent_double_reference(): + """Test setting a value where the double reference does not exist""" + double_child = DoubleChildQuam(child=ChildQuam(value=42), value="#./child/nonexistent") + parent = ParentQuam(child=double_child, ref_value="#./child/nonexistent") + + with pytest.raises(AttributeError): + parent.set_at_reference("ref_value", 789) + + +def test_set_double_reference_to_nonexistent_item(): + """Test setting a value through a double reference to a nonexistent item""" + double_child = DoubleChildQuam(child=ChildQuam(value=42), value="#./nonexistent/value") + parent = ParentQuam(child=double_child, ref_value="#./nonexistent/value") + + with pytest.raises(AttributeError): + parent.set_at_reference("ref_value", 789) + + +def test_set_double_reference_with_invalid_reference(): + """Test setting a value through a double reference with an invalid reference""" + double_child = DoubleChildQuam(child=ChildQuam(value=42), value="#./child/invalid") + parent = ParentQuam(child=double_child, ref_value="#./child/invalid") + + with pytest.raises(AttributeError): + parent.set_at_reference("ref_value", 789) + +def test_set_triple_reference(): + """Test setting a value through a triple reference""" + triple_child = DoubleChildQuam(child=DoubleChildQuam(child=ChildQuam(value=42), value="#./child/value"), value="#./child/value") + parent = ParentQuam(child=triple_child, ref_value="#./child/value") + + assert parent.ref_value == 42 + assert parent.get_unreferenced_value("ref_value") == "#./child/value" + assert parent.child.get_unreferenced_value("value") == "#./child/value" + assert parent.child.child.get_unreferenced_value("value") == "#./child/value" + + # Set value through triple reference + parent.set_at_reference("ref_value", 789) + + # Check that the value was set correctly in the nested child + assert triple_child.child.child.value == 789 + assert triple_child.child.value == 789 + assert triple_child.value == 789 + assert parent.ref_value == 789 + + # Reference string should remain unchanged + assert parent.get_unreferenced_value("ref_value") == "#./child/value" + assert triple_child.get_unreferenced_value("value") == "#./child/value" + assert triple_child.child.get_unreferenced_value("value") == "#./child/value" diff --git a/tests/utils/test_string_reference.py b/tests/utils/test_string_reference.py index 78c582f5..04b2f59a 100644 --- a/tests/utils/test_string_reference.py +++ b/tests/utils/test_string_reference.py @@ -147,3 +147,53 @@ def test_delimiter(): assert transmon.xy.name == "q1$xy" finally: quam.utils.string_reference.DELIMITER = "." + + +def test_get_parent_reference_absolute(): + parent, attr = split_reference("#/a/b") + assert parent == "#/a" + assert attr == "b" + + parent, attr = split_reference("#/a/b/c") + assert parent == "#/a/b" + assert attr == "c" + + parent, attr = split_reference("#/a") + assert parent == "#/" + assert attr == "a" + + with pytest.raises(ValueError): + split_reference("#/") + + +def test_get_parent_reference_relative(): + parent, attr = split_reference("#./a/b") + assert parent == "#./a" + assert attr == "b" + + parent, attr = split_reference("#../a/b") + assert parent == "#../a" + assert attr == "b" + + parent, attr = split_reference("#./a") + assert parent == "#./" + assert attr == "a" + + parent, attr = split_reference("#../a") + assert parent == "#../" + assert attr == "a" + + parent, attr = split_reference("#./") + assert parent == "#../" + assert attr == "" + + parent, attr = split_reference("#../") + assert parent == "#../../" + assert attr == "" + + +def test_get_parent_reference_invalid(): + with pytest.raises(ValueError): + split_reference("a") + with pytest.raises(ValueError): + split_reference("#")