From f6604d36542e7d787c901135eb1932f0852883f7 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 17 Jun 2024 14:41:08 +0200 Subject: [PATCH 01/43] add qubit ipementations --- quam/components/gates/single_qubit_gates.py | 40 ++++++++ quam/components/gates/two_qubit_gates.py | 106 ++++++++++++++++++++ quam/components/qubit.py | 43 ++++++++ quam/components/qubit_pair.py | 13 +++ 4 files changed, 202 insertions(+) create mode 100644 quam/components/gates/single_qubit_gates.py create mode 100644 quam/components/gates/two_qubit_gates.py create mode 100644 quam/components/qubit.py create mode 100644 quam/components/qubit_pair.py diff --git a/quam/components/gates/single_qubit_gates.py b/quam/components/gates/single_qubit_gates.py new file mode 100644 index 00000000..3cf84bab --- /dev/null +++ b/quam/components/gates/single_qubit_gates.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +from typing import Dict +from dataclasses import field +from quam.core import quam_dataclass, QuamComponent +from quam.components.pulses import Pulse + + +@quam_dataclass +class SingleQubitGate(QuamComponent, ABC): + @property + def qubit(self): + from ..qubit import Qubit + + if isinstance(self.parent, Qubit): + return self.parent + elif hasattr(self.parent, "parent") and isinstance(self.parent.parent, Qubit): + return self.parent.parent + else: + raise AttributeError( + "SingleQubitGate is not attached to a qubit. 1Q_gate: {self}" + ) + + def __call__(self): + self.execute() + + @abstractmethod + def execute(self, *args, **kwargs): # TODO Accomodate differing arguments + pass + + +@quam_dataclass +class SinglePulseGate(SingleQubitGate): + """Single-qubit gate for a qubit consisting of a single pulse""" + + pulse_label: str + + def execute(self, amplitude_scale=None, duration=None): + self.qubit.xy.play( # TODO Introduce a "play" method to the qubit + self.pulse_label, amplitude_scale=amplitude_scale, duration=duration + ) diff --git a/quam/components/gates/two_qubit_gates.py b/quam/components/gates/two_qubit_gates.py new file mode 100644 index 00000000..8d00cf39 --- /dev/null +++ b/quam/components/gates/two_qubit_gates.py @@ -0,0 +1,106 @@ +from abc import ABC, abstractmethod +from typing import Dict +from dataclasses import field +from copy import copy + +from quam.components.pulses import Pulse +from quam.core import quam_dataclass, QuamComponent +from quam.utils import string_reference as str_ref + + +__all__ = ["TwoQubitGate", "CZGate"] + + +@quam_dataclass +class TwoQubitGate(QuamComponent, ABC): + @property + def qubit_pair(self): + from ..qubit_pair import QubitPair + + if isinstance(self.parent, QubitPair): + return self.parent + elif hasattr(self.parent, "parent") and isinstance( + self.parent.parent, QubitPair + ): + return self.parent.parent + else: + raise AttributeError( + "TwoQubitGate is not attached to a QubitPair. 2Q_gate: {self}" + ) + + @property + def qubit_control(self): + return self.qubit_pair.qubit_control + + @property + def qubit_target(self): + return self.qubit_pair.qubit_target + + def __call__(self): + self.execute() + + +@quam_dataclass +class CZGate(TwoQubitGate): + """CZ Operation for a qubit pair""" + + # Pulses will be added to qubit elements + # The reason we don't add "flux_to_q1" directly to q1.z is because it is part of + # the CZ operation, i.e. it is only applied as part of a CZ operation + + flux_pulse_control: Pulse + + phase_shift_control: float = 0.0 + phase_shift_target: float = 0.0 + + @property + def gate_label(self) -> str: + try: + return self.parent.get_attr_name(self) + except AttributeError: + return "CZ" + + @property + def flux_pulse_control_label(self) -> str: + if self.flux_pulse_control.id is not None: + pulse_label = self.flux_pulse_control.id + else: + pulse_label = "flux_pulse_control" + + return f"{self.gate_label}{str_ref.DELIMITER}{pulse_label}" + + def execute(self, amplitude_scale=None): + self.qubit_control.z.play( + self.flux_pulse_control_label, + validate=False, + amplitude_scale=amplitude_scale, + ) + self.qubit_control.align(self.qubit_target) + + + self.qubit_control.xy.frame_rotation(self.phase_shift_control) + self.qubit_target.xy.frame_rotation(self.phase_shift_target) + self.qubit_control.align(self.qubit_target) + + @property + def config_settings(self): + return {"after": [self.qubit_control.z]} + + def apply_to_config(self, config: dict) -> None: + pulse = copy(self.flux_pulse_control) + pulse.id = self.flux_pulse_control_label + pulse.parent = None # Reset parent so it can be attached to new parent + pulse.parent = self.qubit_control.z + + if self.flux_pulse_control_label in self.qubit_control.z.operations: + raise ValueError( + "Pulse name already exists in pulse operations. " + f"Channel: {self.qubit_control.z.get_reference()}, " + f"Pulse: {self.flux_pulse_control.get_reference()}, " + f"Pulse name: {self.flux_pulse_control_label}" + ) + + pulse.apply_to_config(config) + + element_config = config["elements"][self.qubit_control.z.name] + element_config["operations"][self.flux_pulse_control_label] = pulse.pulse_name diff --git a/quam/components/qubit.py b/quam/components/qubit.py new file mode 100644 index 00000000..06659eff --- /dev/null +++ b/quam/components/qubit.py @@ -0,0 +1,43 @@ +from typing import Dict, Any +from dataclasses import field + +from quam.core import quam_dataclass, QuamComponent +from .gates.single_qubit_gates import SingleQubitGate +from .gates.two_qubit_gates import TwoQubitGate + + +__all__ = ["Qubit"] + + +@quam_dataclass +class Qubit(QuamComponent): + id: str + gates: Dict[str, SingleQubitGate] = field(default_factory=dict) + + def __matmul__(self, other): + """Allows access to qubit pairs using the '@' operator, e.g. (q1 @ q2)""" + if not isinstance(other, Qubit): + raise ValueError( + "Cannot create a qubit pair (q1 @ q2) with a non-qubit object, " + f"where q1={self} and q2={other}" + ) + + if self is other: + raise ValueError( + "Cannot create a qubit pair with same qubit (q1 @ q1), where q1={self}" + ) + + if not hasattr(self._root, "qubit_pairs"): + raise AttributeError( + "Qubit pairs not found in the root component. " + "Please add a 'qubit_pairs' attribute to the root component." + ) + + for qubit_pair in self._root.qubit_pairs: + if qubit_pair.qubit_control is self and qubit_pair.qubit_target is other: + return qubit_pair + else: + raise ValueError( + "Qubit pair not found: qubit_control={self.name}, " + "qubit_target={other.name}" + ) \ No newline at end of file diff --git a/quam/components/qubit_pair.py b/quam/components/qubit_pair.py new file mode 100644 index 00000000..6e7e7807 --- /dev/null +++ b/quam/components/qubit_pair.py @@ -0,0 +1,13 @@ +from typing import Dict +from dataclasses import field + +from quam.core import quam_dataclass, QuamComponent +from quam.components.qubit import Qubit +from quam.components.gates.two_qubit_gates import TwoQubitGate + + +@quam_dataclass +class QubitPair(QuamComponent): + qubit_control: Qubit # TODO Discuss alternatives to "control" and "target" + qubit_target: Qubit + gates: Dict[str, TwoQubitGate] = field(default_factory=dict) From 9dd59b608599148d223da387dd530cbcafd62781 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 18 Jun 2024 11:00:45 +0200 Subject: [PATCH 02/43] Mostly working 1Q gates --- quam/components/gates/single_qubit_gates.py | 13 +++- quam/components/gates/two_qubit_gates.py | 1 - quam/components/qubit.py | 84 ++++++++++++++++++++- 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/quam/components/gates/single_qubit_gates.py b/quam/components/gates/single_qubit_gates.py index 3cf84bab..8a75c36b 100644 --- a/quam/components/gates/single_qubit_gates.py +++ b/quam/components/gates/single_qubit_gates.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict +from typing import Dict, Union from dataclasses import field from quam.core import quam_dataclass, QuamComponent from quam.components.pulses import Pulse @@ -30,11 +30,16 @@ def execute(self, *args, **kwargs): # TODO Accomodate differing arguments @quam_dataclass class SinglePulseGate(SingleQubitGate): - """Single-qubit gate for a qubit consisting of a single pulse""" + """Single-qubit gate for a qubit consisting of a single pulse - pulse_label: str + Args: + pulse (Union[Pulse, str]): The pulse to be played + + """ + + pulse: Union[Pulse, str] def execute(self, amplitude_scale=None, duration=None): - self.qubit.xy.play( # TODO Introduce a "play" method to the qubit + self.qubit.play_pulse( self.pulse_label, amplitude_scale=amplitude_scale, duration=duration ) diff --git a/quam/components/gates/two_qubit_gates.py b/quam/components/gates/two_qubit_gates.py index 8d00cf39..452e8154 100644 --- a/quam/components/gates/two_qubit_gates.py +++ b/quam/components/gates/two_qubit_gates.py @@ -76,7 +76,6 @@ def execute(self, amplitude_scale=None): amplitude_scale=amplitude_scale, ) self.qubit_control.align(self.qubit_target) - self.qubit_control.xy.frame_rotation(self.phase_shift_control) self.qubit_target.xy.frame_rotation(self.phase_shift_target) diff --git a/quam/components/qubit.py b/quam/components/qubit.py index 06659eff..301b58e1 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -1,6 +1,7 @@ from typing import Dict, Any from dataclasses import field +from quam.components.channels import Channel from quam.core import quam_dataclass, QuamComponent from .gates.single_qubit_gates import SingleQubitGate from .gates.two_qubit_gates import TwoQubitGate @@ -14,6 +15,10 @@ class Qubit(QuamComponent): id: str gates: Dict[str, SingleQubitGate] = field(default_factory=dict) + @property + def name(self): + return self.id if isinstance(self.id, str) else f"q{self.id}" + def __matmul__(self, other): """Allows access to qubit pairs using the '@' operator, e.g. (q1 @ q2)""" if not isinstance(other, Qubit): @@ -40,4 +45,81 @@ def __matmul__(self, other): raise ValueError( "Qubit pair not found: qubit_control={self.name}, " "qubit_target={other.name}" - ) \ No newline at end of file + ) + + def play_pulse( + self, + pulse_name: str, + amplitude_scale: Union[float, AmpValuesType] = None, + duration: QuaNumberType = None, + condition: QuaExpressionType = None, + chirp: ChirpType = None, + truncate: QuaNumberType = None, + timestamp_stream: StreamType = None, + continue_chirp: bool = False, + target: str = "", + validate: bool = True, + ): + """Play a pulse on this channel. + + Args: + pulse_name (str): The name of the pulse to play. Should be registered in + `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). + duration (int): Duration of the pulse in units of the clock cycle (4ns). + If not provided, the default pulse duration will be used. It is possible + to dynamically change the duration of both constant and arbitrary + pulses. Arbitrary pulses can only be stretched, not compressed. + chirp (Union[(list[int], str), (int, str)]): Allows to perform + piecewise linear sweep of the element's intermediate + frequency in time. Input should be a tuple, with the 1st + element being a list of rates and the second should be a + string with the units. The units can be either: 'Hz/nsec', + 'mHz/nsec', 'uHz/nsec', 'pHz/nsec' or 'GHz/sec', 'MHz/sec', + 'KHz/sec', 'Hz/sec', 'mHz/sec'. + truncate (Union[int, QUA variable of type int]): Allows playing + only part of the pulse, truncating the end. If provided, + will play only up to the given time in units of the clock + cycle (4ns). + condition (A logical expression to evaluate.): Will play analog + pulse only if the condition's value is true. Any digital + pulses associated with the operation will always play. + timestamp_stream (Union[str, _ResultSource]): (Supported from + QOP 2.2) Adding a `timestamp_stream` argument will save the + time at which the operation occurred to a stream. If the + `timestamp_stream` is a string ``label``, then the timestamp + handle can be retrieved with + `qm._results.JobResults.get` with the same ``label``. + validate (bool): If True (default), validate that the pulse is registered + in Channel.operations + + Note: + The `element` argument from `qm.qua.play()`is not needed, as it is + automatically set to `self.name`. + + """ + attrs = self.get_attrs(follow_references=False, include_defaults=True) + channels = {key: val for key, val in attrs.items() if isinstance(val, Channel)} + for channel in channels.values(): + if pulse_name not in channel.operations: + continue + + channel.play( + pulse_name=pulse_name, + amplitude_scale=amplitude_scale, + duration=duration, + condition=condition, + chirp=chirp, + truncate=truncate, + timestamp_stream=timestamp_stream, + continue_chirp=continue_chirp, + target=target, + validate=validate, + ) + break + else: + raise ValueError( + f"Pulse name not found in any channel operations of qubit: " + f"{pulse_name=}\nqubit={self.name}" + ) From b4955958077a0cdb2bce827746da8a0e735ab2e7 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 18 Jun 2024 11:03:20 +0200 Subject: [PATCH 03/43] working single-qubit gates --- quam/components/gates/single_qubit_gates.py | 5 +++-- quam/components/qubit.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/quam/components/gates/single_qubit_gates.py b/quam/components/gates/single_qubit_gates.py index 8a75c36b..36d48a44 100644 --- a/quam/components/gates/single_qubit_gates.py +++ b/quam/components/gates/single_qubit_gates.py @@ -33,11 +33,12 @@ class SinglePulseGate(SingleQubitGate): """Single-qubit gate for a qubit consisting of a single pulse Args: - pulse (Union[Pulse, str]): The pulse to be played + pulse: Name of pulse to be played on qubit. Should be a key in + `channel.operations` for one of the qubit's channels """ - pulse: Union[Pulse, str] + pulse: str def execute(self, amplitude_scale=None, duration=None): self.qubit.play_pulse( diff --git a/quam/components/qubit.py b/quam/components/qubit.py index 301b58e1..34d5cff5 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -60,7 +60,8 @@ def play_pulse( target: str = "", validate: bool = True, ): - """Play a pulse on this channel. + """Play a pulse on the qubit, the corresponding channel will be determined + based on the pulse name. Args: pulse_name (str): The name of the pulse to play. Should be registered in @@ -98,6 +99,8 @@ def play_pulse( The `element` argument from `qm.qua.play()`is not needed, as it is automatically set to `self.name`. + Raises: + ValueError: If the pulse name is not found in any channel operations of the qubit. """ attrs = self.get_attrs(follow_references=False, include_defaults=True) channels = {key: val for key, val in attrs.items() if isinstance(val, Channel)} From 85701ecf5bae02edf8df28abfa2a23fc0c60b578 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 18 Jun 2024 11:07:46 +0200 Subject: [PATCH 04/43] small cleanup --- quam/components/qubit.py | 11 +++++++++-- quam/components/qubit_pair.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/quam/components/qubit.py b/quam/components/qubit.py index 34d5cff5..071ac109 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -1,10 +1,17 @@ -from typing import Dict, Any +from typing import Dict, Union from dataclasses import field from quam.components.channels import Channel from quam.core import quam_dataclass, QuamComponent from .gates.single_qubit_gates import SingleQubitGate -from .gates.two_qubit_gates import TwoQubitGate + +from qm.qua._dsl import ( + AmpValuesType, + QuaNumberType, + QuaExpressionType, + ChirpType, + StreamType, +) __all__ = ["Qubit"] diff --git a/quam/components/qubit_pair.py b/quam/components/qubit_pair.py index 6e7e7807..092f706a 100644 --- a/quam/components/qubit_pair.py +++ b/quam/components/qubit_pair.py @@ -8,6 +8,6 @@ @quam_dataclass class QubitPair(QuamComponent): - qubit_control: Qubit # TODO Discuss alternatives to "control" and "target" + qubit_control: Qubit qubit_target: Qubit gates: Dict[str, TwoQubitGate] = field(default_factory=dict) From 12703bc2bd903cf0597981f63816c035f8257c75 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 24 Jun 2024 20:25:30 +0200 Subject: [PATCH 05/43] remove unnecessary imports --- quam/components/gates/single_qubit_gates.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/quam/components/gates/single_qubit_gates.py b/quam/components/gates/single_qubit_gates.py index 36d48a44..d40744b0 100644 --- a/quam/components/gates/single_qubit_gates.py +++ b/quam/components/gates/single_qubit_gates.py @@ -1,8 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Union -from dataclasses import field from quam.core import quam_dataclass, QuamComponent -from quam.components.pulses import Pulse @quam_dataclass From 9bd7019117a1ed3423143ad89924788a48e6cb3a Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 8 Oct 2024 10:02:52 +0200 Subject: [PATCH 06/43] rename gate -> gate_implementation --- .../single_qubit_gate_implementations.py} | 4 ++-- .../two_qubit_gate_implementations.py} | 6 +++--- quam/components/qubit.py | 6 ++++-- quam/components/qubit_pair.py | 6 ++++-- 4 files changed, 13 insertions(+), 9 deletions(-) rename quam/components/{gates/single_qubit_gates.py => gate_implementations/single_qubit_gate_implementations.py} (89%) rename quam/components/{gates/two_qubit_gates.py => gate_implementations/two_qubit_gate_implementations.py} (94%) diff --git a/quam/components/gates/single_qubit_gates.py b/quam/components/gate_implementations/single_qubit_gate_implementations.py similarity index 89% rename from quam/components/gates/single_qubit_gates.py rename to quam/components/gate_implementations/single_qubit_gate_implementations.py index d40744b0..58dace64 100644 --- a/quam/components/gates/single_qubit_gates.py +++ b/quam/components/gate_implementations/single_qubit_gate_implementations.py @@ -3,7 +3,7 @@ @quam_dataclass -class SingleQubitGate(QuamComponent, ABC): +class SingleQubitGateImplementation(QuamComponent, ABC): @property def qubit(self): from ..qubit import Qubit @@ -26,7 +26,7 @@ def execute(self, *args, **kwargs): # TODO Accomodate differing arguments @quam_dataclass -class SinglePulseGate(SingleQubitGate): +class SinglePulseGateImplementation(SingleQubitGateImplementation): """Single-qubit gate for a qubit consisting of a single pulse Args: diff --git a/quam/components/gates/two_qubit_gates.py b/quam/components/gate_implementations/two_qubit_gate_implementations.py similarity index 94% rename from quam/components/gates/two_qubit_gates.py rename to quam/components/gate_implementations/two_qubit_gate_implementations.py index 452e8154..dbb9ccb5 100644 --- a/quam/components/gates/two_qubit_gates.py +++ b/quam/components/gate_implementations/two_qubit_gate_implementations.py @@ -8,11 +8,11 @@ from quam.utils import string_reference as str_ref -__all__ = ["TwoQubitGate", "CZGate"] +__all__ = ["TwoQubitGateImplementation", "CZGateImplementation"] @quam_dataclass -class TwoQubitGate(QuamComponent, ABC): +class TwoQubitGateImplementation(QuamComponent, ABC): @property def qubit_pair(self): from ..qubit_pair import QubitPair @@ -41,7 +41,7 @@ def __call__(self): @quam_dataclass -class CZGate(TwoQubitGate): +class CZGateImplementation(TwoQubitGateImplementation): """CZ Operation for a qubit pair""" # Pulses will be added to qubit elements diff --git a/quam/components/qubit.py b/quam/components/qubit.py index 071ac109..c000375c 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -3,7 +3,9 @@ from quam.components.channels import Channel from quam.core import quam_dataclass, QuamComponent -from .gates.single_qubit_gates import SingleQubitGate +from .gate_implementations.single_qubit_gate_implementations import ( + SingleQubitGateImplementation, +) from qm.qua._dsl import ( AmpValuesType, @@ -20,7 +22,7 @@ @quam_dataclass class Qubit(QuamComponent): id: str - gates: Dict[str, SingleQubitGate] = field(default_factory=dict) + gates: Dict[str, SingleQubitGateImplementation] = field(default_factory=dict) @property def name(self): diff --git a/quam/components/qubit_pair.py b/quam/components/qubit_pair.py index 092f706a..866b7014 100644 --- a/quam/components/qubit_pair.py +++ b/quam/components/qubit_pair.py @@ -3,11 +3,13 @@ from quam.core import quam_dataclass, QuamComponent from quam.components.qubit import Qubit -from quam.components.gates.two_qubit_gates import TwoQubitGate +from quam.components.gate_implementations.two_qubit_gate_implementations import ( + TwoQubitGateImplementation, +) @quam_dataclass class QubitPair(QuamComponent): qubit_control: Qubit qubit_target: Qubit - gates: Dict[str, TwoQubitGate] = field(default_factory=dict) + gates: Dict[str, TwoQubitGateImplementation] = field(default_factory=dict) From c88be57baa396b83a4afcab949334b695978c200 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 18 Oct 2024 14:09:34 +0200 Subject: [PATCH 07/43] Directly play from pulse --- .../single_qubit_gate_implementations.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/quam/components/gate_implementations/single_qubit_gate_implementations.py b/quam/components/gate_implementations/single_qubit_gate_implementations.py index 58dace64..a1a11d7f 100644 --- a/quam/components/gate_implementations/single_qubit_gate_implementations.py +++ b/quam/components/gate_implementations/single_qubit_gate_implementations.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from quam.components.pulses import Pulse from quam.core import quam_dataclass, QuamComponent @@ -35,9 +36,7 @@ class SinglePulseGateImplementation(SingleQubitGateImplementation): """ - pulse: str + pulse: Pulse - def execute(self, amplitude_scale=None, duration=None): - self.qubit.play_pulse( - self.pulse_label, amplitude_scale=amplitude_scale, duration=duration - ) + def execute(self, *, amplitude_scale=None, duration=None, **kwargs): + self.pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) From 98550a6b629b7368e07e5796c4bf8f1ccbf75e5c Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:24:45 +0100 Subject: [PATCH 08/43] remove qubit.play_pulse --- quam/components/qubit.py | 82 +--------------------------------------- 1 file changed, 1 insertion(+), 81 deletions(-) diff --git a/quam/components/qubit.py b/quam/components/qubit.py index c000375c..8df74b49 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -7,7 +7,7 @@ SingleQubitGateImplementation, ) -from qm.qua._dsl import ( +from qm.qua._type_hinting import ( AmpValuesType, QuaNumberType, QuaExpressionType, @@ -55,83 +55,3 @@ def __matmul__(self, other): "Qubit pair not found: qubit_control={self.name}, " "qubit_target={other.name}" ) - - def play_pulse( - self, - pulse_name: str, - amplitude_scale: Union[float, AmpValuesType] = None, - duration: QuaNumberType = None, - condition: QuaExpressionType = None, - chirp: ChirpType = None, - truncate: QuaNumberType = None, - timestamp_stream: StreamType = None, - continue_chirp: bool = False, - target: str = "", - validate: bool = True, - ): - """Play a pulse on the qubit, the corresponding channel will be determined - based on the pulse name. - - Args: - pulse_name (str): The name of the pulse to play. Should be registered in - `self.operations`. - amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. - Can be either a float, or qua.amp(float). - duration (int): Duration of the pulse in units of the clock cycle (4ns). - If not provided, the default pulse duration will be used. It is possible - to dynamically change the duration of both constant and arbitrary - pulses. Arbitrary pulses can only be stretched, not compressed. - chirp (Union[(list[int], str), (int, str)]): Allows to perform - piecewise linear sweep of the element's intermediate - frequency in time. Input should be a tuple, with the 1st - element being a list of rates and the second should be a - string with the units. The units can be either: 'Hz/nsec', - 'mHz/nsec', 'uHz/nsec', 'pHz/nsec' or 'GHz/sec', 'MHz/sec', - 'KHz/sec', 'Hz/sec', 'mHz/sec'. - truncate (Union[int, QUA variable of type int]): Allows playing - only part of the pulse, truncating the end. If provided, - will play only up to the given time in units of the clock - cycle (4ns). - condition (A logical expression to evaluate.): Will play analog - pulse only if the condition's value is true. Any digital - pulses associated with the operation will always play. - timestamp_stream (Union[str, _ResultSource]): (Supported from - QOP 2.2) Adding a `timestamp_stream` argument will save the - time at which the operation occurred to a stream. If the - `timestamp_stream` is a string ``label``, then the timestamp - handle can be retrieved with - `qm._results.JobResults.get` with the same ``label``. - validate (bool): If True (default), validate that the pulse is registered - in Channel.operations - - Note: - The `element` argument from `qm.qua.play()`is not needed, as it is - automatically set to `self.name`. - - Raises: - ValueError: If the pulse name is not found in any channel operations of the qubit. - """ - attrs = self.get_attrs(follow_references=False, include_defaults=True) - channels = {key: val for key, val in attrs.items() if isinstance(val, Channel)} - for channel in channels.values(): - if pulse_name not in channel.operations: - continue - - channel.play( - pulse_name=pulse_name, - amplitude_scale=amplitude_scale, - duration=duration, - condition=condition, - chirp=chirp, - truncate=truncate, - timestamp_stream=timestamp_stream, - continue_chirp=continue_chirp, - target=target, - validate=validate, - ) - break - else: - raise ValueError( - f"Pulse name not found in any channel operations of qubit: " - f"{pulse_name=}\nqubit={self.name}" - ) From 740d590214cc9b06f6ee57f8414f5aef70f7765d Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:27:10 +0100 Subject: [PATCH 09/43] add qubit to init --- quam/components/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quam/components/__init__.py b/quam/components/__init__.py index 5c3d6a38..76bd1a16 100644 --- a/quam/components/__init__.py +++ b/quam/components/__init__.py @@ -3,11 +3,13 @@ from .octave import * from .channels import * from . import pulses +from .qubit import * __all__ = [ *basic_quam.__all__, *hardware.__all__, *channels.__all__, *octave.__all__, + *qubit.__all__, "pulses", ] From bf32a83adeb821a437566363c340857dd4ca9bfa Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:27:16 +0100 Subject: [PATCH 10/43] add first qubit tests --- tests/components/test_qubit.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/components/test_qubit.py diff --git a/tests/components/test_qubit.py b/tests/components/test_qubit.py new file mode 100644 index 00000000..3c395b82 --- /dev/null +++ b/tests/components/test_qubit.py @@ -0,0 +1,11 @@ +from quam.components import Qubit + + +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" From e25d10a2fafd228496aecfd92fcf298a81272de5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:28:20 +0100 Subject: [PATCH 11/43] add int type to qubit.id --- quam/components/qubit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quam/components/qubit.py b/quam/components/qubit.py index 8df74b49..fe6e032a 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -21,7 +21,7 @@ @quam_dataclass class Qubit(QuamComponent): - id: str + id: Union[str, int] gates: Dict[str, SingleQubitGateImplementation] = field(default_factory=dict) @property From 2159c93a63f7c53823f94fbdef39562288ebfe00 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:32:16 +0100 Subject: [PATCH 12/43] add channels --- quam/components/qubit.py | 10 +++++++++ tests/components/test_qubit.py | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/quam/components/qubit.py b/quam/components/qubit.py index fe6e032a..07895cdd 100644 --- a/quam/components/qubit.py +++ b/quam/components/qubit.py @@ -28,6 +28,16 @@ class Qubit(QuamComponent): def name(self): return self.id if isinstance(self.id, str) else f"q{self.id}" + @property + def channels(self): + return { + key: val + for key, val in self.get_attrs( + follow_references=True, include_defaults=True + ).items() + if isinstance(val, Channel) + } + def __matmul__(self, other): """Allows access to qubit pairs using the '@' operator, e.g. (q1 @ q2)""" if not isinstance(other, Qubit): diff --git a/tests/components/test_qubit.py b/tests/components/test_qubit.py index 3c395b82..931e2c6d 100644 --- a/tests/components/test_qubit.py +++ b/tests/components/test_qubit.py @@ -1,4 +1,6 @@ from quam.components import Qubit +from quam.components.channels import IQChannel +from quam.core.quam_classes import quam_dataclass def test_qubit_name_int(): @@ -9,3 +11,42 @@ def test_qubit_name_int(): def test_qubit_name_str(): qubit = Qubit(id="qubit0") assert qubit.name == "qubit0" + + +@quam_dataclass +class TestQubit(Qubit): + xy: IQChannel + resonator: IQChannel + + +def test_qubit_channels(): + qubit = TestQubit( + id=0, + xy=IQChannel( + id="xy", + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=None, + ), + resonator=IQChannel( + id="resonator", + opx_output_I=("con1", 3), + opx_output_Q=("con1", 4), + frequency_converter_up=None, + ), + ) + assert qubit.channels == {"xy": qubit.xy} + + +def test_qubit_channels_referenced(): + qubit = TestQubit( + id=0, + xy=IQChannel( + id="xy", + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=None, + ), + resonator="#./xy", + ) + assert qubit.channels == {"xy": qubit.xy, "resonator": qubit.xy} From 5348c5d6a713809bbbeac8f8a64c1ac91cdbd323 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:54:49 +0100 Subject: [PATCH 13/43] Cleaning up qubit and operations --- quam/components/__init__.py | 2 +- quam/components/operations/__init__.py | 10 ++++ quam/components/operations/base_operation.py | 9 +++ .../qubit_operations.py} | 23 +++----- .../qubit_pair_operations.py} | 59 ++++++++----------- .../components/quantum_components/__init__.py | 0 .../quantum_components/quantum_component.py | 25 ++++++++ .../{ => quantum_components}/qubit.py | 32 +++++----- .../quantum_components/qubit_pair.py | 17 ++++++ quam/components/qubit_pair.py | 15 ----- 10 files changed, 112 insertions(+), 80 deletions(-) create mode 100644 quam/components/operations/__init__.py create mode 100644 quam/components/operations/base_operation.py rename quam/components/{gate_implementations/single_qubit_gate_implementations.py => operations/qubit_operations.py} (60%) rename quam/components/{gate_implementations/two_qubit_gate_implementations.py => operations/qubit_pair_operations.py} (52%) create mode 100644 quam/components/quantum_components/__init__.py create mode 100644 quam/components/quantum_components/quantum_component.py rename quam/components/{ => quantum_components}/qubit.py (68%) create mode 100644 quam/components/quantum_components/qubit_pair.py delete mode 100644 quam/components/qubit_pair.py diff --git a/quam/components/__init__.py b/quam/components/__init__.py index 76bd1a16..73f0d989 100644 --- a/quam/components/__init__.py +++ b/quam/components/__init__.py @@ -3,7 +3,7 @@ from .octave import * from .channels import * from . import pulses -from .qubit import * +from .quantum_components.qubit import * __all__ = [ *basic_quam.__all__, diff --git a/quam/components/operations/__init__.py b/quam/components/operations/__init__.py new file mode 100644 index 00000000..40e6fecc --- /dev/null +++ b/quam/components/operations/__init__.py @@ -0,0 +1,10 @@ +from .base_operation import * +from .qubit_operations import * +from .qubit_pair_operations import * + + +__all__ = [ + *base_operation.__all__, + *qubit_operations.__all__, + *qubit_pair_operations.__all__, +] diff --git a/quam/components/operations/base_operation.py b/quam/components/operations/base_operation.py new file mode 100644 index 00000000..b681493b --- /dev/null +++ b/quam/components/operations/base_operation.py @@ -0,0 +1,9 @@ +from quam.core.quam_classes import quam_dataclass, QuamComponent + + +__all__ = ["BaseOperation"] + + +@quam_dataclass +class BaseOperation(QuamComponent): + id: str diff --git a/quam/components/gate_implementations/single_qubit_gate_implementations.py b/quam/components/operations/qubit_operations.py similarity index 60% rename from quam/components/gate_implementations/single_qubit_gate_implementations.py rename to quam/components/operations/qubit_operations.py index a1a11d7f..b08ac33f 100644 --- a/quam/components/gate_implementations/single_qubit_gate_implementations.py +++ b/quam/components/operations/qubit_operations.py @@ -1,33 +1,28 @@ from abc import ABC, abstractmethod +from quam.components.operations.base_operation import BaseOperation from quam.components.pulses import Pulse -from quam.core import quam_dataclass, QuamComponent +from quam.core import quam_dataclass + + +__all__ = ["QubitOperation", "QubitPulseOperation"] @quam_dataclass -class SingleQubitGateImplementation(QuamComponent, ABC): +class QubitOperation(BaseOperation, ABC): @property def qubit(self): - from ..qubit import Qubit + from quam.components.quantum_components.qubit import Qubit if isinstance(self.parent, Qubit): return self.parent elif hasattr(self.parent, "parent") and isinstance(self.parent.parent, Qubit): return self.parent.parent else: - raise AttributeError( - "SingleQubitGate is not attached to a qubit. 1Q_gate: {self}" - ) - - def __call__(self): - self.execute() - - @abstractmethod - def execute(self, *args, **kwargs): # TODO Accomodate differing arguments - pass + raise AttributeError("QubitOperation is not attached to a qubit: {self}") @quam_dataclass -class SinglePulseGateImplementation(SingleQubitGateImplementation): +class QubitPulseOperation(QubitOperation): """Single-qubit gate for a qubit consisting of a single pulse Args: diff --git a/quam/components/gate_implementations/two_qubit_gate_implementations.py b/quam/components/operations/qubit_pair_operations.py similarity index 52% rename from quam/components/gate_implementations/two_qubit_gate_implementations.py rename to quam/components/operations/qubit_pair_operations.py index dbb9ccb5..5b1df2dc 100644 --- a/quam/components/gate_implementations/two_qubit_gate_implementations.py +++ b/quam/components/operations/qubit_pair_operations.py @@ -1,21 +1,21 @@ -from abc import ABC, abstractmethod -from typing import Dict -from dataclasses import field +from abc import ABC from copy import copy from quam.components.pulses import Pulse -from quam.core import quam_dataclass, QuamComponent +from quam.components.operations import BaseOperation +from quam.components.quantum_components.qubit import Qubit +from quam.core import quam_dataclass from quam.utils import string_reference as str_ref -__all__ = ["TwoQubitGateImplementation", "CZGateImplementation"] +__all__ = ["QubitPairOperation", "CZOperation"] @quam_dataclass -class TwoQubitGateImplementation(QuamComponent, ABC): +class QubitPairOperation(BaseOperation, ABC): @property - def qubit_pair(self): - from ..qubit_pair import QubitPair + def qubit_pair(self): # TODO Add QubitPair return type + from quam.components.quantum_components.qubit_pair import QubitPair if isinstance(self.parent, QubitPair): return self.parent @@ -29,25 +29,18 @@ def qubit_pair(self): ) @property - def qubit_control(self): + def qubit_control(self) -> Qubit: return self.qubit_pair.qubit_control @property - def qubit_target(self): + def qubit_target(self) -> Qubit: return self.qubit_pair.qubit_target - def __call__(self): - self.execute() - @quam_dataclass -class CZGateImplementation(TwoQubitGateImplementation): +class CZOperation(QubitPairOperation): """CZ Operation for a qubit pair""" - # Pulses will be added to qubit elements - # The reason we don't add "flux_to_q1" directly to q1.z is because it is part of - # the CZ operation, i.e. it is only applied as part of a CZ operation - flux_pulse_control: Pulse phase_shift_control: float = 0.0 @@ -69,7 +62,7 @@ def flux_pulse_control_label(self) -> str: return f"{self.gate_label}{str_ref.DELIMITER}{pulse_label}" - def execute(self, amplitude_scale=None): + def execute(self, *, amplitude_scale=None): self.qubit_control.z.play( self.flux_pulse_control_label, validate=False, @@ -79,27 +72,23 @@ def execute(self, amplitude_scale=None): self.qubit_control.xy.frame_rotation(self.phase_shift_control) self.qubit_target.xy.frame_rotation(self.phase_shift_target) - self.qubit_control.align(self.qubit_target) + self.qubit_pair.align() @property def config_settings(self): return {"after": [self.qubit_control.z]} def apply_to_config(self, config: dict) -> None: - pulse = copy(self.flux_pulse_control) - pulse.id = self.flux_pulse_control_label - pulse.parent = None # Reset parent so it can be attached to new parent - pulse.parent = self.qubit_control.z - - if self.flux_pulse_control_label in self.qubit_control.z.operations: - raise ValueError( - "Pulse name already exists in pulse operations. " - f"Channel: {self.qubit_control.z.get_reference()}, " - f"Pulse: {self.flux_pulse_control.get_reference()}, " - f"Pulse name: {self.flux_pulse_control_label}" - ) + if self.flux_pulse_control.parent is self: + + pulse = copy(self.flux_pulse_control) + pulse.id = self.flux_pulse_control_label + pulse.parent = None # Reset parent so it can be attached to new parent + pulse.parent = self.qubit_control.z - pulse.apply_to_config(config) + self.flux_pulse_control.apply_to_config(config) - element_config = config["elements"][self.qubit_control.z.name] - element_config["operations"][self.flux_pulse_control_label] = pulse.pulse_name + element_config = config["elements"][self.qubit_control.z.name] + element_config["operations"][ + self.flux_pulse_control_label + ] = pulse.pulse_name diff --git a/quam/components/quantum_components/__init__.py b/quam/components/quantum_components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py new file mode 100644 index 00000000..89e01ba3 --- /dev/null +++ b/quam/components/quantum_components/quantum_component.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from dataclasses import field +from typing import Any, Dict, Union +from quam.core.quam_classes import quam_dataclass, QuamComponent +from quam.components.operations import BaseOperation + +__all__ = ["QuantumComponent"] + + +@quam_dataclass +class QuantumComponent(QuamComponent, ABC): + id: Union[str, int] + operations: Dict[str, BaseOperation] = field(default_factory=dict) + + @property + @abstractmethod + def name(self) -> str: + pass + + def __call__(self): + self.execute() + + @abstractmethod + def execute(self, *args, **kwargs) -> Any: + pass diff --git a/quam/components/qubit.py b/quam/components/quantum_components/qubit.py similarity index 68% rename from quam/components/qubit.py rename to quam/components/quantum_components/qubit.py index 07895cdd..cc782bf1 100644 --- a/quam/components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -1,19 +1,11 @@ from typing import Dict, Union from dataclasses import field +from qm import qua + from quam.components.channels import Channel from quam.core import quam_dataclass, QuamComponent -from .gate_implementations.single_qubit_gate_implementations import ( - SingleQubitGateImplementation, -) - -from qm.qua._type_hinting import ( - AmpValuesType, - QuaNumberType, - QuaExpressionType, - ChirpType, - StreamType, -) +from quam.components.operations import QubitOperation __all__ = ["Qubit"] @@ -22,14 +14,16 @@ @quam_dataclass class Qubit(QuamComponent): id: Union[str, int] - gates: Dict[str, SingleQubitGateImplementation] = field(default_factory=dict) + gates: Dict[str, QubitOperation] = field(default_factory=dict) @property - def name(self): + def name(self) -> str: + """Returns the name of the qubit""" return self.id if isinstance(self.id, str) else f"q{self.id}" @property - def channels(self): + def channels(self) -> Dict[str, Channel]: + """Returns a dictionary of all channels of the qubit""" return { key: val for key, val in self.get_attrs( @@ -38,7 +32,15 @@ def channels(self): if isinstance(val, Channel) } - def __matmul__(self, other): + def align(self, *other_qubits: "Qubit"): + """Aligns the execution of all channels of this qubit and all other qubits""" + channel_names = [channel.name for channel in self.channels.values()] + for qubit in other_qubits: + channel_names.extend([channel.name for channel in qubit.channels.values()]) + + qua.align(*channel_names) + + def __matmul__(self, other): # TODO Add QubitPair return type """Allows access to qubit pairs using the '@' operator, e.g. (q1 @ q2)""" if not isinstance(other, Qubit): raise ValueError( diff --git a/quam/components/quantum_components/qubit_pair.py b/quam/components/quantum_components/qubit_pair.py new file mode 100644 index 00000000..8551261e --- /dev/null +++ b/quam/components/quantum_components/qubit_pair.py @@ -0,0 +1,17 @@ +from typing import Dict +from dataclasses import field + +from quam.core import quam_dataclass, QuamComponent +from quam.components.quantum_components.qubit import Qubit +from quam.components.operations import QubitPairOperation + + +@quam_dataclass +class QubitPair(QuamComponent): + qubit_control: Qubit + qubit_target: Qubit + gates: Dict[str, QubitPairOperation] = field(default_factory=dict) + + def align(self): + """Aligns the execution of all channels of both qubits""" + self.qubit_control.align(self.qubit_target) diff --git a/quam/components/qubit_pair.py b/quam/components/qubit_pair.py deleted file mode 100644 index 866b7014..00000000 --- a/quam/components/qubit_pair.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Dict -from dataclasses import field - -from quam.core import quam_dataclass, QuamComponent -from quam.components.qubit import Qubit -from quam.components.gate_implementations.two_qubit_gate_implementations import ( - TwoQubitGateImplementation, -) - - -@quam_dataclass -class QubitPair(QuamComponent): - qubit_control: Qubit - qubit_target: Qubit - gates: Dict[str, TwoQubitGateImplementation] = field(default_factory=dict) From cb8336bf9ac43dd1a69815bb4204336fc160d346 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 20:58:21 +0100 Subject: [PATCH 14/43] small fixes to operations --- quam/components/operations/base_operation.py | 7 ++++++- quam/components/quantum_components/quantum_component.py | 9 +++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/quam/components/operations/base_operation.py b/quam/components/operations/base_operation.py index b681493b..0399094e 100644 --- a/quam/components/operations/base_operation.py +++ b/quam/components/operations/base_operation.py @@ -1,3 +1,4 @@ +from abc import ABC, abstractmethod from quam.core.quam_classes import quam_dataclass, QuamComponent @@ -5,5 +6,9 @@ @quam_dataclass -class BaseOperation(QuamComponent): +class BaseOperation(QuamComponent, ABC): id: str + + @abstractmethod + def execute(self, *args, **kwargs): + pass diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index 89e01ba3..4abdcfb8 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -17,9 +17,6 @@ class QuantumComponent(QuamComponent, ABC): def name(self) -> str: pass - def __call__(self): - self.execute() - - @abstractmethod - def execute(self, *args, **kwargs) -> Any: - pass + def apply(self, operation: str, *args, **kwargs) -> Any: + operation_obj = self.operations[operation] + operation_obj.execute(*args, **kwargs) From 60e84794e78435e0c65f97d31e9d1158dd3b6dd5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 26 Nov 2024 21:00:05 +0100 Subject: [PATCH 15/43] rename execute -> apply --- quam/components/operations/base_operation.py | 3 ++- quam/components/operations/qubit_operations.py | 2 +- quam/components/operations/qubit_pair_operations.py | 2 +- quam/components/quantum_components/quantum_component.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/quam/components/operations/base_operation.py b/quam/components/operations/base_operation.py index 0399094e..e4ec9237 100644 --- a/quam/components/operations/base_operation.py +++ b/quam/components/operations/base_operation.py @@ -10,5 +10,6 @@ class BaseOperation(QuamComponent, ABC): id: str @abstractmethod - def execute(self, *args, **kwargs): + def apply(self, *args, **kwargs): + """Applies the operation""" pass diff --git a/quam/components/operations/qubit_operations.py b/quam/components/operations/qubit_operations.py index b08ac33f..6ae35b3c 100644 --- a/quam/components/operations/qubit_operations.py +++ b/quam/components/operations/qubit_operations.py @@ -33,5 +33,5 @@ class QubitPulseOperation(QubitOperation): pulse: Pulse - def execute(self, *, amplitude_scale=None, duration=None, **kwargs): + def apply(self, *, amplitude_scale=None, duration=None, **kwargs): self.pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) diff --git a/quam/components/operations/qubit_pair_operations.py b/quam/components/operations/qubit_pair_operations.py index 5b1df2dc..b6906280 100644 --- a/quam/components/operations/qubit_pair_operations.py +++ b/quam/components/operations/qubit_pair_operations.py @@ -62,7 +62,7 @@ def flux_pulse_control_label(self) -> str: return f"{self.gate_label}{str_ref.DELIMITER}{pulse_label}" - def execute(self, *, amplitude_scale=None): + def apply(self, *, amplitude_scale=None): self.qubit_control.z.play( self.flux_pulse_control_label, validate=False, diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index 4abdcfb8..fb89cb42 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -19,4 +19,4 @@ def name(self) -> str: def apply(self, operation: str, *args, **kwargs) -> Any: operation_obj = self.operations[operation] - operation_obj.execute(*args, **kwargs) + operation_obj.apply(*args, **kwargs) From 921f89265f9e718542da6ecf86878e36584c24f3 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 27 Nov 2024 13:41:35 +0100 Subject: [PATCH 16/43] Added Pulse.play() method. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d1f00b..d23e9b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added - Added time tagging to channels +- Added `Pulse.play()` method ### Fixed - Change location of port feedforward and feedback filters in config From 8bdd548b873cb6e6df5216e31479222f0af14108 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 27 Nov 2024 14:19:05 +0100 Subject: [PATCH 17/43] refactoring --- quam/components/implementations/__init__.py | 10 +++++ .../base_implementation.py} | 4 +- .../qubit_implementations.py} | 8 ++-- .../qubit_pair_implementations.py | 37 +++++++++++++++++++ quam/components/operations/__init__.py | 10 ----- .../quantum_components/quantum_component.py | 4 +- quam/components/quantum_components/qubit.py | 4 +- .../quantum_components/qubit_pair.py | 4 +- .../implementations.py} | 37 ++----------------- 9 files changed, 62 insertions(+), 56 deletions(-) create mode 100644 quam/components/implementations/__init__.py rename quam/components/{operations/base_operation.py => implementations/base_implementation.py} (75%) rename quam/components/{operations/qubit_operations.py => implementations/qubit_implementations.py} (79%) create mode 100644 quam/components/implementations/qubit_pair_implementations.py delete mode 100644 quam/components/operations/__init__.py rename quam/{components/operations/qubit_pair_operations.py => examples/superconducting_qubits/implementations.py} (65%) diff --git a/quam/components/implementations/__init__.py b/quam/components/implementations/__init__.py new file mode 100644 index 00000000..632f7e0a --- /dev/null +++ b/quam/components/implementations/__init__.py @@ -0,0 +1,10 @@ +from .base_implementation import * +from .qubit_implementations import * +from .qubit_pair_implementations import * + + +__all__ = [ + *base_implementation.__all__, + *qubit_implementations.__all__, + *qubit_pair_implementations.__all__, +] diff --git a/quam/components/operations/base_operation.py b/quam/components/implementations/base_implementation.py similarity index 75% rename from quam/components/operations/base_operation.py rename to quam/components/implementations/base_implementation.py index e4ec9237..fb960ca3 100644 --- a/quam/components/operations/base_operation.py +++ b/quam/components/implementations/base_implementation.py @@ -2,11 +2,11 @@ from quam.core.quam_classes import quam_dataclass, QuamComponent -__all__ = ["BaseOperation"] +__all__ = ["BaseImplementation"] @quam_dataclass -class BaseOperation(QuamComponent, ABC): +class BaseImplementation(QuamComponent, ABC): id: str @abstractmethod diff --git a/quam/components/operations/qubit_operations.py b/quam/components/implementations/qubit_implementations.py similarity index 79% rename from quam/components/operations/qubit_operations.py rename to quam/components/implementations/qubit_implementations.py index 6ae35b3c..fccba634 100644 --- a/quam/components/operations/qubit_operations.py +++ b/quam/components/implementations/qubit_implementations.py @@ -1,14 +1,14 @@ from abc import ABC, abstractmethod -from quam.components.operations.base_operation import BaseOperation +from quam.components.implementations.base_implementation import BaseImplementation from quam.components.pulses import Pulse from quam.core import quam_dataclass -__all__ = ["QubitOperation", "QubitPulseOperation"] +__all__ = ["QubitImplementation", "PulseGateImplementation"] @quam_dataclass -class QubitOperation(BaseOperation, ABC): +class QubitImplementation(BaseImplementation, ABC): @property def qubit(self): from quam.components.quantum_components.qubit import Qubit @@ -22,7 +22,7 @@ def qubit(self): @quam_dataclass -class QubitPulseOperation(QubitOperation): +class PulseGateImplementation(QubitImplementation): """Single-qubit gate for a qubit consisting of a single pulse Args: diff --git a/quam/components/implementations/qubit_pair_implementations.py b/quam/components/implementations/qubit_pair_implementations.py new file mode 100644 index 00000000..e5318c32 --- /dev/null +++ b/quam/components/implementations/qubit_pair_implementations.py @@ -0,0 +1,37 @@ +from abc import ABC +from copy import copy + +from quam.components.pulses import Pulse +from quam.components.implementations import BaseImplementation +from quam.components.quantum_components.qubit import Qubit +from quam.core import quam_dataclass +from quam.utils import string_reference as str_ref + + +__all__ = ["QubitPairImplementation"] + + +@quam_dataclass +class QubitPairImplementation(BaseImplementation, ABC): + @property + def qubit_pair(self): # TODO Add QubitPair return type + from quam.components.quantum_components.qubit_pair import QubitPair + + if isinstance(self.parent, QubitPair): + return self.parent + elif hasattr(self.parent, "parent") and isinstance( + self.parent.parent, QubitPair + ): + return self.parent.parent + else: + raise AttributeError( + "TwoQubitGate is not attached to a QubitPair. 2Q_gate: {self}" + ) + + @property + def qubit_control(self) -> Qubit: + return self.qubit_pair.qubit_control + + @property + def qubit_target(self) -> Qubit: + return self.qubit_pair.qubit_target diff --git a/quam/components/operations/__init__.py b/quam/components/operations/__init__.py deleted file mode 100644 index 40e6fecc..00000000 --- a/quam/components/operations/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .base_operation import * -from .qubit_operations import * -from .qubit_pair_operations import * - - -__all__ = [ - *base_operation.__all__, - *qubit_operations.__all__, - *qubit_pair_operations.__all__, -] diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index fb89cb42..26bb35e8 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -2,7 +2,7 @@ from dataclasses import field from typing import Any, Dict, Union from quam.core.quam_classes import quam_dataclass, QuamComponent -from quam.components.operations import BaseOperation +from quam.components.implementations import BaseImplementation __all__ = ["QuantumComponent"] @@ -10,7 +10,7 @@ @quam_dataclass class QuantumComponent(QuamComponent, ABC): id: Union[str, int] - operations: Dict[str, BaseOperation] = field(default_factory=dict) + operations: Dict[str, BaseImplementation] = field(default_factory=dict) @property @abstractmethod diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index cc782bf1..a4fbd0d8 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -5,7 +5,7 @@ from quam.components.channels import Channel from quam.core import quam_dataclass, QuamComponent -from quam.components.operations import QubitOperation +from quam.components.implementations import QubitImplementation __all__ = ["Qubit"] @@ -14,7 +14,7 @@ @quam_dataclass class Qubit(QuamComponent): id: Union[str, int] - gates: Dict[str, QubitOperation] = field(default_factory=dict) + gates: Dict[str, QubitImplementation] = field(default_factory=dict) @property def name(self) -> str: diff --git a/quam/components/quantum_components/qubit_pair.py b/quam/components/quantum_components/qubit_pair.py index 8551261e..b922757a 100644 --- a/quam/components/quantum_components/qubit_pair.py +++ b/quam/components/quantum_components/qubit_pair.py @@ -3,14 +3,14 @@ from quam.core import quam_dataclass, QuamComponent from quam.components.quantum_components.qubit import Qubit -from quam.components.operations import QubitPairOperation +from quam.components.implementations import QubitPairImplementation @quam_dataclass class QubitPair(QuamComponent): qubit_control: Qubit qubit_target: Qubit - gates: Dict[str, QubitPairOperation] = field(default_factory=dict) + gates: Dict[str, QubitPairImplementation] = field(default_factory=dict) def align(self): """Aligns the execution of all channels of both qubits""" diff --git a/quam/components/operations/qubit_pair_operations.py b/quam/examples/superconducting_qubits/implementations.py similarity index 65% rename from quam/components/operations/qubit_pair_operations.py rename to quam/examples/superconducting_qubits/implementations.py index b6906280..46c2e228 100644 --- a/quam/components/operations/qubit_pair_operations.py +++ b/quam/examples/superconducting_qubits/implementations.py @@ -1,44 +1,13 @@ -from abc import ABC from copy import copy -from quam.components.pulses import Pulse -from quam.components.operations import BaseOperation -from quam.components.quantum_components.qubit import Qubit from quam.core import quam_dataclass +from quam.components.pulses import Pulse +from quam.components.implementations import QubitPairImplementation from quam.utils import string_reference as str_ref -__all__ = ["QubitPairOperation", "CZOperation"] - - -@quam_dataclass -class QubitPairOperation(BaseOperation, ABC): - @property - def qubit_pair(self): # TODO Add QubitPair return type - from quam.components.quantum_components.qubit_pair import QubitPair - - if isinstance(self.parent, QubitPair): - return self.parent - elif hasattr(self.parent, "parent") and isinstance( - self.parent.parent, QubitPair - ): - return self.parent.parent - else: - raise AttributeError( - "TwoQubitGate is not attached to a QubitPair. 2Q_gate: {self}" - ) - - @property - def qubit_control(self) -> Qubit: - return self.qubit_pair.qubit_control - - @property - def qubit_target(self) -> Qubit: - return self.qubit_pair.qubit_target - - @quam_dataclass -class CZOperation(QubitPairOperation): +class CZImplementation(QubitPairImplementation): """CZ Operation for a qubit pair""" flux_pulse_control: Pulse From dd1f54efc4e619cf333b713822e2056ceffbd7a1 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 27 Nov 2024 15:02:56 +0100 Subject: [PATCH 18/43] adding specific implementations --- .../implementations/qubit_implementations.py | 23 +++++++++++++++++++ .../qubit_pair_implementations.py | 3 --- ...mplementations.py => cz_implementation.py} | 0 3 files changed, 23 insertions(+), 3 deletions(-) rename quam/examples/superconducting_qubits/{implementations.py => cz_implementation.py} (100%) diff --git a/quam/components/implementations/qubit_implementations.py b/quam/components/implementations/qubit_implementations.py index fccba634..e42db3d8 100644 --- a/quam/components/implementations/qubit_implementations.py +++ b/quam/components/implementations/qubit_implementations.py @@ -1,8 +1,12 @@ from abc import ABC, abstractmethod +from typing import TYPE_CHECKING from quam.components.implementations.base_implementation import BaseImplementation from quam.components.pulses import Pulse from quam.core import quam_dataclass +if TYPE_CHECKING: + from qm.qua import QuaVariableType + __all__ = ["QubitImplementation", "PulseGateImplementation"] @@ -35,3 +39,22 @@ class PulseGateImplementation(QubitImplementation): def apply(self, *, amplitude_scale=None, duration=None, **kwargs): self.pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) + + +@quam_dataclass +class MeasureImplementation(QubitImplementation): + + def apply(self, **kwargs) -> QuaVariableType: + return self.qubit.measure(**kwargs) + + +@quam_dataclass +class AlignImplementation(QubitImplementation): + def apply(self, *other_qubits, **kwargs): + self.qubit.align(*other_qubits, **kwargs) + + +@quam_dataclass +class ResetImplementation(QubitImplementation): + def apply(self, **kwargs): + self.qubit.reset(**kwargs) diff --git a/quam/components/implementations/qubit_pair_implementations.py b/quam/components/implementations/qubit_pair_implementations.py index e5318c32..a3af5034 100644 --- a/quam/components/implementations/qubit_pair_implementations.py +++ b/quam/components/implementations/qubit_pair_implementations.py @@ -1,11 +1,8 @@ from abc import ABC -from copy import copy -from quam.components.pulses import Pulse from quam.components.implementations import BaseImplementation from quam.components.quantum_components.qubit import Qubit from quam.core import quam_dataclass -from quam.utils import string_reference as str_ref __all__ = ["QubitPairImplementation"] diff --git a/quam/examples/superconducting_qubits/implementations.py b/quam/examples/superconducting_qubits/cz_implementation.py similarity index 100% rename from quam/examples/superconducting_qubits/implementations.py rename to quam/examples/superconducting_qubits/cz_implementation.py From a46b9087eb833359314b39bfdd3a3e1c5c2bc971 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 27 Nov 2024 20:03:43 +0100 Subject: [PATCH 19/43] Register operations --- .../implementations/qubit_implementations.py | 2 +- .../components/quantum_components/__init__.py | 4 ++ .../quantum_components/quantum_component.py | 4 +- quam/components/quantum_components/qubit.py | 2 +- .../quantum_components/qubit_pair.py | 2 +- quam/core/__init__.py | 1 + quam/core/operations_registry.py | 30 ++++++++++++ .../cz_implementation.py | 47 +------------------ .../superconducting_qubits/operations.py | 31 ++++++++++++ tests/operations/test_register_operations.py | 14 ++++++ 10 files changed, 87 insertions(+), 50 deletions(-) create mode 100644 quam/core/operations_registry.py create mode 100644 quam/examples/superconducting_qubits/operations.py create mode 100644 tests/operations/test_register_operations.py diff --git a/quam/components/implementations/qubit_implementations.py b/quam/components/implementations/qubit_implementations.py index e42db3d8..fe7e8f71 100644 --- a/quam/components/implementations/qubit_implementations.py +++ b/quam/components/implementations/qubit_implementations.py @@ -1,4 +1,4 @@ -from abc import ABC, abstractmethod +from abc import ABC from typing import TYPE_CHECKING from quam.components.implementations.base_implementation import BaseImplementation from quam.components.pulses import Pulse diff --git a/quam/components/quantum_components/__init__.py b/quam/components/quantum_components/__init__.py index e69de29b..e8887cfe 100644 --- a/quam/components/quantum_components/__init__.py +++ b/quam/components/quantum_components/__init__.py @@ -0,0 +1,4 @@ +from quam.components.quantum_components.qubit import Qubit +from quam.components.quantum_components.qubit_pair import QubitPair + +__all__ = ["Qubit", "QubitPair"] diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index 26bb35e8..8776a27d 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -10,7 +10,7 @@ @quam_dataclass class QuantumComponent(QuamComponent, ABC): id: Union[str, int] - operations: Dict[str, BaseImplementation] = field(default_factory=dict) + implementations: Dict[str, BaseImplementation] = field(default_factory=dict) @property @abstractmethod @@ -18,5 +18,5 @@ def name(self) -> str: pass def apply(self, operation: str, *args, **kwargs) -> Any: - operation_obj = self.operations[operation] + operation_obj = self.implementations[operation] operation_obj.apply(*args, **kwargs) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index a4fbd0d8..b3f826b6 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -14,7 +14,7 @@ @quam_dataclass class Qubit(QuamComponent): id: Union[str, int] - gates: Dict[str, QubitImplementation] = field(default_factory=dict) + implementations: Dict[str, QubitImplementation] = field(default_factory=dict) @property def name(self) -> str: diff --git a/quam/components/quantum_components/qubit_pair.py b/quam/components/quantum_components/qubit_pair.py index b922757a..b0c8bb12 100644 --- a/quam/components/quantum_components/qubit_pair.py +++ b/quam/components/quantum_components/qubit_pair.py @@ -10,7 +10,7 @@ class QubitPair(QuamComponent): qubit_control: Qubit qubit_target: Qubit - gates: Dict[str, QubitPairImplementation] = field(default_factory=dict) + implementations: Dict[str, QubitPairImplementation] = field(default_factory=dict) def align(self): """Aligns the execution of all channels of both qubits""" diff --git a/quam/core/__init__.py b/quam/core/__init__.py index 9795b884..cc6e342c 100644 --- a/quam/core/__init__.py +++ b/quam/core/__init__.py @@ -1,4 +1,5 @@ from .quam_classes import * +from .operations_registry import OperationsRegistry # Exec statement needed to trick Pycharm type checker into recognizing it as a dataclass diff --git a/quam/core/operations_registry.py b/quam/core/operations_registry.py new file mode 100644 index 00000000..1ca5fb16 --- /dev/null +++ b/quam/core/operations_registry.py @@ -0,0 +1,30 @@ +from collections import UserDict +import functools +from typing import Callable + + +class OperationsRegistry(UserDict): + """A registry to store and manage operations.""" + + def register_operation(self, func: Callable) -> Callable: + """ + Register a function as an operation. + + This method stores the function in the operations dictionary and returns a wrapped version of the function + that maintains the original function's signature and docstring. + + Args: + func (callable): The function to register as an operation. + + Returns: + callable: The wrapped function. + """ + + @functools.wraps(func) + def wrapped_operation(*args, **kwargs): + """Call the registered operation with the provided arguments.""" + return func(*args, **kwargs) + + self[func.__name__] = wrapped_operation + + return wrapped_operation diff --git a/quam/examples/superconducting_qubits/cz_implementation.py b/quam/examples/superconducting_qubits/cz_implementation.py index 46c2e228..daba838e 100644 --- a/quam/examples/superconducting_qubits/cz_implementation.py +++ b/quam/examples/superconducting_qubits/cz_implementation.py @@ -1,63 +1,20 @@ -from copy import copy - from quam.core import quam_dataclass from quam.components.pulses import Pulse from quam.components.implementations import QubitPairImplementation -from quam.utils import string_reference as str_ref @quam_dataclass class CZImplementation(QubitPairImplementation): """CZ Operation for a qubit pair""" - flux_pulse_control: Pulse + flux_pulse: Pulse phase_shift_control: float = 0.0 phase_shift_target: float = 0.0 - @property - def gate_label(self) -> str: - try: - return self.parent.get_attr_name(self) - except AttributeError: - return "CZ" - - @property - def flux_pulse_control_label(self) -> str: - if self.flux_pulse_control.id is not None: - pulse_label = self.flux_pulse_control.id - else: - pulse_label = "flux_pulse_control" - - return f"{self.gate_label}{str_ref.DELIMITER}{pulse_label}" - def apply(self, *, amplitude_scale=None): - self.qubit_control.z.play( - self.flux_pulse_control_label, - validate=False, - amplitude_scale=amplitude_scale, - ) + self.flux_pulse.play(amplitude_scale=amplitude_scale) self.qubit_control.align(self.qubit_target) - self.qubit_control.xy.frame_rotation(self.phase_shift_control) self.qubit_target.xy.frame_rotation(self.phase_shift_target) self.qubit_pair.align() - - @property - def config_settings(self): - return {"after": [self.qubit_control.z]} - - def apply_to_config(self, config: dict) -> None: - if self.flux_pulse_control.parent is self: - - pulse = copy(self.flux_pulse_control) - pulse.id = self.flux_pulse_control_label - pulse.parent = None # Reset parent so it can be attached to new parent - pulse.parent = self.qubit_control.z - - self.flux_pulse_control.apply_to_config(config) - - element_config = config["elements"][self.qubit_control.z.name] - element_config["operations"][ - self.flux_pulse_control_label - ] = pulse.pulse_name diff --git a/quam/examples/superconducting_qubits/operations.py b/quam/examples/superconducting_qubits/operations.py new file mode 100644 index 00000000..14f1bede --- /dev/null +++ b/quam/examples/superconducting_qubits/operations.py @@ -0,0 +1,31 @@ +from qm.qua import QuaVariableType +from quam.components import Qubit +from quam.core import OperationsRegistry + +operations_registry = OperationsRegistry() + + +@operations_registry.register_operation +def x(qubit: Qubit, **kwargs): + qubit.apply("X", **kwargs) + + +@operations_registry.register_operation +def y(qubit: Qubit, **kwargs): + qubit.apply("Y", **kwargs) + + +@operations_registry.register_operation +def cz(qubit_control: Qubit, qubit_target: Qubit, **kwargs): + qubit_pair = qubit_control @ qubit_target + qubit_pair.apply("CZ", **kwargs) + + +@operations_registry.register_operation +def measure(qubit: Qubit, **kwargs) -> QuaVariableType: + return qubit.measure(**kwargs) + + +@operations_registry.register_operation +def align(*qubits: Qubit): + qubits[0].align(*qubits[1:]) diff --git a/tests/operations/test_register_operations.py b/tests/operations/test_register_operations.py new file mode 100644 index 00000000..a291c167 --- /dev/null +++ b/tests/operations/test_register_operations.py @@ -0,0 +1,14 @@ +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 711cdd9e25173b3f067b29a84b9ea78810ca1a84 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 27 Nov 2024 20:45:31 +0100 Subject: [PATCH 20/43] Fixed circular references, beginning on doc --- docs/examples/Qubit operations.ipynb | 133 ++++++++++++++++++ quam/components/__init__.py | 6 +- .../implementations/base_implementation.py | 15 +- .../implementations/qubit_implementations.py | 4 +- .../qubit_pair_implementations.py | 4 +- quam/components/quantum_components/qubit.py | 11 +- .../quantum_components/qubit_pair.py | 12 +- quam/core/operations_registry.py | 5 +- quam/utils/reference_class.py | 7 +- 9 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 docs/examples/Qubit operations.ipynb diff --git a/docs/examples/Qubit operations.ipynb b/docs/examples/Qubit operations.ipynb new file mode 100644 index 00000000..13e7728e --- /dev/null +++ b/docs/examples/Qubit operations.ipynb @@ -0,0 +1,133 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-11-27 20:44:26,117 - qm - INFO - Starting session: 4a68c9e2-51d1-4561-80cf-e16b9e54615c\n" + ] + } + ], + "source": [ + "from quam.core import OperationsRegistry\n", + "from quam.components import Qubit, pulses\n", + "from quam.components.channels import IQChannel\n", + "from quam.core import quam_dataclass, QuamRoot" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define QUAM components" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "@quam_dataclass\n", + "class Transmon(Qubit):\n", + " xy: IQChannel\n", + "\n", + "\n", + "@quam_dataclass\n", + "class QUAM(QuamRoot):\n", + " qubit: Transmon = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create QUAM components" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "machine = QUAM()\n", + "\n", + "qubit = machine.qubit = Transmon(\n", + " id=\"qubit\",\n", + " xy=IQChannel(\n", + " opx_output_I=(\"con1\", 1),\n", + " opx_output_Q=(\"con1\", 2),\n", + " frequency_converter_up=None,\n", + " ),\n", + ")\n", + "\n", + "pulse = pulses.SquarePulse(length=100, amplitude=0.1)\n", + "qubit.xy.operations[\"X\"] = pulse" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'SquarePulse' object has no attribute 'play'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[5], line 8\u001b[0m\n\u001b[1;32m 6\u001b[0m qubit\u001b[38;5;241m.\u001b[39mimplementations[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m PulseGateImplementation(pulse\u001b[38;5;241m=\u001b[39mpulse\u001b[38;5;241m.\u001b[39mget_reference())\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m qubit\u001b[38;5;241m.\u001b[39mimplementations[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mid \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m----> 8\u001b[0m \u001b[43mqubit\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mimplementations\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mX\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Repositories/quam/quam/components/implementations/qubit_implementations.py:39\u001b[0m, in \u001b[0;36mPulseGateImplementation.apply\u001b[0;34m(self, amplitude_scale, duration, **kwargs)\u001b[0m\n\u001b[1;32m 38\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39m, amplitude_scale\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, duration\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m---> 39\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpulse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplay\u001b[49m(amplitude_scale\u001b[38;5;241m=\u001b[39mamplitude_scale, duration\u001b[38;5;241m=\u001b[39mduration, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[0;32m~/Repositories/quam/quam/utils/reference_class.py:38\u001b[0m, in \u001b[0;36mReferenceClass.__getattribute__\u001b[0;34m(self, attr)\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__getattribute__\u001b[39m(\u001b[38;5;28mself\u001b[39m, attr: \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Any:\n\u001b[0;32m---> 38\u001b[0m attr_val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__getattribute__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mattr\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m attr \u001b[38;5;129;01min\u001b[39;00m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_is_reference\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_get_referenced_value\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__post_init__\u001b[39m\u001b[38;5;124m\"\u001b[39m]:\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m attr_val\n", + "\u001b[0;31mAttributeError\u001b[0m: 'SquarePulse' object has no attribute 'play'" + ] + } + ], + "source": [ + "from quam.components.implementations.qubit_implementations import PulseGateImplementation\n", + "\n", + "\n", + "qubit = Qubit(id=\"qubit\")\n", + "\n", + "qubit.implementations[\"X\"] = PulseGateImplementation(pulse=pulse.get_reference())\n", + "assert qubit.implementations[\"X\"].id == \"X\"\n", + "qubit.implementations[\"X\"].apply()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/quam/components/__init__.py b/quam/components/__init__.py index 73f0d989..16290a6b 100644 --- a/quam/components/__init__.py +++ b/quam/components/__init__.py @@ -3,13 +3,15 @@ from .octave import * from .channels import * from . import pulses -from .quantum_components.qubit import * +from .quantum_components import * +from . import implementations __all__ = [ *basic_quam.__all__, *hardware.__all__, *channels.__all__, *octave.__all__, - *qubit.__all__, + *quantum_components.__all__, "pulses", + "implementations", ] diff --git a/quam/components/implementations/base_implementation.py b/quam/components/implementations/base_implementation.py index fb960ca3..3c3f4d0b 100644 --- a/quam/components/implementations/base_implementation.py +++ b/quam/components/implementations/base_implementation.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from quam.core.quam_classes import quam_dataclass, QuamComponent +from quam.utils import string_reference as str_ref __all__ = ["BaseImplementation"] @@ -7,7 +8,19 @@ @quam_dataclass class BaseImplementation(QuamComponent, ABC): - id: str + id: str = "#./inferred_id" + + @property + def inferred_id(self): + 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" + ) @abstractmethod def apply(self, *args, **kwargs): diff --git a/quam/components/implementations/qubit_implementations.py b/quam/components/implementations/qubit_implementations.py index fe7e8f71..9b753970 100644 --- a/quam/components/implementations/qubit_implementations.py +++ b/quam/components/implementations/qubit_implementations.py @@ -1,11 +1,9 @@ from abc import ABC -from typing import TYPE_CHECKING from quam.components.implementations.base_implementation import BaseImplementation from quam.components.pulses import Pulse from quam.core import quam_dataclass -if TYPE_CHECKING: - from qm.qua import QuaVariableType +from qm.qua import QuaVariableType __all__ = ["QubitImplementation", "PulseGateImplementation"] diff --git a/quam/components/implementations/qubit_pair_implementations.py b/quam/components/implementations/qubit_pair_implementations.py index a3af5034..a6976fff 100644 --- a/quam/components/implementations/qubit_pair_implementations.py +++ b/quam/components/implementations/qubit_pair_implementations.py @@ -1,9 +1,11 @@ from abc import ABC +from typing import TYPE_CHECKING from quam.components.implementations import BaseImplementation -from quam.components.quantum_components.qubit import Qubit from quam.core import quam_dataclass +from quam.components.quantum_components.qubit import Qubit + __all__ = ["QubitPairImplementation"] diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index b3f826b6..39cd1442 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -1,11 +1,16 @@ -from typing import Dict, Union +from typing import Dict, Union, TYPE_CHECKING, Any from dataclasses import field from qm import qua from quam.components.channels import Channel from quam.core import quam_dataclass, QuamComponent -from quam.components.implementations import QubitImplementation + +if TYPE_CHECKING: + from quam.components.implementations import QubitImplementation + ImplementationType = QubitImplementation +else: + ImplementationType = Any __all__ = ["Qubit"] @@ -14,7 +19,7 @@ @quam_dataclass class Qubit(QuamComponent): id: Union[str, int] - implementations: Dict[str, QubitImplementation] = field(default_factory=dict) + implementations: Dict[str, ImplementationType] = field(default_factory=dict) @property def name(self) -> str: diff --git a/quam/components/quantum_components/qubit_pair.py b/quam/components/quantum_components/qubit_pair.py index b0c8bb12..e78bfa9d 100644 --- a/quam/components/quantum_components/qubit_pair.py +++ b/quam/components/quantum_components/qubit_pair.py @@ -1,16 +1,22 @@ -from typing import Dict +from typing import Dict, TYPE_CHECKING, Any from dataclasses import field from quam.core import quam_dataclass, QuamComponent from quam.components.quantum_components.qubit import Qubit -from quam.components.implementations import QubitPairImplementation + +if TYPE_CHECKING: + from quam.components.implementations import QubitPairImplementation + + ImplementationType = QubitPairImplementation +else: + ImplementationType = Any @quam_dataclass class QubitPair(QuamComponent): qubit_control: Qubit qubit_target: Qubit - implementations: Dict[str, QubitPairImplementation] = field(default_factory=dict) + implementations: Dict[str, ImplementationType] = field(default_factory=dict) def align(self): """Aligns the execution of all channels of both qubits""" diff --git a/quam/core/operations_registry.py b/quam/core/operations_registry.py index 1ca5fb16..beec1ec0 100644 --- a/quam/core/operations_registry.py +++ b/quam/core/operations_registry.py @@ -10,8 +10,9 @@ def register_operation(self, func: Callable) -> Callable: """ Register a function as an operation. - This method stores the function in the operations dictionary and returns a wrapped version of the function - that maintains the original function's signature and docstring. + This method stores the function in the operations dictionary and returns a + wrapped version of the function that maintains the original function's + signature and docstring. Args: func (callable): The function to register as an operation. diff --git a/quam/utils/reference_class.py b/quam/utils/reference_class.py index 58bf0de6..9f7a3f29 100644 --- a/quam/utils/reference_class.py +++ b/quam/utils/reference_class.py @@ -27,8 +27,11 @@ 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) -> str: + """Get the value of an attribute without following references. + + If the value is a reference, the reference string is returned + """ return super().__getattribute__(attr) def __getattribute__(self, attr: str) -> Any: From a952f34c5acd63f3c0fef2101e069d76e1f4e531 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 28 Nov 2024 09:39:52 +0100 Subject: [PATCH 21/43] add get_pulse --- docs/examples/Qubit operations.ipynb | 21 +++++++++-------- .../implementations/base_implementation.py | 3 ++- .../implementations/qubit_implementations.py | 11 +++++---- quam/components/quantum_components/qubit.py | 23 +++++++++++++++++++ 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/docs/examples/Qubit operations.ipynb b/docs/examples/Qubit operations.ipynb index 13e7728e..ac13063c 100644 --- a/docs/examples/Qubit operations.ipynb +++ b/docs/examples/Qubit operations.ipynb @@ -9,7 +9,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2024-11-27 20:44:26,117 - qm - INFO - Starting session: 4a68c9e2-51d1-4561-80cf-e16b9e54615c\n" + "2024-11-27 20:46:27,774 - qm - INFO - Starting session: 181cc6be-9c4a-4b37-a91e-51deab4fa291\n" ] } ], @@ -73,20 +73,23 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { - "ename": "AttributeError", - "evalue": "'SquarePulse' object has no attribute 'play'", + "ename": "IndexError", + "evalue": "list index out of range", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[5], line 8\u001b[0m\n\u001b[1;32m 6\u001b[0m qubit\u001b[38;5;241m.\u001b[39mimplementations[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m PulseGateImplementation(pulse\u001b[38;5;241m=\u001b[39mpulse\u001b[38;5;241m.\u001b[39mget_reference())\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m qubit\u001b[38;5;241m.\u001b[39mimplementations[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mid \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m----> 8\u001b[0m \u001b[43mqubit\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mimplementations\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mX\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/Repositories/quam/quam/components/implementations/qubit_implementations.py:39\u001b[0m, in \u001b[0;36mPulseGateImplementation.apply\u001b[0;34m(self, amplitude_scale, duration, **kwargs)\u001b[0m\n\u001b[1;32m 38\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39m, amplitude_scale\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, duration\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m---> 39\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpulse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplay\u001b[49m(amplitude_scale\u001b[38;5;241m=\u001b[39mamplitude_scale, duration\u001b[38;5;241m=\u001b[39mduration, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", - "File \u001b[0;32m~/Repositories/quam/quam/utils/reference_class.py:38\u001b[0m, in \u001b[0;36mReferenceClass.__getattribute__\u001b[0;34m(self, attr)\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__getattribute__\u001b[39m(\u001b[38;5;28mself\u001b[39m, attr: \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Any:\n\u001b[0;32m---> 38\u001b[0m attr_val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__getattribute__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mattr\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m attr \u001b[38;5;129;01min\u001b[39;00m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_is_reference\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_get_referenced_value\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__post_init__\u001b[39m\u001b[38;5;124m\"\u001b[39m]:\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m attr_val\n", - "\u001b[0;31mAttributeError\u001b[0m: 'SquarePulse' object has no attribute 'play'" + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[4], line 8\u001b[0m\n\u001b[1;32m 6\u001b[0m qubit\u001b[38;5;241m.\u001b[39mimplementations[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m PulseGateImplementation(pulse\u001b[38;5;241m=\u001b[39mpulse\u001b[38;5;241m.\u001b[39mget_reference())\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m qubit\u001b[38;5;241m.\u001b[39mimplementations[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mid \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m----> 8\u001b[0m \u001b[43mqubit\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mimplementations\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mX\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Repositories/quam/quam/components/implementations/qubit_implementations.py:39\u001b[0m, in \u001b[0;36mPulseGateImplementation.apply\u001b[0;34m(self, amplitude_scale, duration, **kwargs)\u001b[0m\n\u001b[1;32m 38\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39m, amplitude_scale\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, duration\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m---> 39\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpulse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplay\u001b[49m\u001b[43m(\u001b[49m\u001b[43mamplitude_scale\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mamplitude_scale\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mduration\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mduration\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Repositories/quam/quam/components/pulses.py:232\u001b[0m, in \u001b[0;36mPulse.play\u001b[0;34m(self, amplitude_scale, duration, condition, chirp, truncate, timestamp_stream, continue_chirp, target, validate)\u001b[0m\n\u001b[1;32m 229\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mchannel \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 230\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPulse \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m is not attached to a channel\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 232\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mchannel\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mplay\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 233\u001b[0m \u001b[43m \u001b[49m\u001b[43mpulse_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 234\u001b[0m \u001b[43m \u001b[49m\u001b[43mamplitude_scale\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mamplitude_scale\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 235\u001b[0m \u001b[43m \u001b[49m\u001b[43mduration\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mduration\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 236\u001b[0m \u001b[43m \u001b[49m\u001b[43mcondition\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcondition\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 237\u001b[0m \u001b[43m \u001b[49m\u001b[43mchirp\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchirp\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 238\u001b[0m \u001b[43m \u001b[49m\u001b[43mtruncate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtruncate\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 239\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimestamp_stream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimestamp_stream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 240\u001b[0m \u001b[43m \u001b[49m\u001b[43mcontinue_chirp\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcontinue_chirp\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 241\u001b[0m \u001b[43m \u001b[49m\u001b[43mtarget\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtarget\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 242\u001b[0m \u001b[43m \u001b[49m\u001b[43mvalidate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mvalidate\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 243\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Repositories/quam/quam/components/channels.py:389\u001b[0m, in \u001b[0;36mChannel.play\u001b[0;34m(self, pulse_name, amplitude_scale, duration, condition, chirp, truncate, timestamp_stream, continue_chirp, target, validate)\u001b[0m\n\u001b[1;32m 384\u001b[0m pulse \u001b[38;5;241m=\u001b[39m pulse_name\n\u001b[1;32m 386\u001b[0m \u001b[38;5;66;03m# At the moment, self.name is not defined for Channel because it could\u001b[39;00m\n\u001b[1;32m 387\u001b[0m \u001b[38;5;66;03m# be a property or dataclass field in a subclass.\u001b[39;00m\n\u001b[1;32m 388\u001b[0m \u001b[38;5;66;03m# # TODO Find elegant solution for Channel.name.\u001b[39;00m\n\u001b[0;32m--> 389\u001b[0m \u001b[43mplay\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 390\u001b[0m \u001b[43m \u001b[49m\u001b[43mpulse\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpulse\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 391\u001b[0m \u001b[43m \u001b[49m\u001b[43melement\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 392\u001b[0m \u001b[43m \u001b[49m\u001b[43mduration\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mduration\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 393\u001b[0m \u001b[43m \u001b[49m\u001b[43mcondition\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcondition\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 394\u001b[0m \u001b[43m \u001b[49m\u001b[43mchirp\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchirp\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 395\u001b[0m \u001b[43m \u001b[49m\u001b[43mtruncate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtruncate\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 396\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimestamp_stream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimestamp_stream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 397\u001b[0m \u001b[43m \u001b[49m\u001b[43mcontinue_chirp\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcontinue_chirp\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 398\u001b[0m \u001b[43m \u001b[49m\u001b[43mtarget\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtarget\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 399\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Repositories/quam/.venv/lib/python3.11/site-packages/qm/qua/_dsl.py:170\u001b[0m, in \u001b[0;36mplay\u001b[0;34m(pulse, element, duration, condition, chirp, truncate, timestamp_stream, continue_chirp, target)\u001b[0m\n\u001b[1;32m 87\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mplay\u001b[39m(\n\u001b[1;32m 88\u001b[0m pulse: PlayPulseType,\n\u001b[1;32m 89\u001b[0m element: \u001b[38;5;28mstr\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 96\u001b[0m target: \u001b[38;5;28mstr\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 97\u001b[0m ):\n\u001b[1;32m 98\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124;03m\"\"\"Play a `pulse` based on an 'operation' defined in `element`.\u001b[39;00m\n\u001b[1;32m 99\u001b[0m \n\u001b[1;32m 100\u001b[0m \u001b[38;5;124;03m The pulse will be modified according to the properties of the element\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 168\u001b[0m \u001b[38;5;124;03m ```\u001b[39;00m\n\u001b[1;32m 169\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 170\u001b[0m body \u001b[38;5;241m=\u001b[39m \u001b[43m_get_scope_as_blocks_body\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m duration \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 172\u001b[0m duration \u001b[38;5;241m=\u001b[39m _unwrap_exp(exp(duration))\n", + "File \u001b[0;32m~/Repositories/quam/.venv/lib/python3.11/site-packages/qm/qua/_dsl.py:1957\u001b[0m, in \u001b[0;36m_get_scope_as_blocks_body\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1955\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_get_scope_as_blocks_body\u001b[39m() \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m _StatementsCollection:\n\u001b[1;32m 1956\u001b[0m \u001b[38;5;28;01mglobal\u001b[39;00m _block_stack\n\u001b[0;32m-> 1957\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28missubclass\u001b[39m(\u001b[38;5;28mtype\u001b[39m(\u001b[43m_block_stack\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m]\u001b[49m), _BodyScope):\n\u001b[1;32m 1958\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m QmQuaException(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mExpecting scope with body.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 1959\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _block_stack[\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m]\u001b[38;5;241m.\u001b[39mbody()\n", + "\u001b[0;31mIndexError\u001b[0m: list index out of range" ] } ], diff --git a/quam/components/implementations/base_implementation.py b/quam/components/implementations/base_implementation.py index 3c3f4d0b..af224a54 100644 --- a/quam/components/implementations/base_implementation.py +++ b/quam/components/implementations/base_implementation.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Any from quam.core.quam_classes import quam_dataclass, QuamComponent from quam.utils import string_reference as str_ref @@ -23,6 +24,6 @@ def inferred_id(self): ) @abstractmethod - def apply(self, *args, **kwargs): + def apply(self, *args, **kwargs) -> Any: """Applies the operation""" pass diff --git a/quam/components/implementations/qubit_implementations.py b/quam/components/implementations/qubit_implementations.py index 9b753970..81e6e509 100644 --- a/quam/components/implementations/qubit_implementations.py +++ b/quam/components/implementations/qubit_implementations.py @@ -1,4 +1,5 @@ from abc import ABC +from typing import Union from quam.components.implementations.base_implementation import BaseImplementation from quam.components.pulses import Pulse from quam.core import quam_dataclass @@ -30,18 +31,20 @@ class PulseGateImplementation(QubitImplementation): Args: pulse: Name of pulse to be played on qubit. Should be a key in `channel.operations` for one of the qubit's channels - """ - pulse: Pulse + pulse: Union[Pulse, str] def apply(self, *, amplitude_scale=None, duration=None, **kwargs): - self.pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) + if isinstance(self.pulse, Pulse): + pulse = self.pulse + else: + pulse = self.qubit.get_pulse(self.pulse) + pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) @quam_dataclass class MeasureImplementation(QubitImplementation): - def apply(self, **kwargs) -> QuaVariableType: return self.qubit.measure(**kwargs) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 39cd1442..dad718a0 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -4,10 +4,12 @@ from qm import qua from quam.components.channels import Channel +from quam.components.pulses import Pulse from quam.core import quam_dataclass, QuamComponent if TYPE_CHECKING: from quam.components.implementations import QubitImplementation + ImplementationType = QubitImplementation else: ImplementationType = Any @@ -37,6 +39,27 @@ def channels(self) -> Dict[str, Channel]: if isinstance(val, Channel) } + def get_pulse(self, pulse_name: str) -> Pulse: + """Returns the pulse with the given name + + Goes through all channels and returns the unique pulse with the given name. + + Raises a ValueError if the pulse is not found or if there are multiple pulses + with the same name. + """ + pulses = [ + pulse + for channel in self.qubit.channels.values() + for key, pulse in channel.operations.items() + if key == pulse_name + ] + if len(pulses) == 0: + raise ValueError(f"Pulse {pulse_name} not found") + elif len(pulses) > 1: + raise ValueError(f"Pulse {pulse_name} is not unique") + else: + return pulses[0] + def align(self, *other_qubits: "Qubit"): """Aligns the execution of all channels of this qubit and all other qubits""" channel_names = [channel.name for channel in self.channels.values()] From c8f23f780694c6a7b956b79d7bd6813fbf3fe696 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 28 Nov 2024 10:49:51 +0100 Subject: [PATCH 22/43] gate work with Arthur --- quam/components/__init__.py | 4 +-- quam/components/implementations/__init__.py | 10 ------- quam/components/macro/__init__.py | 10 +++++++ .../quam_macro.py} | 4 +-- .../qubit_macros.py} | 27 ++++--------------- .../qubit_pair_macros.py} | 6 ++--- .../components/quantum_components/__init__.py | 3 ++- .../quantum_components/quantum_component.py | 6 ++--- quam/components/quantum_components/qubit.py | 13 ++++----- .../quantum_components/qubit_pair.py | 14 +++++----- .../cz_implementation.py | 4 +-- .../superconducting_qubits/operations.py | 19 ++++++++----- 12 files changed, 56 insertions(+), 64 deletions(-) delete mode 100644 quam/components/implementations/__init__.py create mode 100644 quam/components/macro/__init__.py rename quam/components/{implementations/base_implementation.py => macro/quam_macro.py} (90%) rename quam/components/{implementations/qubit_implementations.py => macro/qubit_macros.py} (61%) rename quam/components/{implementations/qubit_pair_implementations.py => macro/qubit_pair_macros.py} (84%) diff --git a/quam/components/__init__.py b/quam/components/__init__.py index 16290a6b..87c5294d 100644 --- a/quam/components/__init__.py +++ b/quam/components/__init__.py @@ -4,7 +4,7 @@ from .channels import * from . import pulses from .quantum_components import * -from . import implementations +from . import macro __all__ = [ *basic_quam.__all__, @@ -13,5 +13,5 @@ *octave.__all__, *quantum_components.__all__, "pulses", - "implementations", + "macro", ] diff --git a/quam/components/implementations/__init__.py b/quam/components/implementations/__init__.py deleted file mode 100644 index 632f7e0a..00000000 --- a/quam/components/implementations/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .base_implementation import * -from .qubit_implementations import * -from .qubit_pair_implementations import * - - -__all__ = [ - *base_implementation.__all__, - *qubit_implementations.__all__, - *qubit_pair_implementations.__all__, -] diff --git a/quam/components/macro/__init__.py b/quam/components/macro/__init__.py new file mode 100644 index 00000000..da36dec1 --- /dev/null +++ b/quam/components/macro/__init__.py @@ -0,0 +1,10 @@ +from .quam_macro import * +from .qubit_macros import * +from .qubit_pair_macros import * + + +__all__ = [ + *quam_macro.__all__, + *qubit_macros.__all__, + *qubit_pair_macros.__all__, +] diff --git a/quam/components/implementations/base_implementation.py b/quam/components/macro/quam_macro.py similarity index 90% rename from quam/components/implementations/base_implementation.py rename to quam/components/macro/quam_macro.py index af224a54..34f26fc1 100644 --- a/quam/components/implementations/base_implementation.py +++ b/quam/components/macro/quam_macro.py @@ -4,11 +4,11 @@ from quam.utils import string_reference as str_ref -__all__ = ["BaseImplementation"] +__all__ = ["QuamMacro"] @quam_dataclass -class BaseImplementation(QuamComponent, ABC): +class QuamMacro(QuamComponent, ABC): id: str = "#./inferred_id" @property diff --git a/quam/components/implementations/qubit_implementations.py b/quam/components/macro/qubit_macros.py similarity index 61% rename from quam/components/implementations/qubit_implementations.py rename to quam/components/macro/qubit_macros.py index 81e6e509..f70731d8 100644 --- a/quam/components/implementations/qubit_implementations.py +++ b/quam/components/macro/qubit_macros.py @@ -1,17 +1,17 @@ from abc import ABC from typing import Union -from quam.components.implementations.base_implementation import BaseImplementation +from quam.components.macro.quam_macro import QuamMacro from quam.components.pulses import Pulse from quam.core import quam_dataclass from qm.qua import QuaVariableType -__all__ = ["QubitImplementation", "PulseGateImplementation"] +__all__ = ["QubitMacro", "PulseMacro"] @quam_dataclass -class QubitImplementation(BaseImplementation, ABC): +class QubitMacro(QuamMacro, ABC): @property def qubit(self): from quam.components.quantum_components.qubit import Qubit @@ -25,7 +25,7 @@ def qubit(self): @quam_dataclass -class PulseGateImplementation(QubitImplementation): +class PulseMacro(QubitMacro): """Single-qubit gate for a qubit consisting of a single pulse Args: @@ -34,6 +34,7 @@ class PulseGateImplementation(QubitImplementation): """ pulse: Union[Pulse, str] + unitary: Optional[List[List[float]]] = None def apply(self, *, amplitude_scale=None, duration=None, **kwargs): if isinstance(self.pulse, Pulse): @@ -41,21 +42,3 @@ def apply(self, *, amplitude_scale=None, duration=None, **kwargs): else: pulse = self.qubit.get_pulse(self.pulse) pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) - - -@quam_dataclass -class MeasureImplementation(QubitImplementation): - def apply(self, **kwargs) -> QuaVariableType: - return self.qubit.measure(**kwargs) - - -@quam_dataclass -class AlignImplementation(QubitImplementation): - def apply(self, *other_qubits, **kwargs): - self.qubit.align(*other_qubits, **kwargs) - - -@quam_dataclass -class ResetImplementation(QubitImplementation): - def apply(self, **kwargs): - self.qubit.reset(**kwargs) diff --git a/quam/components/implementations/qubit_pair_implementations.py b/quam/components/macro/qubit_pair_macros.py similarity index 84% rename from quam/components/implementations/qubit_pair_implementations.py rename to quam/components/macro/qubit_pair_macros.py index a6976fff..f1c3be58 100644 --- a/quam/components/implementations/qubit_pair_implementations.py +++ b/quam/components/macro/qubit_pair_macros.py @@ -1,17 +1,17 @@ from abc import ABC from typing import TYPE_CHECKING -from quam.components.implementations import BaseImplementation +from quam.components.macro import QuamMacro from quam.core import quam_dataclass from quam.components.quantum_components.qubit import Qubit -__all__ = ["QubitPairImplementation"] +__all__ = ["QubitPairMacro"] @quam_dataclass -class QubitPairImplementation(BaseImplementation, ABC): +class QubitPairMacro(QuamMacro, ABC): @property def qubit_pair(self): # TODO Add QubitPair return type from quam.components.quantum_components.qubit_pair import QubitPair diff --git a/quam/components/quantum_components/__init__.py b/quam/components/quantum_components/__init__.py index e8887cfe..aa9854da 100644 --- a/quam/components/quantum_components/__init__.py +++ b/quam/components/quantum_components/__init__.py @@ -1,4 +1,5 @@ +from quam.components.quantum_components.quantum_component import QuantumComponent from quam.components.quantum_components.qubit import Qubit from quam.components.quantum_components.qubit_pair import QubitPair -__all__ = ["Qubit", "QubitPair"] +__all__ = ["QuantumComponent", "Qubit", "QubitPair"] diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index 8776a27d..c419f767 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -2,7 +2,7 @@ from dataclasses import field from typing import Any, Dict, Union from quam.core.quam_classes import quam_dataclass, QuamComponent -from quam.components.implementations import BaseImplementation +from quam.components.macro import QuamMacro __all__ = ["QuantumComponent"] @@ -10,7 +10,7 @@ @quam_dataclass class QuantumComponent(QuamComponent, ABC): id: Union[str, int] - implementations: Dict[str, BaseImplementation] = field(default_factory=dict) + macros: Dict[str, QuamMacro] = field(default_factory=dict) @property @abstractmethod @@ -18,5 +18,5 @@ def name(self) -> str: pass def apply(self, operation: str, *args, **kwargs) -> Any: - operation_obj = self.implementations[operation] + operation_obj = self.macros[operation] operation_obj.apply(*args, **kwargs) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index dad718a0..3df97375 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -5,23 +5,24 @@ from quam.components.channels import Channel from quam.components.pulses import Pulse -from quam.core import quam_dataclass, QuamComponent +from quam.components.quantum_components import QuantumComponent +from quam.core import quam_dataclass if TYPE_CHECKING: - from quam.components.implementations import QubitImplementation + from quam.components.macro import QubitMacro - ImplementationType = QubitImplementation + MacroType = QubitMacro else: - ImplementationType = Any + MacroType = Any __all__ = ["Qubit"] @quam_dataclass -class Qubit(QuamComponent): +class Qubit(QuantumComponent): id: Union[str, int] - implementations: Dict[str, ImplementationType] = field(default_factory=dict) + macros: Dict[str, MacroType] = field(default_factory=dict) @property def name(self) -> str: diff --git a/quam/components/quantum_components/qubit_pair.py b/quam/components/quantum_components/qubit_pair.py index e78bfa9d..16012259 100644 --- a/quam/components/quantum_components/qubit_pair.py +++ b/quam/components/quantum_components/qubit_pair.py @@ -1,22 +1,22 @@ from typing import Dict, TYPE_CHECKING, Any from dataclasses import field -from quam.core import quam_dataclass, QuamComponent -from quam.components.quantum_components.qubit import Qubit +from quam.core import quam_dataclass +from quam.components.quantum_components import QuantumComponent, Qubit if TYPE_CHECKING: - from quam.components.implementations import QubitPairImplementation + from quam.components.macro import QubitPairMacro - ImplementationType = QubitPairImplementation + MacroType = QubitPairMacro else: - ImplementationType = Any + MacroType = Any @quam_dataclass -class QubitPair(QuamComponent): +class QubitPair(QuantumComponent): qubit_control: Qubit qubit_target: Qubit - implementations: Dict[str, ImplementationType] = field(default_factory=dict) + macros: Dict[str, MacroType] = field(default_factory=dict) def align(self): """Aligns the execution of all channels of both qubits""" diff --git a/quam/examples/superconducting_qubits/cz_implementation.py b/quam/examples/superconducting_qubits/cz_implementation.py index daba838e..71e2c033 100644 --- a/quam/examples/superconducting_qubits/cz_implementation.py +++ b/quam/examples/superconducting_qubits/cz_implementation.py @@ -1,10 +1,10 @@ from quam.core import quam_dataclass from quam.components.pulses import Pulse -from quam.components.implementations import QubitPairImplementation +from quam.components.macro import QubitPairMacro @quam_dataclass -class CZImplementation(QubitPairImplementation): +class CZImplementation(QubitPairMacro): """CZ Operation for a qubit pair""" flux_pulse: Pulse diff --git a/quam/examples/superconducting_qubits/operations.py b/quam/examples/superconducting_qubits/operations.py index 14f1bede..8da85fe4 100644 --- a/quam/examples/superconducting_qubits/operations.py +++ b/quam/examples/superconducting_qubits/operations.py @@ -1,7 +1,9 @@ +from typing import Tuple from qm.qua import QuaVariableType -from quam.components import Qubit +from quam.components import Qubit, QubitPair from quam.core import OperationsRegistry + operations_registry = OperationsRegistry() @@ -15,9 +17,13 @@ def y(qubit: Qubit, **kwargs): qubit.apply("Y", **kwargs) +def U_custom(qubit: Qubit, **kwargs): + U = qubit.get_macro(unitary=[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]]) + U.apply(**kwargs) + + @operations_registry.register_operation -def cz(qubit_control: Qubit, qubit_target: Qubit, **kwargs): - qubit_pair = qubit_control @ qubit_target +def cz(qubit_pair: QubitPair, **kwargs): qubit_pair.apply("CZ", **kwargs) @@ -25,7 +31,8 @@ def cz(qubit_control: Qubit, qubit_target: Qubit, **kwargs): def measure(qubit: Qubit, **kwargs) -> QuaVariableType: return qubit.measure(**kwargs) - +# TODO Agree on function contents @operations_registry.register_operation -def align(*qubits: Qubit): - qubits[0].align(*qubits[1:]) +def align(qubits: Tuple[Qubit, ...]): + qubits[0].apply("align", *qubits[1:]) + # qubits[0].align(*qubits[1:]) From 8b994e22ceb134dfe5b7fb2c0738b1c880f470a5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 29 Nov 2024 08:00:27 +0100 Subject: [PATCH 23/43] refactoring macro --- quam/components/macro/__init__.py | 2 +- quam/components/macro/qubit_macros.py | 12 ++++---- .../quantum_components/quantum_component.py | 26 +++++++++++++++-- quam/components/quantum_components/qubit.py | 1 + quam/core/macro/__init__.py | 5 ++++ quam/core/macro/base_macro.py | 14 +++++++++ quam/core/macro/method_macro.py | 29 +++++++++++++++++++ quam/{components => core}/macro/quam_macro.py | 11 ++----- 8 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 quam/core/macro/__init__.py create mode 100644 quam/core/macro/base_macro.py create mode 100644 quam/core/macro/method_macro.py rename quam/{components => core}/macro/quam_macro.py (73%) diff --git a/quam/components/macro/__init__.py b/quam/components/macro/__init__.py index da36dec1..24c6ed50 100644 --- a/quam/components/macro/__init__.py +++ b/quam/components/macro/__init__.py @@ -1,4 +1,4 @@ -from .quam_macro import * +from ...core.macro.quam_macro import * from .qubit_macros import * from .qubit_pair_macros import * diff --git a/quam/components/macro/qubit_macros.py b/quam/components/macro/qubit_macros.py index f70731d8..5730c55c 100644 --- a/quam/components/macro/qubit_macros.py +++ b/quam/components/macro/qubit_macros.py @@ -1,11 +1,9 @@ from abc import ABC -from typing import Union -from quam.components.macro.quam_macro import QuamMacro +from typing import Optional, Union, List +from quam.core.macro.quam_macro import QuamMacro from quam.components.pulses import Pulse from quam.core import quam_dataclass -from qm.qua import QuaVariableType - __all__ = ["QubitMacro", "PulseMacro"] @@ -33,7 +31,7 @@ class PulseMacro(QubitMacro): `channel.operations` for one of the qubit's channels """ - pulse: Union[Pulse, str] + pulse: Union[Pulse, str] # type: ignore unitary: Optional[List[List[float]]] = None def apply(self, *, amplitude_scale=None, duration=None, **kwargs): @@ -41,4 +39,6 @@ def apply(self, *, amplitude_scale=None, duration=None, **kwargs): pulse = self.pulse else: pulse = self.qubit.get_pulse(self.pulse) - pulse.play(amplitude_scale=amplitude_scale, duration=duration, **kwargs) + pulse.play( + amplitude_scale=amplitude_scale, duration=duration, **kwargs # type: ignore + ) diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index c419f767..d9c6caf7 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -1,16 +1,21 @@ from abc import ABC, abstractmethod from dataclasses import field -from typing import Any, Dict, Union +import functools +import inspect +from typing import Any, Callable, Dict, Union, TypeVar, cast from quam.core.quam_classes import quam_dataclass, QuamComponent -from quam.components.macro import QuamMacro +from quam.core.macro import BaseMacro, MethodMacro __all__ = ["QuantumComponent"] +T = TypeVar("T", bound=Callable) + + @quam_dataclass class QuantumComponent(QuamComponent, ABC): id: Union[str, int] - macros: Dict[str, QuamMacro] = field(default_factory=dict) + macros: Dict[str, BaseMacro] = field(default_factory=dict) @property @abstractmethod @@ -20,3 +25,18 @@ def name(self) -> str: def apply(self, operation: str, *args, **kwargs) -> Any: operation_obj = self.macros[operation] operation_obj.apply(*args, **kwargs) + + @staticmethod + def register_macro(func: T) -> T: + """Decorator to register a method as a macro entry point""" + return cast(T, MethodMacro(func)) + + def _get_method_macros(self) -> Dict[str, MethodMacro]: + return dict( + inspect.getmembers( + self, predicate=functools.partial(isinstance, MethodMacro) + ) + ) + + def get_macros(self) -> Dict[str, BaseMacro]: + return {**self.macros, **self._get_method_macros()} diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 3df97375..32beebeb 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -61,6 +61,7 @@ def get_pulse(self, pulse_name: str) -> Pulse: else: return pulses[0] + @QuantumComponent.register_macro def align(self, *other_qubits: "Qubit"): """Aligns the execution of all channels of this qubit and all other qubits""" channel_names = [channel.name for channel in self.channels.values()] diff --git a/quam/core/macro/__init__.py b/quam/core/macro/__init__.py new file mode 100644 index 00000000..df4a7b2a --- /dev/null +++ b/quam/core/macro/__init__.py @@ -0,0 +1,5 @@ +from quam.core.macro.base_macro import BaseMacro +from quam.core.macro.method_macro import MethodMacro +from quam.core.macro.quam_macro import QuamMacro + +__all__ = ["BaseMacro", "MethodMacro", "QuamMacro"] diff --git a/quam/core/macro/base_macro.py b/quam/core/macro/base_macro.py new file mode 100644 index 00000000..4e617830 --- /dev/null +++ b/quam/core/macro/base_macro.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod +from typing import Any + + +__all__ = ["BaseMacro"] + + +class BaseMacro(ABC): + """Base class for all macro types in the system""" + + @abstractmethod + def apply(self, *args, **kwargs) -> Any: + """Applies the macro operation""" + pass diff --git a/quam/core/macro/method_macro.py b/quam/core/macro/method_macro.py new file mode 100644 index 00000000..c014827a --- /dev/null +++ b/quam/core/macro/method_macro.py @@ -0,0 +1,29 @@ +from typing import Any, Callable, TypeVar +import functools + +from quam.core.macro.base_macro import BaseMacro + + +__all__ = ["MethodMacro"] + + +T = TypeVar("T", bound=Callable) + + +class MethodMacro(BaseMacro): + """Decorator that marks methods which should be exposed as macros.""" + + def __init__(self, func: T) -> None: + functools.wraps(func)(self) + self.func = func + + def apply(self, *args, **kwargs) -> Any: + """Implements BaseMacro.apply by calling the wrapped function""" + return self.func(*args, **kwargs) + + def __call__(self, *args, **kwargs): + return self.apply(*args, **kwargs) + + @staticmethod + def is_macro_method(obj: Any) -> bool: + return isinstance(obj, MethodMacro) diff --git a/quam/components/macro/quam_macro.py b/quam/core/macro/quam_macro.py similarity index 73% rename from quam/components/macro/quam_macro.py rename to quam/core/macro/quam_macro.py index 34f26fc1..551f579f 100644 --- a/quam/components/macro/quam_macro.py +++ b/quam/core/macro/quam_macro.py @@ -1,14 +1,14 @@ -from abc import ABC, abstractmethod -from typing import Any +from abc import ABC from quam.core.quam_classes import quam_dataclass, QuamComponent from quam.utils import string_reference as str_ref +from quam.core.macro.base_macro import BaseMacro __all__ = ["QuamMacro"] @quam_dataclass -class QuamMacro(QuamComponent, ABC): +class QuamMacro(QuamComponent, BaseMacro, ABC): id: str = "#./inferred_id" @property @@ -22,8 +22,3 @@ def inferred_id(self): raise AttributeError( f"Cannot infer id of {self} because it is not attached to a parent" ) - - @abstractmethod - def apply(self, *args, **kwargs) -> Any: - """Applies the operation""" - pass From f4d824da198c8d31b31baa5d64401939b9799e51 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 29 Nov 2024 08:48:53 +0100 Subject: [PATCH 24/43] add tests for qubit.get_pulse --- quam/components/macro/__init__.py | 2 - quam/components/macro/qubit_macros.py | 2 +- quam/components/macro/qubit_pair_macros.py | 3 +- quam/components/quantum_components/qubit.py | 2 +- tests/components/test_qubit.py | 47 ++++++++++++++++++--- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/quam/components/macro/__init__.py b/quam/components/macro/__init__.py index 24c6ed50..5b2865ea 100644 --- a/quam/components/macro/__init__.py +++ b/quam/components/macro/__init__.py @@ -1,10 +1,8 @@ -from ...core.macro.quam_macro import * from .qubit_macros import * from .qubit_pair_macros import * __all__ = [ - *quam_macro.__all__, *qubit_macros.__all__, *qubit_pair_macros.__all__, ] diff --git a/quam/components/macro/qubit_macros.py b/quam/components/macro/qubit_macros.py index 5730c55c..8eb9449c 100644 --- a/quam/components/macro/qubit_macros.py +++ b/quam/components/macro/qubit_macros.py @@ -1,6 +1,6 @@ from abc import ABC from typing import Optional, Union, List -from quam.core.macro.quam_macro import QuamMacro +from quam.core.macro import QuamMacro from quam.components.pulses import Pulse from quam.core import quam_dataclass diff --git a/quam/components/macro/qubit_pair_macros.py b/quam/components/macro/qubit_pair_macros.py index f1c3be58..411ea6a8 100644 --- a/quam/components/macro/qubit_pair_macros.py +++ b/quam/components/macro/qubit_pair_macros.py @@ -1,7 +1,6 @@ from abc import ABC -from typing import TYPE_CHECKING -from quam.components.macro import QuamMacro +from quam.core.macro import QuamMacro from quam.core import quam_dataclass from quam.components.quantum_components.qubit import Qubit diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 32beebeb..463c2ab9 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -50,7 +50,7 @@ def get_pulse(self, pulse_name: str) -> Pulse: """ pulses = [ pulse - for channel in self.qubit.channels.values() + for channel in self.channels.values() for key, pulse in channel.operations.items() if key == pulse_name ] diff --git a/tests/components/test_qubit.py b/tests/components/test_qubit.py index 931e2c6d..b6dca6bc 100644 --- a/tests/components/test_qubit.py +++ b/tests/components/test_qubit.py @@ -1,5 +1,7 @@ +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 @@ -19,8 +21,9 @@ class TestQubit(Qubit): resonator: IQChannel -def test_qubit_channels(): - qubit = TestQubit( +@pytest.fixture +def test_qubit(): + return TestQubit( id=0, xy=IQChannel( id="xy", @@ -35,11 +38,15 @@ def test_qubit_channels(): frequency_converter_up=None, ), ) - assert qubit.channels == {"xy": qubit.xy} -def test_qubit_channels_referenced(): - qubit = TestQubit( +def test_qubit_channels(test_qubit): + assert test_qubit.channels == {"xy": test_qubit.xy} + + +@pytest.fixture +def test_qubit_referenced(): + return TestQubit( id=0, xy=IQChannel( id="xy", @@ -49,4 +56,32 @@ def test_qubit_channels_referenced(): ), resonator="#./xy", ) - assert qubit.channels == {"xy": qubit.xy, "resonator": qubit.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 From 0af61a6abde3be43ffb8a6a22e1ee56192f07997 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 29 Nov 2024 08:59:30 +0100 Subject: [PATCH 25/43] add test for align --- quam/components/quantum_components/qubit.py | 3 ++- tests/components/test_qubit.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 463c2ab9..9f545a63 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -2,6 +2,7 @@ from dataclasses import field from qm import qua +from qm.qua import align from quam.components.channels import Channel from quam.components.pulses import Pulse @@ -68,7 +69,7 @@ def align(self, *other_qubits: "Qubit"): for qubit in other_qubits: channel_names.extend([channel.name for channel in qubit.channels.values()]) - qua.align(*channel_names) + align(*channel_names) def __matmul__(self, other): # TODO Add QubitPair return type """Allows access to qubit pairs using the '@' operator, e.g. (q1 @ q2)""" diff --git a/tests/components/test_qubit.py b/tests/components/test_qubit.py index b6dca6bc..713b7208 100644 --- a/tests/components/test_qubit.py +++ b/tests/components/test_qubit.py @@ -26,13 +26,11 @@ def test_qubit(): return TestQubit( id=0, xy=IQChannel( - id="xy", opx_output_I=("con1", 1), opx_output_Q=("con1", 2), frequency_converter_up=None, ), resonator=IQChannel( - id="resonator", opx_output_I=("con1", 3), opx_output_Q=("con1", 4), frequency_converter_up=None, @@ -49,7 +47,6 @@ def test_qubit_referenced(): return TestQubit( id=0, xy=IQChannel( - id="xy", opx_output_I=("con1", 1), opx_output_Q=("con1", 2), frequency_converter_up=None, @@ -85,3 +82,12 @@ def test_qubit_get_pulse_unique(test_qubit): 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") From 1e39fd09154324da487e06604c433db21d0ef5a5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 29 Nov 2024 10:21:01 +0100 Subject: [PATCH 26/43] added pulse macro test --- .../quantum_components/quantum_component.py | 7 +- quam/core/macro/method_macro.py | 11 +++ tests/components/test_qubit.py | 19 +++- tests/macros/test_pulse_macro.py | 86 +++++++++++++++++++ 4 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 tests/macros/test_pulse_macro.py diff --git a/quam/components/quantum_components/quantum_component.py b/quam/components/quantum_components/quantum_component.py index d9c6caf7..f43909d7 100644 --- a/quam/components/quantum_components/quantum_component.py +++ b/quam/components/quantum_components/quantum_component.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod from dataclasses import field -import functools import inspect from typing import Any, Callable, Dict, Union, TypeVar, cast from quam.core.quam_classes import quam_dataclass, QuamComponent @@ -23,7 +22,7 @@ def name(self) -> str: pass def apply(self, operation: str, *args, **kwargs) -> Any: - operation_obj = self.macros[operation] + operation_obj = self.get_macros()[operation] operation_obj.apply(*args, **kwargs) @staticmethod @@ -33,9 +32,7 @@ def register_macro(func: T) -> T: def _get_method_macros(self) -> Dict[str, MethodMacro]: return dict( - inspect.getmembers( - self, predicate=functools.partial(isinstance, MethodMacro) - ) + inspect.getmembers(self, predicate=lambda x: isinstance(x, MethodMacro)) ) def get_macros(self) -> Dict[str, BaseMacro]: diff --git a/quam/core/macro/method_macro.py b/quam/core/macro/method_macro.py index c014827a..83c842fa 100644 --- a/quam/core/macro/method_macro.py +++ b/quam/core/macro/method_macro.py @@ -16,12 +16,23 @@ class MethodMacro(BaseMacro): def __init__(self, func: T) -> None: functools.wraps(func)(self) self.func = func + self.instance = None + + def __get__(self, instance, owner): + # Store the instance to which this method is bound + self.instance = instance + return self def apply(self, *args, **kwargs) -> Any: """Implements BaseMacro.apply by calling the wrapped function""" + if self.instance is not None: + # Call the function with the instance as the first argument + return self.func(self.instance, *args, **kwargs) return self.func(*args, **kwargs) def __call__(self, *args, **kwargs): + if args and args[0] is self.instance: + args = args[1:] return self.apply(*args, **kwargs) @staticmethod diff --git a/tests/components/test_qubit.py b/tests/components/test_qubit.py index 713b7208..5e4df0c6 100644 --- a/tests/components/test_qubit.py +++ b/tests/components/test_qubit.py @@ -39,7 +39,10 @@ def test_qubit(): def test_qubit_channels(test_qubit): - assert test_qubit.channels == {"xy": test_qubit.xy} + assert test_qubit.channels == { + "xy": test_qubit.xy, + "resonator": test_qubit.resonator, + } @pytest.fixture @@ -91,3 +94,17 @@ def test_qubit_align(test_qubit, mocker): 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 new file mode 100644 index 00000000..8bfb788f --- /dev/null +++ b/tests/macros/test_pulse_macro.py @@ -0,0 +1,86 @@ +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 +from quam.components.macro import PulseMacro + + +@quam_dataclass +class TestQubit(Qubit): + xy: IQChannel + + +@quam_dataclass +class QUAM(QuamRoot): + qubit: TestQubit + + +@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, + operations={"test_pulse": SquarePulse(length=100, amplitude=1.0)}, + ), + ) + + +def test_pulse_macro_no_pulse(test_qubit): + pulse_macro = PulseMacro() + + +def test_pulse_macro_pulse_string(test_qubit, mocker): + pulse_macro = PulseMacro(pulse="test_pulse") + assert pulse_macro.pulse == "test_pulse" + + test_qubit.macros["test_pulse"] = pulse_macro + + assert test_qubit.get_macros() == { + "test_pulse": pulse_macro, + "align": test_qubit.align, + } + + with pytest.raises(IndexError): + test_qubit.apply("test_pulse") + + mocker.patch("quam.components.channels.play") + + test_qubit.apply("test_pulse") + + from quam.components.channels import play + + play.assert_called_once() + + +def test_pulse_macro_pulse_object_error(test_qubit): + pulse_macro = PulseMacro( + pulse=SquarePulse(id="test_pulse", length=100, amplitude=1.0) + ) + test_qubit.macros["pulse_macro"] = pulse_macro + with pytest.raises( + ValueError, match="Pulse 'test_pulse' is not attached to a channel" + ): + test_qubit.apply("pulse_macro") + + +def test_pulse_macro_pulse_reference(test_qubit, mocker): + machine = QUAM(qubit=test_qubit) # Need root to get pulse reference + + pulse_macro = PulseMacro( + pulse=test_qubit.xy.operations["test_pulse"].get_reference() + ) + assert pulse_macro.pulse == test_qubit.xy.operations["test_pulse"] + + test_qubit.macros["pulse_macro"] = pulse_macro + + mocker.patch("quam.components.channels.play") + + test_qubit.apply("pulse_macro") + + from quam.components.channels import play + + play.assert_called_once() From 2e2579ac8ffab40edf284668ab5cb5832b943a72 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 29 Nov 2024 10:24:11 +0100 Subject: [PATCH 27/43] additional test --- tests/macros/test_pulse_macro.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/macros/test_pulse_macro.py b/tests/macros/test_pulse_macro.py index 8bfb788f..b9b4a6cc 100644 --- a/tests/macros/test_pulse_macro.py +++ b/tests/macros/test_pulse_macro.py @@ -30,7 +30,10 @@ def test_qubit(): def test_pulse_macro_no_pulse(test_qubit): - pulse_macro = PulseMacro() + with pytest.raises( + TypeError, match="missing 1 required keyword-only argument: 'pulse'" + ): + PulseMacro() def test_pulse_macro_pulse_string(test_qubit, mocker): From f55fb45b07d2aab1384a67e74bc97ddd6496b7b3 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 2 Dec 2024 20:41:31 +0100 Subject: [PATCH 28/43] Working operations decorator --- quam/core/__init__.py | 2 +- quam/core/operation/__init__.py | 5 ++ quam/core/operation/function_properties.py | 55 +++++++++++++++++++ quam/core/operation/operation.py | 31 +++++++++++ .../{ => operation}/operations_registry.py | 23 +++++--- .../superconducting_qubits/operations.py | 21 +++---- 6 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 quam/core/operation/__init__.py create mode 100644 quam/core/operation/function_properties.py create mode 100644 quam/core/operation/operation.py rename quam/core/{ => operation}/operations_registry.py (50%) diff --git a/quam/core/__init__.py b/quam/core/__init__.py index cc6e342c..ab8205d5 100644 --- a/quam/core/__init__.py +++ b/quam/core/__init__.py @@ -1,5 +1,5 @@ from .quam_classes import * -from .operations_registry import OperationsRegistry +from .operation.operations_registry import OperationsRegistry # Exec statement needed to trick Pycharm type checker into recognizing it as a dataclass diff --git a/quam/core/operation/__init__.py b/quam/core/operation/__init__.py new file mode 100644 index 00000000..3c4d16d7 --- /dev/null +++ b/quam/core/operation/__init__.py @@ -0,0 +1,5 @@ +from quam.core.operation.function_properties import FunctionProperties +from quam.core.operation.operation import Operation +from quam.core.operation.operations_registry import OperationsRegistry + +__all__ = ["FunctionProperties", "Operation", "OperationsRegistry"] diff --git a/quam/core/operation/function_properties.py b/quam/core/operation/function_properties.py new file mode 100644 index 00000000..d3085092 --- /dev/null +++ b/quam/core/operation/function_properties.py @@ -0,0 +1,55 @@ +from typing import Any, Callable +import inspect +from typing import get_type_hints +from dataclasses import dataclass, field + +from quam.components import QuantumComponent + + +__all__ = ["FunctionProperties"] + + +@dataclass +class FunctionProperties: + quantum_component_name: str + quantum_component_type: type[QuantumComponent] + name: str = "" + required_args: list[str] = field(default_factory=list) + optional_args: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_function(cls, func: Callable) -> "FunctionProperties": + signature = inspect.signature(func) + parameters = signature.parameters + + if not len(parameters): + raise ValueError( + f"Operation {func.__name__} must accept a QuantumComponent" + ) + + parameters_iterator = iter(parameters) + + # Get first parameter and check if it is a QuantumComponent + first_param_name = next(parameters_iterator) + first_param_type = get_type_hints(func).get(first_param_name, None) + # First parameter must be a QuantumComponent + if first_param_type and issubclass(first_param_type, QuantumComponent): + function_properties = FunctionProperties( + quantum_component_name=first_param_name, + quantum_component_type=first_param_type, + name=func.__name__, + ) + else: + raise ValueError( + f"Operation {func.__name__} must accept a QuantumComponent as its" + " first argument" + ) + + for param in parameters_iterator: + param = parameters[param] + if param.default == inspect.Parameter.empty: + function_properties.required_args.append(param.name) + else: + function_properties.optional_args[param.name] = param.default + + return function_properties diff --git a/quam/core/operation/operation.py b/quam/core/operation/operation.py new file mode 100644 index 00000000..f949b60b --- /dev/null +++ b/quam/core/operation/operation.py @@ -0,0 +1,31 @@ +from typing import Callable + +from quam.core.operation.function_properties import FunctionProperties +from quam.components import QuantumComponent + + +__all__ = ["Operation"] + + +class Operation: + def __init__(self, func: Callable, unitary=None): + self.func = func + self.unitary = unitary + self.properties = FunctionProperties.from_function(func) + + def get_macro(self, quantum_component: QuantumComponent): + macros = quantum_component.get_macros() + return macros[self.properties.name] + + def __call__(self, *args, **kwargs): + if not args or not isinstance(args[0], QuantumComponent): + if self.properties.quantum_component_type is not None: + raise ValueError( + f"First argument to {self.properties.name} must be a " + f"{self.properties.quantum_component_type.__name__}" + ) + + quantum_component, *required_args = args + + macro = self.get_macro(quantum_component) + return macro.apply(quantum_component, *required_args, **kwargs) diff --git a/quam/core/operations_registry.py b/quam/core/operation/operations_registry.py similarity index 50% rename from quam/core/operations_registry.py rename to quam/core/operation/operations_registry.py index beec1ec0..a62bbd56 100644 --- a/quam/core/operations_registry.py +++ b/quam/core/operation/operations_registry.py @@ -1,12 +1,19 @@ from collections import UserDict import functools -from typing import Callable +from typing import Callable, Optional, TypeVar, Any + +from quam.core.operation import Operation + + +__all__ = ["OperationsRegistry"] + +T = TypeVar("T", bound=Callable[..., Any]) class OperationsRegistry(UserDict): """A registry to store and manage operations.""" - def register_operation(self, func: Callable) -> Callable: + def register_operation(self, func: Optional[T] = None, unitary=None) -> T: """ Register a function as an operation. @@ -20,12 +27,12 @@ def register_operation(self, func: Callable) -> Callable: Returns: callable: The wrapped function. """ + if func is None: + return functools.partial(self.register_operation, unitary=unitary) - @functools.wraps(func) - def wrapped_operation(*args, **kwargs): - """Call the registered operation with the provided arguments.""" - return func(*args, **kwargs) + operation = Operation(func, unitary=unitary) + operation = functools.update_wrapper(operation, func) - self[func.__name__] = wrapped_operation + self[func.__name__] = operation - return wrapped_operation + return operation # type: ignore diff --git a/quam/examples/superconducting_qubits/operations.py b/quam/examples/superconducting_qubits/operations.py index 8da85fe4..ed428a89 100644 --- a/quam/examples/superconducting_qubits/operations.py +++ b/quam/examples/superconducting_qubits/operations.py @@ -9,30 +9,31 @@ @operations_registry.register_operation def x(qubit: Qubit, **kwargs): - qubit.apply("X", **kwargs) + pass @operations_registry.register_operation def y(qubit: Qubit, **kwargs): - qubit.apply("Y", **kwargs) + pass -def U_custom(qubit: Qubit, **kwargs): - U = qubit.get_macro(unitary=[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]]) - U.apply(**kwargs) +@operations_registry.register_operation( + unitary=[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]] +) +def U(qubit: Qubit, **kwargs): + pass @operations_registry.register_operation def cz(qubit_pair: QubitPair, **kwargs): - qubit_pair.apply("CZ", **kwargs) + pass @operations_registry.register_operation def measure(qubit: Qubit, **kwargs) -> QuaVariableType: - return qubit.measure(**kwargs) + pass + -# TODO Agree on function contents @operations_registry.register_operation def align(qubits: Tuple[Qubit, ...]): - qubits[0].apply("align", *qubits[1:]) - # qubits[0].align(*qubits[1:]) + pass From d2d6301d15cad17e65570e06cee0c33c626ac92d Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 2 Dec 2024 20:49:14 +0100 Subject: [PATCH 29/43] add macro tests --- tests/macros/test_method_macro.py | 46 +++++++++++++++++++++++++++++++ tests/macros/test_pulse_macro.py | 5 ++++ 2 files changed, 51 insertions(+) create mode 100644 tests/macros/test_method_macro.py diff --git a/tests/macros/test_method_macro.py b/tests/macros/test_method_macro.py new file mode 100644 index 00000000..c13e2cf4 --- /dev/null +++ b/tests/macros/test_method_macro.py @@ -0,0 +1,46 @@ +from quam.core.macro.method_macro import MethodMacro + + +class TestClass: + def __init__(self, value: int): + self.value = value + + @MethodMacro + def add(self, x: int) -> int: + return self.value + x + + +def test_method_macro_binding(): + """Test that MethodMacro correctly binds to instance methods""" + obj = TestClass(5) + assert isinstance(obj.add, MethodMacro) + assert MethodMacro.is_macro_method(obj.add) + assert obj.add.instance == obj + + +def test_method_macro_apply(): + """Test that MethodMacro.apply works with instance methods""" + obj = TestClass(5) + assert obj.add.apply(3) == 8 # 5 + 3 + assert obj.add(3) == 8 # Should work the same way + + +def test_is_macro_method(): + """Test the is_macro_method static method""" + obj = TestClass(5) + + assert MethodMacro.is_macro_method(obj.add) + assert not MethodMacro.is_macro_method(lambda x: x) + assert not MethodMacro.is_macro_method(42) + + +def test_method_macro_preserves_metadata(): + """Test that MethodMacro preserves the original function's metadata""" + + def original(x: int) -> int: + """Test docstring""" + return x + + decorated = MethodMacro(original) + assert decorated.__doc__ == original.__doc__ + assert decorated.__name__ == original.__name__ diff --git a/tests/macros/test_pulse_macro.py b/tests/macros/test_pulse_macro.py index b9b4a6cc..15f5c6f5 100644 --- a/tests/macros/test_pulse_macro.py +++ b/tests/macros/test_pulse_macro.py @@ -40,8 +40,13 @@ def test_pulse_macro_pulse_string(test_qubit, mocker): pulse_macro = PulseMacro(pulse="test_pulse") assert pulse_macro.pulse == "test_pulse" + with pytest.raises(AttributeError): + pulse_macro.qubit + test_qubit.macros["test_pulse"] = pulse_macro + assert pulse_macro.qubit is test_qubit + assert test_qubit.get_macros() == { "test_pulse": pulse_macro, "align": test_qubit.align, From afe6c6c7987920a7e5123b86bf4528b80cf6b327 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 2 Dec 2024 21:24:24 +0100 Subject: [PATCH 30/43] 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 9f545a63..9c8e8d8e 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 16012259..f5f335bf 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 00000000..00e58915 --- /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 00000000..298bc06e --- /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 00000000..44201e17 --- /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 5e4df0c6..00000000 --- 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 15f5c6f5..3a631a41 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 a291c167..3a4ae45f 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 From 071c807811f22b4025e7f0c110570e8d7a2e8edc Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 11:14:06 +0100 Subject: [PATCH 31/43] improved function_properties and added tests --- quam/core/operation/function_properties.py | 132 +++++++++-- tests/operations/test_function_properties.py | 221 +++++++++++++++++++ 2 files changed, 330 insertions(+), 23 deletions(-) create mode 100644 tests/operations/test_function_properties.py diff --git a/quam/core/operation/function_properties.py b/quam/core/operation/function_properties.py index d3085092..7449dc84 100644 --- a/quam/core/operation/function_properties.py +++ b/quam/core/operation/function_properties.py @@ -1,7 +1,8 @@ -from typing import Any, Callable +from typing import Any, Callable, Optional, Type, get_origin, get_args, TypeVar import inspect from typing import get_type_hints from dataclasses import dataclass, field +import keyword from quam.components import QuantumComponent @@ -9,47 +10,132 @@ __all__ = ["FunctionProperties"] +QC = TypeVar("QC", bound=QuantumComponent) + + @dataclass class FunctionProperties: + """ + Properties of a quantum operation function. + + This class extracts and stores metadata about functions that operate on + quantum components, including argument information and type requirements. + + Attributes: + quantum_component_name: Name of the parameter accepting the quantum component + quantum_component_type: Type of quantum component the function operates on + name: Name of the function + required_args: List of required argument names after the quantum component + optional_args: Dictionary of optional arguments and their default values + """ quantum_component_name: str - quantum_component_type: type[QuantumComponent] + quantum_component_type: Type[QC] name: str = "" required_args: list[str] = field(default_factory=list) optional_args: dict[str, Any] = field(default_factory=dict) + def __post_init__(self): + # Make a new list/dict to avoid sharing between instances + self.required_args = list(self.required_args) + self.optional_args = dict(self.optional_args) + + # Validate argument names + all_args = self.required_args + list(self.optional_args) + for arg in all_args: + if not arg.isidentifier(): + raise ValueError(f"Invalid argument name: {arg!r}") + if keyword.iskeyword(arg): + raise ValueError(f"Argument name cannot be a Python keyword: {arg!r}") + + @staticmethod + def _resolve_type(type_hint: Any) -> Optional[Type]: + """ + Resolve type hints, including string forward references and complex types. + + Args: + type_hint: Any type annotation + + Returns: + The resolved base type, or None if unresolvable + """ + if type_hint is None: + return None + # Handle string forward references + if isinstance(type_hint, str): + return None + # Handle Optional, Union, etc + if get_origin(type_hint) is not None: + args = get_args(type_hint) + return args[0] if args else None + # Handle regular types + if isinstance(type_hint, type): + return type_hint + return None + + @staticmethod + def _is_quantum_component_type(type_hint: Optional[Type]) -> bool: + """Check if type is or inherits from QuantumComponent.""" + try: + return ( + type_hint is not None + and isinstance(type_hint, type) + and issubclass(type_hint, QuantumComponent) + ) + except TypeError: + return False + @classmethod def from_function(cls, func: Callable) -> "FunctionProperties": + if not callable(func): + raise ValueError(f"Input {func!r} must be a callable") + signature = inspect.signature(func) parameters = signature.parameters - if not len(parameters): + if not parameters: raise ValueError( - f"Operation {func.__name__} must accept a QuantumComponent" + f"Operation {func.__name__!r} must accept at least one argument " + "(a QuantumComponent)" ) - parameters_iterator = iter(parameters) + # Try to get type hints, gracefully handle missing annotations + try: + type_hints = get_type_hints(func) + except Exception: + type_hints = {} - # Get first parameter and check if it is a QuantumComponent + parameters_iterator = iter(parameters) first_param_name = next(parameters_iterator) - first_param_type = get_type_hints(func).get(first_param_name, None) - # First parameter must be a QuantumComponent - if first_param_type and issubclass(first_param_type, QuantumComponent): - function_properties = FunctionProperties( - quantum_component_name=first_param_name, - quantum_component_type=first_param_type, - name=func.__name__, - ) - else: - raise ValueError( - f"Operation {func.__name__} must accept a QuantumComponent as its" - " first argument" - ) + + # Get and resolve the type of the first parameter + first_param_type = cls._resolve_type(type_hints.get(first_param_name)) + + if not cls._is_quantum_component_type(first_param_type): + if first_param_type is None: + msg = ( + f"Operation {func.__name__!r} is missing type annotation for " + f"first parameter {first_param_name!r}" + ) + else: + msg = ( + f"Operation {func.__name__!r} must accept a QuantumComponent " + f"as its first argument, got {first_param_type!r}" + ) + raise ValueError(msg) + + function_properties = cls( + quantum_component_name=first_param_name, + quantum_component_type=first_param_type, # type: ignore + name=func.__name__, + ) - for param in parameters_iterator: - param = parameters[param] + # Process remaining parameters + for param_name in parameters_iterator: + param = parameters[param_name] if param.default == inspect.Parameter.empty: - function_properties.required_args.append(param.name) + function_properties.required_args.append(param_name) else: - function_properties.optional_args[param.name] = param.default + # Store the default value directly + function_properties.optional_args[param_name] = param.default return function_properties diff --git a/tests/operations/test_function_properties.py b/tests/operations/test_function_properties.py new file mode 100644 index 00000000..de979ca2 --- /dev/null +++ b/tests/operations/test_function_properties.py @@ -0,0 +1,221 @@ +import pytest +from quam.core.operation.function_properties import FunctionProperties +from quam.components import QuantumComponent + + +class DummyQuantumComponent(QuantumComponent): + """Dummy component for testing.""" + + pass + + +def test_function_properties_initialization(): + """Test basic initialization of FunctionProperties.""" + props = FunctionProperties( + quantum_component_name="component", + quantum_component_type=DummyQuantumComponent, + name="test_function", + required_args=["arg1", "arg2"], + optional_args={"opt1": 1, "opt2": "default"}, + ) + + assert props.quantum_component_name == "component" + assert props.quantum_component_type == DummyQuantumComponent + assert props.name == "test_function" + assert props.required_args == ["arg1", "arg2"] + assert props.optional_args == {"opt1": 1, "opt2": "default"} + + +def test_from_function_with_valid_function(): + """Test from_function with a valid function signature.""" + + def valid_operation( + component: DummyQuantumComponent, arg1: int, arg2: str, opt1: int = 1 + ): + pass + + props = FunctionProperties.from_function(valid_operation) + + assert props.quantum_component_name == "component" + assert props.quantum_component_type == DummyQuantumComponent + assert props.name == "valid_operation" + assert props.required_args == ["arg1", "arg2"] + assert props.optional_args == {"opt1": 1} + + +def test_from_function_with_only_required_args(): + """Test from_function with a function that has only required arguments.""" + + def operation(component: DummyQuantumComponent, arg1: int, arg2: str): + pass + + props = FunctionProperties.from_function(operation) + + assert props.quantum_component_name == "component" + assert props.required_args == ["arg1", "arg2"] + assert props.optional_args == {} + + +def test_from_function_with_only_optional_args(): + """Test from_function with a function that has only optional arguments.""" + + def operation( + component: DummyQuantumComponent, arg1: int = 1, arg2: str = "default" + ): + pass + + props = FunctionProperties.from_function(operation) + + assert props.quantum_component_name == "component" + assert props.required_args == [] + assert props.optional_args == {"arg1": 1, "arg2": "default"} + + +def test_from_function_with_no_args(): + """Test from_function with a function that has only the component parameter.""" + + def operation(component: DummyQuantumComponent): + pass + + props = FunctionProperties.from_function(operation) + + assert props.quantum_component_name == "component" + assert props.required_args == [] + assert props.optional_args == {} + + +def test_from_function_invalid_first_arg(): + """Test from_function with a function that doesn't have QuantumComponent as first arg.""" + + def invalid_operation(x: int, component: DummyQuantumComponent): + pass + + with pytest.raises( + ValueError, match="must accept a QuantumComponent as its first argument" + ): + FunctionProperties.from_function(invalid_operation) + + +def test_from_function_no_args(): + """Test from_function with a function that has no arguments.""" + + def invalid_operation(): + pass + + with pytest.raises(ValueError, match="must accept at least one argument"): + FunctionProperties.from_function(invalid_operation) + + +def test_from_function_wrong_type(): + """Test from_function with a function that has wrong type for first argument.""" + + def invalid_operation(component: int): + pass + + with pytest.raises( + ValueError, match="must accept a QuantumComponent as its first argument" + ): + FunctionProperties.from_function(invalid_operation) + + +def test_from_function_with_optional_type(): + """Test handling of Optional type hints.""" + from typing import Optional + + def operation(component: DummyQuantumComponent, arg: Optional[int] = None): + pass + + props = FunctionProperties.from_function(operation) + assert props.optional_args["arg"] is None + + +def test_function_properties_container_independence(): + """Test that container attributes are independent between instances.""" + props1 = FunctionProperties( + quantum_component_name="comp1", + quantum_component_type=DummyQuantumComponent, + required_args=["arg1"], + optional_args={"opt1": 1}, + ) + props2 = FunctionProperties( + quantum_component_name="comp2", + quantum_component_type=DummyQuantumComponent, + required_args=["arg1"], + optional_args={"opt1": 1}, + ) + + # Modify containers in first instance + props1.required_args.append("arg2") + props1.optional_args["opt2"] = 2 + + # Check that second instance wasn't affected + assert props2.required_args == ["arg1"] + assert props2.optional_args == {"opt1": 1} + + +def test_function_properties_invalid_argument_name(): + """Test that invalid argument names are rejected.""" + with pytest.raises(ValueError, match="Invalid argument name: '123invalid'"): + FunctionProperties( + quantum_component_name="comp", + quantum_component_type=DummyQuantumComponent, + required_args=["123invalid"], + ) + + with pytest.raises(ValueError, match="Invalid argument name: 'invalid@name'"): + FunctionProperties( + quantum_component_name="comp", + quantum_component_type=DummyQuantumComponent, + optional_args={"invalid@name": 1}, + ) + + +def test_function_properties_python_keyword_argument(): + """Test that Python keywords are rejected as argument names.""" + with pytest.raises(ValueError, match="Argument name cannot be a Python keyword: 'class'"): + FunctionProperties( + quantum_component_name="comp", + quantum_component_type=DummyQuantumComponent, + required_args=["class"], + ) + + with pytest.raises(ValueError, match="Argument name cannot be a Python keyword: 'return'"): + FunctionProperties( + quantum_component_name="comp", + quantum_component_type=DummyQuantumComponent, + optional_args={"return": 1}, + ) + + +def test_from_function_with_complex_type_hints(): + """Test handling of complex type hints like Union and Optional.""" + from typing import Union, Optional + + def operation( + component: DummyQuantumComponent, + arg1: Union[int, str], + arg2: Optional[float] = None + ): + pass + + props = FunctionProperties.from_function(operation) + assert props.required_args == ["arg1"] + assert props.optional_args == {"arg2": None} + + +def test_from_function_without_annotations(): + """Test that function works with parameters that have no type annotations.""" + def operation(component, arg1, arg2=None): + pass + + with pytest.raises(ValueError, match="missing type annotation"): + FunctionProperties.from_function(operation) + + +def test_from_function_with_non_type_annotation(): + """Test handling of invalid type annotations.""" + def operation(component: "not a real type", arg1: int): + pass + + with pytest.raises(ValueError, match="missing type annotation"): + FunctionProperties.from_function(operation) From b5c7f1d48a335ee2adfc2f967e2b0e543bc8d9c8 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 11:49:40 +0100 Subject: [PATCH 32/43] minor changes to test_function_properties --- tests/operations/test_function_properties.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/operations/test_function_properties.py b/tests/operations/test_function_properties.py index de979ca2..9142680b 100644 --- a/tests/operations/test_function_properties.py +++ b/tests/operations/test_function_properties.py @@ -1,8 +1,10 @@ import pytest from quam.core.operation.function_properties import FunctionProperties +from quam.core.quam_classes import quam_dataclass from quam.components import QuantumComponent +@quam_dataclass class DummyQuantumComponent(QuantumComponent): """Dummy component for testing.""" @@ -85,7 +87,8 @@ def operation(component: DummyQuantumComponent): def test_from_function_invalid_first_arg(): - """Test from_function with a function that doesn't have QuantumComponent as first arg.""" + """Test from_function with a function that doesn't have QuantumComponent as + first arg.""" def invalid_operation(x: int, component: DummyQuantumComponent): pass @@ -172,14 +175,18 @@ def test_function_properties_invalid_argument_name(): def test_function_properties_python_keyword_argument(): """Test that Python keywords are rejected as argument names.""" - with pytest.raises(ValueError, match="Argument name cannot be a Python keyword: 'class'"): + with pytest.raises( + ValueError, match="Argument name cannot be a Python keyword: 'class'" + ): FunctionProperties( quantum_component_name="comp", quantum_component_type=DummyQuantumComponent, required_args=["class"], ) - with pytest.raises(ValueError, match="Argument name cannot be a Python keyword: 'return'"): + with pytest.raises( + ValueError, match="Argument name cannot be a Python keyword: 'return'" + ): FunctionProperties( quantum_component_name="comp", quantum_component_type=DummyQuantumComponent, @@ -194,7 +201,7 @@ def test_from_function_with_complex_type_hints(): def operation( component: DummyQuantumComponent, arg1: Union[int, str], - arg2: Optional[float] = None + arg2: Optional[float] = None, ): pass @@ -205,6 +212,7 @@ def operation( def test_from_function_without_annotations(): """Test that function works with parameters that have no type annotations.""" + def operation(component, arg1, arg2=None): pass @@ -214,7 +222,8 @@ def operation(component, arg1, arg2=None): def test_from_function_with_non_type_annotation(): """Test handling of invalid type annotations.""" - def operation(component: "not a real type", arg1: int): + + def operation(component: "not a real type", arg1: int): # type: ignore pass with pytest.raises(ValueError, match="missing type annotation"): From 2240f6fceb3609ff56905c78c64ed0161e27a04c Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 11:49:56 +0100 Subject: [PATCH 33/43] fixes in test_qubit_pair --- .../quantum_components/test_qubit_pair.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/components/quantum_components/test_qubit_pair.py b/tests/components/quantum_components/test_qubit_pair.py index 44201e17..100200c3 100644 --- a/tests/components/quantum_components/test_qubit_pair.py +++ b/tests/components/quantum_components/test_qubit_pair.py @@ -19,7 +19,7 @@ class MockQubitPair(QubitPair): @quam_dataclass class QUAM(QuamRoot): qubits: Dict[str, MockQubit] - qubit_pairs: str[str, MockQubitPair] + qubit_pairs: Dict[str, MockQubitPair] @pytest.fixture @@ -51,9 +51,7 @@ def test_qubit_target(): @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 + id="pair_1", qubit_control=test_qubit_control, qubit_target=test_qubit_target ) @@ -65,7 +63,9 @@ def test_quam(test_qubit_control, test_qubit_target, test_qubit_pair): ) -def test_qubit_pair_initialization(test_qubit_pair, test_qubit_control, test_qubit_target): +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 @@ -76,10 +76,10 @@ def test_qubit_pair_initialization(test_qubit_pair, test_qubit_control, test_qub 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') - + 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) @@ -87,9 +87,9 @@ 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 @@ -98,11 +98,13 @@ def test_qubit_pair_via_matmul(test_quam): 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"): + + with pytest.raises( + ValueError, match="Cannot create a qubit pair .* with a non-qubit object" + ): _ = control @ "not_a_qubit" @@ -110,7 +112,7 @@ 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 From 3e03cf87cb88122101ea87a8f6c26f57d50af95f Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 11:50:11 +0100 Subject: [PATCH 34/43] first tests for test_operation --- quam/core/operation/operation.py | 67 ++++++++++++++++---- tests/operations/test_operations.py | 94 +++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 tests/operations/test_operations.py diff --git a/quam/core/operation/operation.py b/quam/core/operation/operation.py index f949b60b..0ef7647d 100644 --- a/quam/core/operation/operation.py +++ b/quam/core/operation/operation.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Optional, Any from quam.core.operation.function_properties import FunctionProperties from quam.components import QuantumComponent @@ -8,24 +8,69 @@ class Operation: - def __init__(self, func: Callable, unitary=None): + def __init__(self, func: Callable, unitary: Optional[Any] = None): + """ + Initialize a quantum operation. + + This is typically used implicitly from the decorator @operations_registry.register_operation. + + Args: + func: The function implementing the operation + unitary: Optional unitary matrix representing the operation + """ self.func = func self.unitary = unitary self.properties = FunctionProperties.from_function(func) def get_macro(self, quantum_component: QuantumComponent): + """ + Get the macro implementation for this operation from a quantum component. + + Args: + quantum_component: Component to get the macro from + + Returns: + The macro implementation + + Raises: + KeyError: If the macro is not implemented for this component + """ macros = quantum_component.get_macros() - return macros[self.properties.name] + try: + return macros[self.properties.name] + except KeyError: + raise KeyError( + f"Operation '{self.properties.name}' is not implemented for " + f"{quantum_component.__class__.__name__}" + ) def __call__(self, *args, **kwargs): - if not args or not isinstance(args[0], QuantumComponent): - if self.properties.quantum_component_type is not None: - raise ValueError( - f"First argument to {self.properties.name} must be a " - f"{self.properties.quantum_component_type.__name__}" - ) + """ + Execute the operation on a quantum component. - quantum_component, *required_args = args + Args: + *args: Positional arguments, first must be a quantum component + **kwargs: Keyword arguments for the operation + + Returns: + Result of the macro execution + + Raises: + ValueError: If first argument is not the correct quantum component type + """ + if not args: + raise ValueError( + f"Operation {self.properties.name} requires at least one argument" + ) + quantum_component = args[0] + if not isinstance(quantum_component, self.properties.quantum_component_type): + raise ValueError( + f"First argument to {self.properties.name} must be a " + f"{self.properties.quantum_component_type.__name__}, got " + f"{type(quantum_component).__name__}" + ) + + quantum_component, *required_args = args macro = self.get_macro(quantum_component) - return macro.apply(quantum_component, *required_args, **kwargs) + return macro.apply(*required_args, **kwargs) diff --git a/tests/operations/test_operations.py b/tests/operations/test_operations.py new file mode 100644 index 00000000..43e3fc21 --- /dev/null +++ b/tests/operations/test_operations.py @@ -0,0 +1,94 @@ +import pytest +from quam.core.operation.operation import Operation +from quam.core.operation.function_properties import FunctionProperties +from quam.components import Qubit +from quam.components.macro import QubitMacro +from quam.core import quam_dataclass + + +@quam_dataclass +class TestMacro(QubitMacro): + """Simple macro class for testing purposes""" + + def apply(self, *args, **kwargs): + # Return inputs to verify they were passed correctly + return (self.qubit, args, kwargs) + + +@pytest.fixture +def test_qubit(): + """Fixture providing a qubit with common test macros""" + qubit = Qubit(id="test_qubit") + + # Add some common macros + qubit.macros["x_gate"] = TestMacro() + qubit.macros["test_op"] = TestMacro() + + return qubit + + +def test_operation_initialization(): + def sample_op(qubit: Qubit): + pass + + op = Operation(sample_op) + assert op.func == sample_op + assert op.unitary is None + assert isinstance(op.properties, FunctionProperties) + assert op.properties.name == "sample_op" + assert op.properties.quantum_component_type == Qubit + + +def test_operation_get_macro(test_qubit): + def x_gate(qubit: Qubit): + pass + + op = Operation(x_gate) + retrieved_macro = op.get_macro(test_qubit) + assert retrieved_macro == test_qubit.macros["x_gate"] + + +def test_operation_get_macro_missing(test_qubit): + def missing_op(qubit: Qubit): + pass + + op = Operation(missing_op) + with pytest.raises(KeyError, match="Operation 'missing_op' is not implemented"): + op.get_macro(test_qubit) + + +def test_operation_call(test_qubit): + def test_op(qubit: Qubit, amplitude: float = 1.0): + pass + + op = Operation(test_op) + result = op(test_qubit, amplitude=0.5) + + # Check results + assert result[0] == test_qubit # First element should be the qubit + assert result[1] == () # No positional args + assert result[2] == {"amplitude": 0.5} # Keyword args + + +def test_operation_call_invalid_component(): + def test_op(qubit: Qubit): + pass + + op = Operation(test_op) + + # Try to call with wrong type + with pytest.raises(ValueError, match="First argument to test_op must be a Qubit"): + op("not_a_qubit") + + +def test_operation_call_no_args(): + def test_op(qubit: Qubit): + pass + + op = Operation(test_op) + + # Try to call with no arguments + with pytest.raises( + ValueError, match="Operation test_op requires at least one argument" + ): + op() From b25142cee23040480b2db0743e0692c5702c3f38 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 11:56:36 +0100 Subject: [PATCH 35/43] added final tests to test_operations --- tests/operations/test_operations.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/operations/test_operations.py b/tests/operations/test_operations.py index 43e3fc21..39440765 100644 --- a/tests/operations/test_operations.py +++ b/tests/operations/test_operations.py @@ -15,6 +15,15 @@ def apply(self, *args, **kwargs): return (self.qubit, args, kwargs) +@quam_dataclass +class TestMacro2(QubitMacro): + """Test macro class that requires a positional argument""" + + def apply(self, required_arg, **kwargs): + # Return inputs to verify they were passed correctly + return (self.qubit, (required_arg,), kwargs) + + @pytest.fixture def test_qubit(): """Fixture providing a qubit with common test macros""" @@ -92,3 +101,41 @@ def test_op(qubit: Qubit): ValueError, match="Operation test_op requires at least one argument" ): op() + + +def test_operation_with_unitary(): + def test_op(qubit: Qubit): + pass + + test_unitary = [[1, 0], [0, 1]] # Example unitary matrix + op = Operation(test_op, unitary=test_unitary) + assert op.unitary == test_unitary + + +def test_operation_call_multiple_args(test_qubit): + def test_op(qubit: Qubit, arg1: float, arg2: str): + pass + + op = Operation(test_op) + result = op(test_qubit, 1.0, "test") + + assert result[0] == test_qubit + assert result[1] == (1.0, "test") + assert result[2] == {} # No keyword args + + +def test_operation_call_out_of_order_kwargs(test_qubit): + def test_op(qubit: Qubit, arg1: float, arg2: str = "default"): + pass + + # Use TestMacro2 which requires a positional argument + macro = TestMacro2() + test_qubit.macros["test_op"] = macro + + op = Operation(test_op) + # Pass arg2 before arg1, making arg1 (a positional arg) into a kwarg + result = op(test_qubit, arg2="test", required_arg=1.0) + + assert result[0] == test_qubit + assert result[1] == (1.0,) # arg1 as positional arg + assert result[2] == {"arg2": "test"} # arg2 as kwarg From e7a95eb06bebf9b335b5399a5a038f58a5607819 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 12:01:57 +0100 Subject: [PATCH 36/43] added tests for register_operations --- tests/operations/conftest.py | 25 ++++++ tests/operations/test_operations.py | 39 ++------ tests/operations/test_register_operations.py | 94 +++++++++++++++++++- 3 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 tests/operations/conftest.py diff --git a/tests/operations/conftest.py b/tests/operations/conftest.py new file mode 100644 index 00000000..38e7f445 --- /dev/null +++ b/tests/operations/conftest.py @@ -0,0 +1,25 @@ +import pytest +from quam.core.quam_classes import quam_dataclass +from quam.components import Qubit +from quam.components.macro import QubitMacro + + +@quam_dataclass +class TestMacro(QubitMacro): + """Simple macro class for testing purposes""" + + def apply(self, *args, **kwargs): + # Return inputs to verify they were passed correctly + return (self.qubit, args, kwargs) + + +@pytest.fixture +def test_qubit(): + """Fixture providing a qubit with common test macros""" + qubit = Qubit(id="test_qubit") + + # Add some common macros + qubit.macros["x_gate"] = TestMacro() + qubit.macros["test_op"] = TestMacro() + + return qubit diff --git a/tests/operations/test_operations.py b/tests/operations/test_operations.py index 39440765..339711cc 100644 --- a/tests/operations/test_operations.py +++ b/tests/operations/test_operations.py @@ -6,36 +6,6 @@ from quam.core import quam_dataclass -@quam_dataclass -class TestMacro(QubitMacro): - """Simple macro class for testing purposes""" - - def apply(self, *args, **kwargs): - # Return inputs to verify they were passed correctly - return (self.qubit, args, kwargs) - - -@quam_dataclass -class TestMacro2(QubitMacro): - """Test macro class that requires a positional argument""" - - def apply(self, required_arg, **kwargs): - # Return inputs to verify they were passed correctly - return (self.qubit, (required_arg,), kwargs) - - -@pytest.fixture -def test_qubit(): - """Fixture providing a qubit with common test macros""" - qubit = Qubit(id="test_qubit") - - # Add some common macros - qubit.macros["x_gate"] = TestMacro() - qubit.macros["test_op"] = TestMacro() - - return qubit - - def test_operation_initialization(): def sample_op(qubit: Qubit): pass @@ -124,6 +94,15 @@ def test_op(qubit: Qubit, arg1: float, arg2: str): assert result[2] == {} # No keyword args +@quam_dataclass +class TestMacro2(QubitMacro): + """Test macro class that requires a positional argument""" + + def apply(self, required_arg, **kwargs): + # Return inputs to verify they were passed correctly + return (self.qubit, (required_arg,), kwargs) + + def test_operation_call_out_of_order_kwargs(test_qubit): def test_op(qubit: Qubit, arg1: float, arg2: str = "default"): pass diff --git a/tests/operations/test_register_operations.py b/tests/operations/test_register_operations.py index 3a4ae45f..d0bb005e 100644 --- a/tests/operations/test_register_operations.py +++ b/tests/operations/test_register_operations.py @@ -1,2 +1,92 @@ -from quam.core import OperationsRegistry -from quam.components.quantum_components.qubit import Qubit +import pytest +from quam.core.operation.operations_registry import OperationsRegistry +from quam.core.operation.operation import Operation +from quam.components import Qubit + + +def test_operations_registry_initialization(): + registry = OperationsRegistry() + assert len(registry) == 0 + + +def test_register_operation_basic(): + def test_op(qubit: Qubit): + pass + + registry = OperationsRegistry() + wrapped_op = registry.register_operation(test_op) + + # Check the operation was registered + assert "test_op" in registry + assert isinstance(registry["test_op"], Operation) + + # Check the wrapped operation maintains the original function's metadata + assert wrapped_op.__name__ == "test_op" + assert wrapped_op.__doc__ == test_op.__doc__ + + +def test_register_operation_with_unitary(): + def test_op(qubit: Qubit): + pass + + registry = OperationsRegistry() + test_unitary = [[1, 0], [0, 1]] + wrapped_op = registry.register_operation(test_op, unitary=test_unitary) + + assert registry["test_op"].unitary == test_unitary + + +def test_register_operation_as_decorator(): + registry = OperationsRegistry() + + @registry.register_operation + def test_op(qubit: Qubit): + pass + + assert "test_op" in registry + assert isinstance(registry["test_op"], Operation) + + +def test_register_operation_as_decorator_with_unitary(): + registry = OperationsRegistry() + test_unitary = [[1, 0], [0, 1]] + + @registry.register_operation(unitary=test_unitary) + def test_op(qubit: Qubit): + pass + + assert "test_op" in registry + assert isinstance(registry["test_op"], Operation) + assert registry["test_op"].unitary == test_unitary + + +def test_register_multiple_operations(): + registry = OperationsRegistry() + + def op1(qubit: Qubit): + pass + + def op2(qubit: Qubit): + pass + + registry.register_operation(op1) + registry.register_operation(op2) + + assert len(registry) == 2 + assert "op1" in registry + assert "op2" in registry + + +def test_registered_operation_callable(test_qubit): + registry = OperationsRegistry() + + @registry.register_operation + def test_op(qubit: Qubit, amplitude: float = 1.0): + pass + + # Verify the registered operation can be called and works correctly + result = registry["test_op"](test_qubit, amplitude=0.5) + + assert result[0] == test_qubit + assert result[1] == () + assert result[2] == {"amplitude": 0.5} From c01124c1eff750436f12b9a7865e067f766ab52a Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 6 Dec 2024 12:10:06 +0100 Subject: [PATCH 37/43] fixed qubit pair tests --- quam/components/quantum_components/qubit.py | 8 +++++- .../quantum_components/test_qubit_pair.py | 26 ++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 9c8e8d8e..ea95b612 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -1,3 +1,4 @@ +from collections import UserDict from typing import Dict, Union, TYPE_CHECKING, Any from dataclasses import field @@ -103,7 +104,12 @@ def __matmul__(self, other): # TODO Add QubitPair return type "Please add a 'qubit_pairs' attribute to the root component." ) - for qubit_pair in self._root.qubit_pairs: + if isinstance(self._root.qubit_pairs, UserDict): + qubit_pairs = self._root.qubit_pairs.values() + else: + qubit_pairs = self._root.qubit_pairs + + for qubit_pair in qubit_pairs: if qubit_pair.qubit_control is self and qubit_pair.qubit_target is other: return qubit_pair else: diff --git a/tests/components/quantum_components/test_qubit_pair.py b/tests/components/quantum_components/test_qubit_pair.py index 100200c3..8da1f449 100644 --- a/tests/components/quantum_components/test_qubit_pair.py +++ b/tests/components/quantum_components/test_qubit_pair.py @@ -1,3 +1,5 @@ +from collections import UserDict +from dataclasses import field from typing import Dict, List import pytest from quam.components import Qubit, QubitPair @@ -19,7 +21,7 @@ class MockQubitPair(QubitPair): @quam_dataclass class QUAM(QuamRoot): qubits: Dict[str, MockQubit] - qubit_pairs: Dict[str, MockQubitPair] + qubit_pairs: Dict[str, MockQubitPair] = field(default_factory=dict) @pytest.fixture @@ -49,18 +51,22 @@ def test_qubit_target(): @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 +def test_quam(test_qubit_control, test_qubit_target): + machine = QUAM( + qubits={"control": test_qubit_control, "target": test_qubit_target}, + ) + + machine.qubit_pairs["pair_1"] = MockQubitPair( + id="pair_1", + qubit_control=test_qubit_control.get_reference(), + qubit_target=test_qubit_target.get_reference(), ) + return machine @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(test_quam): + return test_quam.qubit_pairs["pair_1"] def test_qubit_pair_initialization( @@ -70,7 +76,7 @@ def test_qubit_pair_initialization( 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 isinstance(test_qubit_pair.macros, UserDict) assert len(test_qubit_pair.macros) == 0 From 3fd27a1976168627bb6861d95049bad6d70547d3 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 9 Dec 2024 15:14:07 +0100 Subject: [PATCH 38/43] Slight changes to macros --- quam/components/macro/qubit_macros.py | 1 - quam/components/quantum_components/qubit.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quam/components/macro/qubit_macros.py b/quam/components/macro/qubit_macros.py index 8eb9449c..4f5608f6 100644 --- a/quam/components/macro/qubit_macros.py +++ b/quam/components/macro/qubit_macros.py @@ -32,7 +32,6 @@ class PulseMacro(QubitMacro): """ pulse: Union[Pulse, str] # type: ignore - unitary: Optional[List[List[float]]] = None def apply(self, *, amplitude_scale=None, duration=None, **kwargs): if isinstance(self.pulse, Pulse): diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index ea95b612..18faea2c 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -1,4 +1,5 @@ from collections import UserDict +from collections.abc import Iterable from typing import Dict, Union, TYPE_CHECKING, Any from dataclasses import field @@ -77,7 +78,7 @@ def get_pulse(self, pulse_name: str) -> Pulse: return pulses[0] @QuantumComponent.register_macro - def align(self, *other_qubits: "Qubit"): + def align(self, other_qubits: Iterable["Qubit"]): """Aligns the execution of all channels of this qubit and all other qubits""" channel_names = [channel.name for channel in self.channels.values()] for qubit in other_qubits: From 9a4be2aa3ba8a72fa726a213bc3c9fe8a31ffc8b Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 9 Dec 2024 21:22:40 +0100 Subject: [PATCH 39/43] add documentation for qubits and qubit pairs --- docs/components/qubits-and-qubit-pairs.md | 132 ++++++++++++++++++++ quam/components/quantum_components/qubit.py | 4 +- 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 docs/components/qubits-and-qubit-pairs.md diff --git a/docs/components/qubits-and-qubit-pairs.md b/docs/components/qubits-and-qubit-pairs.md new file mode 100644 index 00000000..aa0ee813 --- /dev/null +++ b/docs/components/qubits-and-qubit-pairs.md @@ -0,0 +1,132 @@ +# Qubits and Qubit Pairs + +## Overview +Qubits and qubit pairs are essential components in quantum processing units (QPUs), implemented as subclasses of `QuantumComponent`. + +### Qubits +The `Qubit` class models a physical qubit on the QPU, encapsulating: +- Qubit-specific attributes (e.g., frequency) +- Quantum control channels (drive, flux, readout) +- Single-qubit gate operations +- Hardware-specific logic and calibration data + +The `Qubit` class is typically subclassed to add channels and other qubit-specific information as properties. In this example, we define a `Transmon` class, a subclass of `Qubit`, with specific channels: + +```python +from quam.components import BasicQuam, Qubit, IQChannel, SingleChannel +from quam.core import quam_dataclass + +@quam_dataclass +class Transmon(Qubit): + drive: IQChannel + flux: SingleChannel + +machine = BasicQuam() +``` + +We create two qubit instances, `q1` and `q2`, as follows: + +```python +q1 = machine.qubits["q1"] = Transmon( + drive=IQChannel( + opx_output_I=("con1", 1, 1), + opx_output_Q=("con1", 1, 2), + frequency_converter_up=None), + flux=SingleChannel(opx_output=("con1", 1, 3)), +) + +q2 = machine.qubits["q2"] = Transmon( + drive=IQChannel( + opx_output_I=("con1", 1, 5), + opx_output_Q=("con1", 1, 6), + frequency_converter_up=None), + flux=SingleChannel(opx_output=("con1", 1, 7)), +) +``` + +### Qubit Pairs +The `QubitPair` class models the interaction between two qubits, managing: +- Two-qubit gate operations +- Coupling elements (e.g., tunable couplers) +- Interaction-specific properties and calibrations +- Hardware topology constraints + +We create a `QubitPair` using the qubits `q1` and `q2`: + +```python +machine.qubit_pairs["q1@q2"] = QubitPair( + qubit_control=q1.get_reference(), # "#/qubits/q1" + qubit_target=q2.get_reference() # "#/qubits/q2" +) +``` + +The `get_reference()` method is used to obtain a reference to the qubit, ensuring each QuAM component has a single parent, which for qubits is the `machine.qubits` dictionary. + +Both components offer interfaces for quantum operations through macros, enabling hardware-agnostic control while maintaining device-specific implementations. + +## Quantum Components +The `QuantumComponent` class is the base class for qubits and qubit pairs, providing: +- A unique identifier via the `id` property +- A collection of macros defining operations +- An abstract `name` property that derived classes must implement +- A standardized method to apply operations through the `apply()` method + +## Qubits +A `Qubit` represents a single quantum bit, acting as: +- A container for quantum control channels (drive, flux, readout, etc.) +- A collection point for pulse operations on its channels +- An endpoint for single-qubit operations via macros + + + +### Key Features + +```python +# Accessing channels +channels = q1.channels # Returns a dictionary of all channels + +# Finding pulses +pulse = q1.get_pulse("pi") # Retrieves the pulse named "pi" from any channel + +# Aligning operations +q1.align(q2) # Synchronizes all channels of q1 and q2 +``` + +## Qubit Pairs +A `QubitPair` represents the relationship between two qubits, typically used for two-qubit operations. It includes: +- References to both the control and target qubits +- Macros for two-qubit operations +- Automatic naming based on the constituent qubits + +### Key Features + +Once the qubit pair is added to the root-level [QuamRoot.qubit_pairs][quam.core.quam_classes.QuamRoot.qubit_pairs] dictionary, it can be accessed directly from the qubits using the `@` operator: + +```python +q1 @ q2 # Returns the qubit pair +``` +```python +# Automatic naming +pair = machine.qubit_pairs["q1@q2"] +pair.name # Returns "q1@q2" + +# Accessing qubits +pair.qubit_control, pair.qubit_target # Returns the control and target qubits + +# Applying two-qubit operations +pair.apply("cz_gate") # Applies the CZ gate macro +``` + + +## Macros and Operations +Both qubits and qubit pairs can contain macros, which serve as high-level interfaces to quantum operations. These macros: +- Define the implementation of quantum gates +- Can be registered using the `@QuantumComponent.register_macro` decorator +- Are accessible through the `apply()` method or directly as methods +- Provide a bridge between the hardware configuration and gate-level operations + +For detailed information about macros and gate-level operations, see: +- [Macros Documentation](./macros.md) +- [Gate-Level Operations Documentation](./operations.md) + +This documentation provides a high-level overview of the qubit and qubit pair functionality while referencing the macro and gate-level operations that will be detailed in other documentation pages. \ No newline at end of file diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 18faea2c..0a61e22f 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -78,8 +78,10 @@ def get_pulse(self, pulse_name: str) -> Pulse: return pulses[0] @QuantumComponent.register_macro - def align(self, other_qubits: Iterable["Qubit"]): + def align(self, other_qubits: Union["Qubit", Iterable["Qubit"]]): """Aligns the execution of all channels of this qubit and all other qubits""" + if isinstance(other_qubits, Qubit): + other_qubits = [other_qubits] channel_names = [channel.name for channel in self.channels.values()] for qubit in other_qubits: channel_names.extend([channel.name for channel in qubit.channels.values()]) From dba13c63faaa78dbfdb955e976329a9062007d0c Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 11 Dec 2024 14:55:14 +0100 Subject: [PATCH 40/43] remove unitary --- quam/core/operation/operation.py | 4 +--- quam/core/operation/operations_registry.py | 9 +++---- .../superconducting_qubits/operations.py | 6 ++--- tests/operations/test_operations.py | 10 -------- tests/operations/test_register_operations.py | 24 ------------------- 5 files changed, 8 insertions(+), 45 deletions(-) diff --git a/quam/core/operation/operation.py b/quam/core/operation/operation.py index 0ef7647d..02021668 100644 --- a/quam/core/operation/operation.py +++ b/quam/core/operation/operation.py @@ -8,7 +8,7 @@ class Operation: - def __init__(self, func: Callable, unitary: Optional[Any] = None): + def __init__(self, func: Callable): """ Initialize a quantum operation. @@ -16,10 +16,8 @@ def __init__(self, func: Callable, unitary: Optional[Any] = None): Args: func: The function implementing the operation - unitary: Optional unitary matrix representing the operation """ self.func = func - self.unitary = unitary self.properties = FunctionProperties.from_function(func) def get_macro(self, quantum_component: QuantumComponent): diff --git a/quam/core/operation/operations_registry.py b/quam/core/operation/operations_registry.py index a62bbd56..ffecea94 100644 --- a/quam/core/operation/operations_registry.py +++ b/quam/core/operation/operations_registry.py @@ -13,7 +13,7 @@ class OperationsRegistry(UserDict): """A registry to store and manage operations.""" - def register_operation(self, func: Optional[T] = None, unitary=None) -> T: + def register_operation(self, func: Optional[T]) -> T: """ Register a function as an operation. @@ -27,10 +27,11 @@ def register_operation(self, func: Optional[T] = None, unitary=None) -> T: Returns: callable: The wrapped function. """ - if func is None: - return functools.partial(self.register_operation, unitary=unitary) + # Optionally add this later such that we can pass parameters to the decorator + # if func is None: + # return functools.partial(self.register_operation) - operation = Operation(func, unitary=unitary) + operation = Operation(func) operation = functools.update_wrapper(operation, func) self[func.__name__] = operation diff --git a/quam/examples/superconducting_qubits/operations.py b/quam/examples/superconducting_qubits/operations.py index ed428a89..1a93923c 100644 --- a/quam/examples/superconducting_qubits/operations.py +++ b/quam/examples/superconducting_qubits/operations.py @@ -17,10 +17,8 @@ def y(qubit: Qubit, **kwargs): pass -@operations_registry.register_operation( - unitary=[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]] -) -def U(qubit: Qubit, **kwargs): +@operations_registry.register_operation +def Rx(qubit: Qubit, angle: float, **kwargs): pass diff --git a/tests/operations/test_operations.py b/tests/operations/test_operations.py index 339711cc..af86c246 100644 --- a/tests/operations/test_operations.py +++ b/tests/operations/test_operations.py @@ -12,7 +12,6 @@ def sample_op(qubit: Qubit): op = Operation(sample_op) assert op.func == sample_op - assert op.unitary is None assert isinstance(op.properties, FunctionProperties) assert op.properties.name == "sample_op" assert op.properties.quantum_component_type == Qubit @@ -73,15 +72,6 @@ def test_op(qubit: Qubit): op() -def test_operation_with_unitary(): - def test_op(qubit: Qubit): - pass - - test_unitary = [[1, 0], [0, 1]] # Example unitary matrix - op = Operation(test_op, unitary=test_unitary) - assert op.unitary == test_unitary - - def test_operation_call_multiple_args(test_qubit): def test_op(qubit: Qubit, arg1: float, arg2: str): pass diff --git a/tests/operations/test_register_operations.py b/tests/operations/test_register_operations.py index d0bb005e..36055e23 100644 --- a/tests/operations/test_register_operations.py +++ b/tests/operations/test_register_operations.py @@ -25,17 +25,6 @@ def test_op(qubit: Qubit): assert wrapped_op.__doc__ == test_op.__doc__ -def test_register_operation_with_unitary(): - def test_op(qubit: Qubit): - pass - - registry = OperationsRegistry() - test_unitary = [[1, 0], [0, 1]] - wrapped_op = registry.register_operation(test_op, unitary=test_unitary) - - assert registry["test_op"].unitary == test_unitary - - def test_register_operation_as_decorator(): registry = OperationsRegistry() @@ -47,19 +36,6 @@ def test_op(qubit: Qubit): assert isinstance(registry["test_op"], Operation) -def test_register_operation_as_decorator_with_unitary(): - registry = OperationsRegistry() - test_unitary = [[1, 0], [0, 1]] - - @registry.register_operation(unitary=test_unitary) - def test_op(qubit: Qubit): - pass - - assert "test_op" in registry - assert isinstance(registry["test_op"], Operation) - assert registry["test_op"].unitary == test_unitary - - def test_register_multiple_operations(): registry = OperationsRegistry() From e7addc7ddc70b5e7e82da3fac3fef2ab006ff22c Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 12 Dec 2024 11:14:25 +0100 Subject: [PATCH 41/43] Align is no longer a macro --- quam/components/quantum_components/qubit.py | 5 +---- tests/components/quantum_components/test_qubit.py | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 0a61e22f..8fca4e32 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -77,11 +77,8 @@ def get_pulse(self, pulse_name: str) -> Pulse: else: return pulses[0] - @QuantumComponent.register_macro - def align(self, other_qubits: Union["Qubit", Iterable["Qubit"]]): + def align(self, *other_qubits: "Qubit"): """Aligns the execution of all channels of this qubit and all other qubits""" - if isinstance(other_qubits, Qubit): - other_qubits = [other_qubits] channel_names = [channel.name for channel in self.channels.values()] for qubit in other_qubits: channel_names.extend([channel.name for channel in qubit.channels.values()]) diff --git a/tests/components/quantum_components/test_qubit.py b/tests/components/quantum_components/test_qubit.py index 298bc06e..270fd05d 100644 --- a/tests/components/quantum_components/test_qubit.py +++ b/tests/components/quantum_components/test_qubit.py @@ -57,13 +57,13 @@ def test_qubit_get_pulse_unique(mock_qubit): assert mock_qubit.get_pulse("test_pulse") == pulse -def test_qubit_align(mock_qubit_with_resonator, mocker): +def test_qubit_align(mock_qubit_with_resonator, mock_qubit, mocker): mocker.patch("quam.components.quantum_components.qubit.align") - mock_qubit_with_resonator.align(mock_qubit_with_resonator) + mock_qubit_with_resonator.align(mock_qubit) from quam.components.quantum_components.qubit import align - align.assert_called_once_with("q1.xy", "q1.resonator") + align.assert_called_once_with("q1.xy", "q1.resonator", "q0.xy") def test_qubit_get_macros(mock_qubit): @@ -73,7 +73,7 @@ def test_qubit_get_macros(mock_qubit): def test_qubit_apply_align(mock_qubit_with_resonator, mocker): mocker.patch("quam.components.quantum_components.qubit.align") - mock_qubit_with_resonator.apply("align") + mock_qubit_with_resonator.align() from quam.components.quantum_components.qubit import align From d292ddb66a53da436bd27d0816d74bd5d5aee9ff Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 12 Dec 2024 11:23:58 +0100 Subject: [PATCH 42/43] fix align --- quam/components/quantum_components/qubit.py | 28 +++++++++++++++---- .../quantum_components/test_qubit.py | 4 +-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/quam/components/quantum_components/qubit.py b/quam/components/quantum_components/qubit.py index 8fca4e32..424d2289 100644 --- a/quam/components/quantum_components/qubit.py +++ b/quam/components/quantum_components/qubit.py @@ -1,6 +1,6 @@ from collections import UserDict from collections.abc import Iterable -from typing import Dict, Union, TYPE_CHECKING, Any +from typing import Dict, List, Optional, Union, TYPE_CHECKING, Any from dataclasses import field from qm import qua @@ -77,11 +77,29 @@ def get_pulse(self, pulse_name: str) -> Pulse: else: return pulses[0] - def align(self, *other_qubits: "Qubit"): + @QuantumComponent.register_macro + def align( + self, + other_qubits: Optional[Union["Qubit", Iterable["Qubit"]]] = None, + *args: "Qubit", + ): """Aligns the execution of all channels of this qubit and all other qubits""" - channel_names = [channel.name for channel in self.channels.values()] - for qubit in other_qubits: - channel_names.extend([channel.name for channel in qubit.channels.values()]) + quantum_components = [self] + + if isinstance(other_qubits, Qubit): + quantum_components.append(other_qubits) + elif isinstance(other_qubits, Iterable): + quantum_components.extend(other_qubits) + elif other_qubits is not None: + raise ValueError(f"Invalid type for other_qubits: {type(other_qubits)}") + + if args: + assert all(isinstance(arg, Qubit) for arg in args) + quantum_components.extend(args) + + channel_names = { + ch.name for qubit in quantum_components for ch in qubit.channels.values() + } align(*channel_names) diff --git a/tests/components/quantum_components/test_qubit.py b/tests/components/quantum_components/test_qubit.py index 270fd05d..e6704ef6 100644 --- a/tests/components/quantum_components/test_qubit.py +++ b/tests/components/quantum_components/test_qubit.py @@ -63,7 +63,7 @@ def test_qubit_align(mock_qubit_with_resonator, mock_qubit, mocker): from quam.components.quantum_components.qubit import align - align.assert_called_once_with("q1.xy", "q1.resonator", "q0.xy") + align.assert_called_once_with(*{"q1.xy", "q1.resonator", "q0.xy"}) def test_qubit_get_macros(mock_qubit): @@ -77,7 +77,7 @@ def test_qubit_apply_align(mock_qubit_with_resonator, mocker): from quam.components.quantum_components.qubit import align - align.assert_called_once_with("q1.xy", "q1.resonator") + align.assert_called_once_with(*{"q1.xy", "q1.resonator"}) def test_qubit_inferred_id_direct(): From 433040fbe146f02273853e90321fc2e6aad3d9a0 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 12 Dec 2024 11:58:20 +0100 Subject: [PATCH 43/43] working measure operation --- quam/core/operation/function_properties.py | 47 ++++++------------- .../quantum_components/test_qubit.py | 8 +++- tests/operations/test_function_properties.py | 42 +++++++++++++++-- tests/operations/test_operations.py | 12 +++++ 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/quam/core/operation/function_properties.py b/quam/core/operation/function_properties.py index 7449dc84..1a1c3432 100644 --- a/quam/core/operation/function_properties.py +++ b/quam/core/operation/function_properties.py @@ -17,10 +17,10 @@ class FunctionProperties: """ Properties of a quantum operation function. - + This class extracts and stores metadata about functions that operate on quantum components, including argument information and type requirements. - + Attributes: quantum_component_name: Name of the parameter accepting the quantum component quantum_component_type: Type of quantum component the function operates on @@ -28,17 +28,19 @@ class FunctionProperties: required_args: List of required argument names after the quantum component optional_args: Dictionary of optional arguments and their default values """ + quantum_component_name: str quantum_component_type: Type[QC] name: str = "" required_args: list[str] = field(default_factory=list) optional_args: dict[str, Any] = field(default_factory=dict) + return_type: Optional[Type] = None def __post_init__(self): # Make a new list/dict to avoid sharing between instances self.required_args = list(self.required_args) self.optional_args = dict(self.optional_args) - + # Validate argument names all_args = self.required_args + list(self.optional_args) for arg in all_args: @@ -47,31 +49,6 @@ def __post_init__(self): if keyword.iskeyword(arg): raise ValueError(f"Argument name cannot be a Python keyword: {arg!r}") - @staticmethod - def _resolve_type(type_hint: Any) -> Optional[Type]: - """ - Resolve type hints, including string forward references and complex types. - - Args: - type_hint: Any type annotation - - Returns: - The resolved base type, or None if unresolvable - """ - if type_hint is None: - return None - # Handle string forward references - if isinstance(type_hint, str): - return None - # Handle Optional, Union, etc - if get_origin(type_hint) is not None: - args = get_args(type_hint) - return args[0] if args else None - # Handle regular types - if isinstance(type_hint, type): - return type_hint - return None - @staticmethod def _is_quantum_component_type(type_hint: Optional[Type]) -> bool: """Check if type is or inherits from QuantumComponent.""" @@ -101,15 +78,16 @@ def from_function(cls, func: Callable) -> "FunctionProperties": # Try to get type hints, gracefully handle missing annotations try: type_hints = get_type_hints(func) - except Exception: - type_hints = {} + except (NameError, TypeError): + # Fallback to using the raw annotations if get_type_hints fails + type_hints = getattr(func, "__annotations__", {}) parameters_iterator = iter(parameters) first_param_name = next(parameters_iterator) - + # Get and resolve the type of the first parameter - first_param_type = cls._resolve_type(type_hints.get(first_param_name)) - + first_param_type = type_hints.get(first_param_name) + if not cls._is_quantum_component_type(first_param_type): if first_param_type is None: msg = ( @@ -138,4 +116,7 @@ def from_function(cls, func: Callable) -> "FunctionProperties": # Store the default value directly function_properties.optional_args[param_name] = param.default + # Get the return type from the function annotations + function_properties.return_type = type_hints.get("return") + return function_properties diff --git a/tests/components/quantum_components/test_qubit.py b/tests/components/quantum_components/test_qubit.py index e6704ef6..f5c8ba39 100644 --- a/tests/components/quantum_components/test_qubit.py +++ b/tests/components/quantum_components/test_qubit.py @@ -63,7 +63,9 @@ def test_qubit_align(mock_qubit_with_resonator, mock_qubit, mocker): from quam.components.quantum_components.qubit import align - align.assert_called_once_with(*{"q1.xy", "q1.resonator", "q0.xy"}) + align.assert_called_once() + called_args, _ = align.call_args + assert set(called_args) == {"q1.xy", "q1.resonator", "q0.xy"} def test_qubit_get_macros(mock_qubit): @@ -77,7 +79,9 @@ def test_qubit_apply_align(mock_qubit_with_resonator, mocker): from quam.components.quantum_components.qubit import align - align.assert_called_once_with(*{"q1.xy", "q1.resonator"}) + align.assert_called_once() + called_args, _ = align.call_args + assert set(called_args) == {"q1.xy", "q1.resonator"} def test_qubit_inferred_id_direct(): diff --git a/tests/operations/test_function_properties.py b/tests/operations/test_function_properties.py index 9142680b..0e4c3076 100644 --- a/tests/operations/test_function_properties.py +++ b/tests/operations/test_function_properties.py @@ -220,11 +220,43 @@ def operation(component, arg1, arg2=None): FunctionProperties.from_function(operation) -def test_from_function_with_non_type_annotation(): - """Test handling of invalid type annotations.""" +def test_from_function_with_return_type(): + """Test that return type is correctly captured.""" - def operation(component: "not a real type", arg1: int): # type: ignore + def operation(component: DummyQuantumComponent) -> int: + return 42 + + props = FunctionProperties.from_function(operation) + assert props.return_type == int + + +def test_from_function_with_optional_return_type(): + """Test handling of Optional return type.""" + from typing import Optional + + def operation(component: DummyQuantumComponent) -> Optional[int]: + return None + + props = FunctionProperties.from_function(operation) + assert props.return_type == Optional[int] + + +def test_from_function_with_qua_return_type(): + """Test handling of QUA variable return types.""" + from qm.qua._expressions import QuaBoolType + + def operation(component: DummyQuantumComponent) -> QuaBoolType: pass - with pytest.raises(ValueError, match="missing type annotation"): - FunctionProperties.from_function(operation) + props = FunctionProperties.from_function(operation) + assert props.return_type == QuaBoolType + + +def test_from_function_without_return_type(): + """Test handling of functions without return type annotation.""" + + def operation(component: DummyQuantumComponent): + pass + + props = FunctionProperties.from_function(operation) + assert props.return_type is None diff --git a/tests/operations/test_operations.py b/tests/operations/test_operations.py index af86c246..4763d3dd 100644 --- a/tests/operations/test_operations.py +++ b/tests/operations/test_operations.py @@ -108,3 +108,15 @@ def test_op(qubit: Qubit, arg1: float, arg2: str = "default"): assert result[0] == test_qubit assert result[1] == (1.0,) # arg1 as positional arg assert result[2] == {"arg2": "test"} # arg2 as kwarg + + +def test_measure_operation(test_qubit): + from qm.qua._expressions import QuaBoolType + + def measure(qubit: Qubit, **kwargs) -> QuaBoolType: + pass + + op = Operation(measure) + + assert op.properties.return_type == QuaBoolType + assert op.func == measure