Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into fix/quam-root-referen…
Browse files Browse the repository at this point in the history
…cing
  • Loading branch information
nulinspiratie committed Dec 12, 2024
2 parents 80dcca5 + 31dff5d commit 7c30b2a
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 19 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
## [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)]

### Fixed
- Fixed issues with parameters being references in a QuamRoot object


## [0.3.8]
### Added
- Added time tagging to channels
Expand Down
12 changes: 8 additions & 4 deletions quam/components/pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,17 +460,21 @@ class ReadoutPulse(BaseReadoutPulse, ABC):
integration weights in radians.
"""

integration_weights: Union[List[float], List[Tuple[float, int]]] = None
integration_weights: Union[List[float], List[Tuple[float, int]]] = (
"#./default_integration_weights"
)
integration_weights_angle: float = 0

@property
def default_integration_weights(self) -> List[Tuple[float, int]]:
return [(1, self.length)]

def integration_weights_function(self) -> List[Tuple[Union[complex, float], int]]:
from qualang_tools.config import convert_integration_weights

phase = np.exp(1j * self.integration_weights_angle)

if self.integration_weights is None or not len(self.integration_weights):
integration_weights = [(1, self.length)]
elif isinstance(self.integration_weights[0], float):
if isinstance(self.integration_weights[0], float):
integration_weights = convert_integration_weights(self.integration_weights)
else:
integration_weights = self.integration_weights
Expand Down
36 changes: 36 additions & 0 deletions quam/core/quam_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions quam/utils/reference_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 33 additions & 0 deletions quam/utils/string_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 15 additions & 13 deletions tests/components/pulses/test_pulse_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,22 @@ def test_constant_readout_pulse_integration_weights_default():
compare_integration_weights(expected_weights, weights)


def test_default_integration_weights():
pulse = pulses.SquareReadoutPulse(length=100, amplitude=1)
assert pulse.default_integration_weights == [(1, 100)]


def test_empty_integration_weights():
for weights in ([], np.array([]), None):
pulse = pulses.SquareReadoutPulse(
length=100, amplitude=1, integration_weights=weights
)

weights = pulse.integration_weights_function()
expected_weights = {
"real": [(1, 100)],
"imag": [(0, 100)],
"minus_real": [(-1, 100)],
"minus_imag": [(0, 100)],
}
compare_integration_weights(expected_weights, weights)
pulse = pulses.SquareReadoutPulse(length=100, amplitude=1)

weights = pulse.integration_weights_function()
expected_weights = {
"real": [(1, 100)],
"imag": [(0, 100)],
"minus_real": [(-1, 100)],
"minus_imag": [(0, 100)],
}
compare_integration_weights(expected_weights, weights)


def test_constant_readout_pulse_integration_weights_phase_shift():
Expand Down
157 changes: 157 additions & 0 deletions tests/quam_base/test_set_at_reference.py
Original file line number Diff line number Diff line change
@@ -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"
50 changes: 50 additions & 0 deletions tests/utils/test_string_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("#")

0 comments on commit 7c30b2a

Please sign in to comment.