From afe6c6c7987920a7e5123b86bf4528b80cf6b327 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 2 Dec 2024 21:24:24 +0100 Subject: [PATCH] add tests for qubit --- quam/components/quantum_components/qubit.py | 15 ++- .../quantum_components/qubit_pair.py | 9 ++ .../components/quantum_components/conftest.py | 62 ++++++++++ .../quantum_components/test_qubit.py | 101 +++++++++++++++ .../quantum_components/test_qubit_pair.py | 116 ++++++++++++++++++ tests/components/test_qubit.py | 110 ----------------- tests/macros/test_pulse_macro.py | 6 +- tests/operations/test_register_operations.py | 14 +-- 8 files changed, 306 insertions(+), 127 deletions(-) create mode 100644 tests/components/quantum_components/conftest.py create mode 100644 tests/components/quantum_components/test_qubit.py create mode 100644 tests/components/quantum_components/test_qubit_pair.py delete mode 100644 tests/components/test_qubit.py diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 9f545a6..9c8e8d8 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -8,6 +8,7 @@ from quam.components.pulses import Pulse from quam.components.quantum_components import QuantumComponent from quam.core import quam_dataclass +from quam.utils import string_reference as str_ref if TYPE_CHECKING: from quam.components.macro import QubitMacro @@ -22,9 +23,21 @@ @quam_dataclass class Qubit(QuantumComponent): - id: Union[str, int] + id: Union[str, int] = "#./inferred_id" macros: Dict[str, MacroType] = field(default_factory=dict) + @property + def inferred_id(self) -> Union[str, int]: + if not str_ref.is_reference(self.get_unreferenced_value("id")): + return self.id + elif self.parent is not None: + name = self.parent.get_attr_name(self) + return name + else: + raise AttributeError( + f"Cannot infer id of {self} because it is not attached to a parent" + ) + @property def name(self) -> str: """Returns the name of the qubit""" diff --git a/quam/components/quantum_components/qubit_pair.py b/quam/components/quantum_components/qubit_pair.py index 1601225..f5f335b 100644 --- a/quam/components/quantum_components/qubit_pair.py +++ b/quam/components/quantum_components/qubit_pair.py @@ -3,6 +3,7 @@ from quam.core import quam_dataclass from quam.components.quantum_components import QuantumComponent, Qubit +from quam.utils import string_reference as str_ref if TYPE_CHECKING: from quam.components.macro import QubitPairMacro @@ -14,10 +15,18 @@ @quam_dataclass class QubitPair(QuantumComponent): + id: str = "#./name" qubit_control: Qubit qubit_target: Qubit macros: Dict[str, MacroType] = field(default_factory=dict) + @property + def name(self) -> str: + if not str_ref.is_reference(self.get_unreferenced_value("id")): + return self.id + else: + return f"{self.qubit_control.name}@{self.qubit_target.name}" + def align(self): """Aligns the execution of all channels of both qubits""" self.qubit_control.align(self.qubit_target) diff --git a/tests/components/quantum_components/conftest.py b/tests/components/quantum_components/conftest.py new file mode 100644 index 0000000..00e5891 --- /dev/null +++ b/tests/components/quantum_components/conftest.py @@ -0,0 +1,62 @@ +from typing import Dict, Optional +import pytest +from quam.components import Qubit, QubitPair +from quam.components.channels import IQChannel +from quam.core.quam_classes import QuamRoot, quam_dataclass +from dataclasses import field + + +@quam_dataclass +class MockQubit(Qubit): + xy: IQChannel + resonator: Optional[IQChannel] = None + + +@quam_dataclass +class TestQUAM(QuamRoot): + qubits: Dict[str, MockQubit] = field(default_factory=dict) + qubit_pairs: Dict[str, QubitPair] = field(default_factory=dict) + + +@pytest.fixture +def mock_qubit(): + """Basic mock qubit with xy channel""" + return MockQubit( + id="q0", + xy=IQChannel( + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=None, + ), + ) + + +@pytest.fixture +def mock_qubit_with_resonator(): + """Mock qubit with both xy and resonator channels""" + return MockQubit( + id="q1", + xy=IQChannel( + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=None, + ), + resonator=IQChannel( + opx_output_I=("con1", 3), + opx_output_Q=("con1", 4), + frequency_converter_up=None, + ), + ) + + +@pytest.fixture +def test_quam(mock_qubit, mock_qubit_with_resonator): + """Test QUAM instance with qubits and qubit pairs""" + machine = TestQUAM( + qubits={"q0": mock_qubit, "q1": mock_qubit_with_resonator}, + ) + machine.qubit_pairs["pair_0"] = QubitPair( + qubit_control=mock_qubit.get_reference(), + qubit_target=mock_qubit_with_resonator.get_reference(), + ) + return machine diff --git a/tests/components/quantum_components/test_qubit.py b/tests/components/quantum_components/test_qubit.py new file mode 100644 index 0000000..298bc06 --- /dev/null +++ b/tests/components/quantum_components/test_qubit.py @@ -0,0 +1,101 @@ +from typing import Optional +import pytest +from quam.components import Qubit +from quam.components.channels import IQChannel +from quam.components.pulses import SquarePulse +from quam.core.quam_classes import QuamRoot, quam_dataclass + + +def test_qubit_name_int(): + qubit = Qubit(id=0) + assert qubit.name == "q0" + + +def test_qubit_name_str(): + qubit = Qubit(id="qubit0") + assert qubit.name == "qubit0" + + +def test_qubit_channels(mock_qubit_with_resonator): + assert mock_qubit_with_resonator.channels == { + "xy": mock_qubit_with_resonator.xy, + "resonator": mock_qubit_with_resonator.resonator, + } + + +def test_qubit_channels_referenced(mock_qubit): + # Set resonator as a reference to xy channel + mock_qubit.resonator = "#./xy" + + assert mock_qubit.channels == { + "xy": mock_qubit.xy, + "resonator": mock_qubit.xy, + } + + +def test_qubit_get_pulse_not_found(mock_qubit): + with pytest.raises(ValueError, match="Pulse test_pulse not found"): + mock_qubit.get_pulse("test_pulse") + + +def test_qubit_get_pulse_not_unique(mock_qubit_with_resonator): + mock_qubit_with_resonator.xy.operations["test_pulse"] = SquarePulse( + length=100, amplitude=1.0 + ) + mock_qubit_with_resonator.resonator.operations["test_pulse"] = SquarePulse( + length=100, amplitude=1.0 + ) + + with pytest.raises(ValueError, match="Pulse test_pulse is not unique"): + mock_qubit_with_resonator.get_pulse("test_pulse") + + +def test_qubit_get_pulse_unique(mock_qubit): + pulse = SquarePulse(length=100, amplitude=1.0) + mock_qubit.xy.operations["test_pulse"] = pulse + + assert mock_qubit.get_pulse("test_pulse") == pulse + + +def test_qubit_align(mock_qubit_with_resonator, mocker): + mocker.patch("quam.components.quantum_components.qubit.align") + mock_qubit_with_resonator.align(mock_qubit_with_resonator) + + from quam.components.quantum_components.qubit import align + + align.assert_called_once_with("q1.xy", "q1.resonator") + + +def test_qubit_get_macros(mock_qubit): + assert mock_qubit.macros == {} + assert mock_qubit.get_macros() == {"align": mock_qubit.align} + + +def test_qubit_apply_align(mock_qubit_with_resonator, mocker): + mocker.patch("quam.components.quantum_components.qubit.align") + mock_qubit_with_resonator.apply("align") + + from quam.components.quantum_components.qubit import align + + align.assert_called_once_with("q1.xy", "q1.resonator") + + +def test_qubit_inferred_id_direct(): + """Test inferred_id when id is a direct value""" + qubit = Qubit(id=0) + assert qubit.inferred_id == 0 + + +def test_qubit_inferred_id_with_parent(test_quam): + """Test inferred_id when id is a reference and qubit has parent""" + test_quam.qubits["q2"] = Qubit() + assert test_quam.qubits["q2"].inferred_id == "q2" + + +def test_qubit_inferred_id_no_parent(): + """Test inferred_id when id is a reference but qubit has no parent""" + qubit = Qubit(id="#./inferred_id") + with pytest.raises( + AttributeError, match="Cannot infer id .* not attached to a parent" + ): + _ = qubit.inferred_id diff --git a/tests/components/quantum_components/test_qubit_pair.py b/tests/components/quantum_components/test_qubit_pair.py new file mode 100644 index 0000000..44201e1 --- /dev/null +++ b/tests/components/quantum_components/test_qubit_pair.py @@ -0,0 +1,116 @@ +from typing import Dict, List +import pytest +from quam.components import Qubit, QubitPair +from quam.components.channels import IQChannel +from quam.core.quam_classes import QuamRoot, quam_dataclass + + +@quam_dataclass +class MockQubit(Qubit): + xy: IQChannel = None + + +@quam_dataclass +class MockQubitPair(QubitPair): + qubit_control: MockQubit + qubit_target: MockQubit + + +@quam_dataclass +class QUAM(QuamRoot): + qubits: Dict[str, MockQubit] + qubit_pairs: str[str, MockQubitPair] + + +@pytest.fixture +def test_qubit_control(): + return MockQubit( + id="q1", + xy=IQChannel( + id="xy_control", + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=None, + ), + ) + + +@pytest.fixture +def test_qubit_target(): + return MockQubit( + id="q2", + xy=IQChannel( + id="xy_target", + opx_output_I=("con1", 5), + opx_output_Q=("con1", 6), + frequency_converter_up=None, + ), + ) + + +@pytest.fixture +def test_qubit_pair(test_qubit_control, test_qubit_target): + return MockQubitPair( + id="pair_1", + qubit_control=test_qubit_control, + qubit_target=test_qubit_target + ) + + +@pytest.fixture +def test_quam(test_qubit_control, test_qubit_target, test_qubit_pair): + return QUAM( + qubits={"control": test_qubit_control, "target": test_qubit_target}, + qubit_pairs=[test_qubit_pair], + ) + + +def test_qubit_pair_initialization(test_qubit_pair, test_qubit_control, test_qubit_target): + """Test that QubitPair is initialized correctly""" + assert test_qubit_pair.qubit_control == test_qubit_control + assert test_qubit_pair.qubit_target == test_qubit_target + assert test_qubit_pair.name == "pair_1" + assert isinstance(test_qubit_pair.macros, dict) + assert len(test_qubit_pair.macros) == 0 + + +def test_qubit_pair_align(test_qubit_pair, mocker): + """Test that align method calls the control qubit's align method with correct args""" + mock_align = mocker.patch.object(test_qubit_pair.qubit_control, 'align') + + test_qubit_pair.align() + + mock_align.assert_called_once_with(test_qubit_pair.qubit_target) + + +def test_qubit_pair_via_matmul(test_quam): + """Test that qubit pair can be accessed via @ operator""" + control = test_quam.qubits["control"] + target = test_quam.qubits["target"] + + qubit_pair = control @ target + + assert isinstance(qubit_pair, QubitPair) + assert qubit_pair.qubit_control == control + assert qubit_pair.qubit_target == target + + +def test_matmul_with_invalid_qubit(test_quam): + """Test that @ operator raises error for invalid qubit pairs""" + control = test_quam.qubits["control"] + + with pytest.raises(ValueError, match="Cannot create a qubit pair with same qubit"): + _ = control @ control + + with pytest.raises(ValueError, match="Cannot create a qubit pair .* with a non-qubit object"): + _ = control @ "not_a_qubit" + + +def test_matmul_with_nonexistent_pair(test_quam): + """Test that @ operator raises error for non-existent qubit pairs""" + target = test_quam.qubits["target"] + control = test_quam.qubits["control"] + + # Try to access pair in reverse order (target @ control) when only (control @ target) exists + with pytest.raises(ValueError, match="Qubit pair not found"): + _ = target @ control diff --git a/tests/components/test_qubit.py b/tests/components/test_qubit.py deleted file mode 100644 index 5e4df0c..0000000 --- a/tests/components/test_qubit.py +++ /dev/null @@ -1,110 +0,0 @@ -import pytest -from quam.components import Qubit -from quam.components.channels import IQChannel -from quam.components.pulses import SquarePulse -from quam.core.quam_classes import quam_dataclass - - -def test_qubit_name_int(): - qubit = Qubit(id=0) - assert qubit.name == "q0" - - -def test_qubit_name_str(): - qubit = Qubit(id="qubit0") - assert qubit.name == "qubit0" - - -@quam_dataclass -class TestQubit(Qubit): - xy: IQChannel - resonator: IQChannel - - -@pytest.fixture -def test_qubit(): - return TestQubit( - id=0, - xy=IQChannel( - opx_output_I=("con1", 1), - opx_output_Q=("con1", 2), - frequency_converter_up=None, - ), - resonator=IQChannel( - opx_output_I=("con1", 3), - opx_output_Q=("con1", 4), - frequency_converter_up=None, - ), - ) - - -def test_qubit_channels(test_qubit): - assert test_qubit.channels == { - "xy": test_qubit.xy, - "resonator": test_qubit.resonator, - } - - -@pytest.fixture -def test_qubit_referenced(): - return TestQubit( - id=0, - xy=IQChannel( - opx_output_I=("con1", 1), - opx_output_Q=("con1", 2), - frequency_converter_up=None, - ), - resonator="#./xy", - ) - - -def test_qubit_channels_referenced(test_qubit_referenced): - assert test_qubit_referenced.channels == { - "xy": test_qubit_referenced.xy, - "resonator": test_qubit_referenced.xy, - } - - -def test_qubit_get_pulse_not_found(test_qubit): - with pytest.raises(ValueError, match="Pulse test_pulse not found"): - test_qubit.get_pulse("test_pulse") - - -def test_qubit_get_pulse_not_unique(test_qubit): - test_qubit.xy.operations["test_pulse"] = SquarePulse(length=100, amplitude=1.0) - test_qubit.resonator.operations["test_pulse"] = SquarePulse( - length=100, amplitude=1.0 - ) - - with pytest.raises(ValueError, match="Pulse test_pulse is not unique"): - test_qubit.get_pulse("test_pulse") - - -def test_qubit_get_pulse_unique(test_qubit): - pulse = SquarePulse(length=100, amplitude=1.0) - test_qubit.xy.operations["test_pulse"] = pulse - - assert test_qubit.get_pulse("test_pulse") == pulse - - -def test_qubit_align(test_qubit, mocker): - mocker.patch("quam.components.quantum_components.qubit.align") - test_qubit.align(test_qubit) - - from quam.components.quantum_components.qubit import align - - align.assert_called_once_with("q0.xy", "q0.resonator") - - -def test_qubit_get_macros(test_qubit): - assert test_qubit.macros == {} - assert test_qubit.get_macros() == {"align": test_qubit.align} - - -def test_qubit_apply_align(test_qubit, mocker): - mocker.patch("quam.components.quantum_components.qubit.align") - test_qubit.apply("align") - - from quam.components.quantum_components.qubit import align - - align.assert_called_once_with("q0.xy", "q0.resonator") diff --git a/tests/macros/test_pulse_macro.py b/tests/macros/test_pulse_macro.py index 15f5c6f..3a631a4 100644 --- a/tests/macros/test_pulse_macro.py +++ b/tests/macros/test_pulse_macro.py @@ -7,18 +7,18 @@ @quam_dataclass -class TestQubit(Qubit): +class MockQubit(Qubit): xy: IQChannel @quam_dataclass class QUAM(QuamRoot): - qubit: TestQubit + qubit: MockQubit @pytest.fixture def test_qubit(): - return TestQubit( + return MockQubit( id=0, xy=IQChannel( opx_output_I=("con1", 1), diff --git a/tests/operations/test_register_operations.py b/tests/operations/test_register_operations.py index a291c16..3a4ae45 100644 --- a/tests/operations/test_register_operations.py +++ b/tests/operations/test_register_operations.py @@ -1,14 +1,2 @@ from quam.core import OperationsRegistry - - -def test_register_operations(): - operations_registry = OperationsRegistry() - - @operations_registry.register_operation - def test_operation(a: int, b: int) -> int: - return a + b - - assert dict(operations_registry) == {"test_operation": test_operation} - - assert test_operation(1, 2) == 3 - assert operations_registry["test_operation"](1, 2) == 3 +from quam.components.quantum_components.qubit import Qubit