From d62e288961af1b9a42b14667e1167fca301e2708 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer <3620100+rmshaffer@users.noreply.github.com> Date: Mon, 6 May 2024 12:04:13 -0400 Subject: [PATCH] feat: Add gate modifiers and additional supported gates to AutoQASM (#958) * Implement control, control_state, power gate modifiers * Enable gate modifiers on custom gates * Add prx gate * Add physical qubit test case * Update type checks * Use BasisStateInput for control_state * Simplify control_state logic * Add docstrings and improve test parameterizations --- src/braket/experimental/autoqasm/api.py | 5 +- .../autoqasm/instructions/gates.py | 157 ++++++++++++++---- .../autoqasm/instructions/instructions.py | 85 +++++++++- .../autoqasm/instructions/measurements.py | 8 +- .../autoqasm/test_gate_decorator.py | 21 +++ .../autoqasm/test_gate_definitions.py | 75 +++++++++ 6 files changed, 310 insertions(+), 41 deletions(-) diff --git a/src/braket/experimental/autoqasm/api.py b/src/braket/experimental/autoqasm/api.py index 9319866af..da39354c5 100644 --- a/src/braket/experimental/autoqasm/api.py +++ b/src/braket/experimental/autoqasm/api.py @@ -538,7 +538,7 @@ def _convert_gate( ) qubit_args = [args[i] for i in gate_args.qubit_indices] angle_args = [args[i] for i in gate_args.angle_indices] - aq_instructions.instructions._qubit_instruction(gate_name, qubit_args, *angle_args) + aq_instructions.instructions._qubit_instruction(gate_name, qubit_args, *angle_args, **kwargs) def _wrap_for_oqpy_gate( @@ -574,6 +574,9 @@ def _get_gate_args(f: Callable) -> aq_program.GateArgs: gate_args = aq_program.GateArgs() sig = inspect.signature(f) for param in sig.parameters.values(): + if param.kind == param.VAR_KEYWORD: + continue + if param.annotation is param.empty: raise errors.MissingParameterTypeError( f'Parameter "{param.name}" for gate "{f.__name__}" ' diff --git a/src/braket/experimental/autoqasm/instructions/gates.py b/src/braket/experimental/autoqasm/instructions/gates.py index b8b5e28fd..f7556c242 100644 --- a/src/braket/experimental/autoqasm/instructions/gates.py +++ b/src/braket/experimental/autoqasm/instructions/gates.py @@ -29,6 +29,7 @@ def ccnot( control_0: QubitIdentifierType, control_1: QubitIdentifierType, target: QubitIdentifierType, + **kwargs, ) -> None: """CCNOT gate or Toffoli gate. @@ -38,12 +39,13 @@ def ccnot( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("ccnot", [control_0, control_1, target]) + _qubit_instruction("ccnot", [control_0, control_1, target], **kwargs) def cnot( control: QubitIdentifierType, target: QubitIdentifierType, + **kwargs, ) -> None: """Controlled NOT gate. @@ -52,13 +54,14 @@ def cnot( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("cnot", [control, target]) + _qubit_instruction("cnot", [control, target], **kwargs) def cphaseshift( control: QubitIdentifierType, target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Controlled phase shift gate. @@ -68,13 +71,14 @@ def cphaseshift( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("cphaseshift", [control, target], angle) + _qubit_instruction("cphaseshift", [control, target], angle, **kwargs) def cphaseshift00( control: QubitIdentifierType, target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Controlled phase shift gate for phasing the \\|00> state. @@ -84,13 +88,14 @@ def cphaseshift00( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("cphaseshift00", [control, target], angle) + _qubit_instruction("cphaseshift00", [control, target], angle, **kwargs) def cphaseshift01( control: QubitIdentifierType, target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Controlled phase shift gate for phasing the \\|01> state. @@ -100,13 +105,14 @@ def cphaseshift01( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("cphaseshift01", [control, target], angle) + _qubit_instruction("cphaseshift01", [control, target], angle, **kwargs) def cphaseshift10( control: QubitIdentifierType, target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Controlled phase shift gate for phasing the \\|10> state. @@ -116,13 +122,14 @@ def cphaseshift10( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("cphaseshift10", [control, target], angle) + _qubit_instruction("cphaseshift10", [control, target], angle, **kwargs) def cswap( control: QubitIdentifierType, target_0: QubitIdentifierType, target_1: QubitIdentifierType, + **kwargs, ) -> None: """Controlled Swap gate. @@ -132,12 +139,13 @@ def cswap( target_1 (QubitIdentifierType): Target qubit 1. """ - _qubit_instruction("cswap", [control, target_0, target_1]) + _qubit_instruction("cswap", [control, target_0, target_1], **kwargs) def cv( control: QubitIdentifierType, target: QubitIdentifierType, + **kwargs, ) -> None: """Controlled Sqrt of NOT gate. @@ -146,12 +154,13 @@ def cv( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("cv", [control, target]) + _qubit_instruction("cv", [control, target], **kwargs) def cy( control: QubitIdentifierType, target: QubitIdentifierType, + **kwargs, ) -> None: """Controlled Pauli-Y gate. @@ -160,12 +169,13 @@ def cy( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("cy", [control, target]) + _qubit_instruction("cy", [control, target], **kwargs) def cz( control: QubitIdentifierType, target: QubitIdentifierType, + **kwargs, ) -> None: """Controlled Pauli-Z gate. @@ -174,12 +184,13 @@ def cz( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("cz", [control, target]) + _qubit_instruction("cz", [control, target], **kwargs) def ecr( target_0: QubitIdentifierType, target_1: QubitIdentifierType, + **kwargs, ) -> None: """An echoed RZX(pi/2) gate. @@ -188,12 +199,26 @@ def ecr( target_1 (QubitIdentifierType): Target qubit 1. """ - _qubit_instruction("ecr", [target_0, target_1]) + _qubit_instruction("ecr", [target_0, target_1], **kwargs) + + +def gphase( + angle: GateParameterType, + **kwargs, +) -> None: + """Global phase gate. + + Args: + angle (GateParameterType): Global phase in radians. + + """ + _qubit_instruction("gphase", [], angle, **kwargs) def gpi( target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """IonQ GPi gate. @@ -202,12 +227,13 @@ def gpi( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("gpi", [target], angle) + _qubit_instruction("gpi", [target], angle, **kwargs) def gpi2( target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """IonQ GPi2 gate. @@ -216,11 +242,12 @@ def gpi2( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("gpi2", [target], angle) + _qubit_instruction("gpi2", [target], angle, **kwargs) def h( target: QubitIdentifierType, + **kwargs, ) -> None: """Hadamard gate. @@ -228,11 +255,12 @@ def h( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("h", [target]) + _qubit_instruction("h", [target], **kwargs) def i( target: QubitIdentifierType, + **kwargs, ) -> None: """Identity gate. @@ -240,12 +268,13 @@ def i( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("i", [target]) + _qubit_instruction("i", [target], **kwargs) def iswap( target_0: QubitIdentifierType, target_1: QubitIdentifierType, + **kwargs, ) -> None: """ISwap gate. @@ -254,7 +283,7 @@ def iswap( target_1 (QubitIdentifierType): Target qubit 1. """ - _qubit_instruction("iswap", [target_0, target_1]) + _qubit_instruction("iswap", [target_0, target_1], **kwargs) def ms( @@ -263,6 +292,7 @@ def ms( angle_0: GateParameterType, angle_1: GateParameterType, angle_2: GateParameterType, + **kwargs, ) -> None: """IonQ Mølmer-Sørenson gate. @@ -274,12 +304,13 @@ def ms( angle_2 (GateParameterType): Rotation angle 2 in radians. """ - _qubit_instruction("ms", [target_0, target_1], angle_0, angle_1, angle_2) + _qubit_instruction("ms", [target_0, target_1], angle_0, angle_1, angle_2, **kwargs) def phaseshift( target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Phase shift gate. @@ -288,13 +319,31 @@ def phaseshift( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("phaseshift", [target], angle) + _qubit_instruction("phaseshift", [target], angle, **kwargs) + + +def prx( + target: QubitIdentifierType, + angle_0: GateParameterType, + angle_1: GateParameterType, + **kwargs, +) -> None: + """PhaseRx gate. + + Args: + target (QubitIdentifierType): Target qubit. + angle_0 (GateParameterType): First angle in radians. + angle_1 (GateParameterType): Second angle in radians. + + """ + _qubit_instruction("prx", [target], angle_0, angle_1, **kwargs) def pswap( target_0: QubitIdentifierType, target_1: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """PSwap gate. @@ -304,12 +353,13 @@ def pswap( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("pswap", [target_0, target_1], angle) + _qubit_instruction("pswap", [target_0, target_1], angle, **kwargs) def rx( target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """X-axis rotation gate. @@ -318,12 +368,13 @@ def rx( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("rx", [target], angle) + _qubit_instruction("rx", [target], angle, **kwargs) def ry( target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Y-axis rotation gate. @@ -332,12 +383,13 @@ def ry( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("ry", [target], angle) + _qubit_instruction("ry", [target], angle, **kwargs) def rz( target: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Z-axis rotation gate. @@ -346,11 +398,12 @@ def rz( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("rz", [target], angle) + _qubit_instruction("rz", [target], angle, **kwargs) def s( target: QubitIdentifierType, + **kwargs, ) -> None: """S gate. @@ -358,11 +411,12 @@ def s( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("s", [target]) + _qubit_instruction("s", [target], **kwargs) def si( target: QubitIdentifierType, + **kwargs, ) -> None: """Conjugate transpose of S gate. @@ -370,12 +424,13 @@ def si( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("si", [target]) + _qubit_instruction("si", [target], **kwargs) def swap( target_0: QubitIdentifierType, target_1: QubitIdentifierType, + **kwargs, ) -> None: """Swap gate. @@ -384,11 +439,12 @@ def swap( target_1 (QubitIdentifierType): Target qubit 1. """ - _qubit_instruction("swap", [target_0, target_1]) + _qubit_instruction("swap", [target_0, target_1], **kwargs) def t( target: QubitIdentifierType, + **kwargs, ) -> None: """T gate. @@ -396,11 +452,12 @@ def t( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("t", [target]) + _qubit_instruction("t", [target], **kwargs) def ti( target: QubitIdentifierType, + **kwargs, ) -> None: """Conjugate transpose of T gate. @@ -408,11 +465,31 @@ def ti( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("ti", [target]) + _qubit_instruction("ti", [target], **kwargs) + + +def u( + target: QubitIdentifierType, + angle_0: GateParameterType, + angle_1: GateParameterType, + angle_2: GateParameterType, + **kwargs, +) -> None: + """Generalized single-qubit rotation gate. + + Args: + target (QubitIdentifierType): Target qubit. + angle_0 (GateParameterType): Rotation angle theta in radians. + angle_1 (GateParameterType): Rotation angle phi in radians. + angle_2 (GateParameterType): Rotation angle lambda in radians. + + """ + _qubit_instruction("u", [target], angle_0, angle_1, angle_2, **kwargs) def v( target: QubitIdentifierType, + **kwargs, ) -> None: """Square root of not gate. @@ -420,11 +497,12 @@ def v( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("v", [target]) + _qubit_instruction("v", [target], **kwargs) def vi( target: QubitIdentifierType, + **kwargs, ) -> None: """Conjugate transpose of square root of not gate. @@ -432,11 +510,12 @@ def vi( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("vi", [target]) + _qubit_instruction("vi", [target], **kwargs) def x( target: QubitIdentifierType, + **kwargs, ) -> None: """Pauli-X gate. @@ -444,13 +523,14 @@ def x( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("x", [target]) + _qubit_instruction("x", [target], **kwargs) def xx( target_0: QubitIdentifierType, target_1: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Ising XX coupling gate. @@ -460,13 +540,14 @@ def xx( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("xx", [target_0, target_1], angle) + _qubit_instruction("xx", [target_0, target_1], angle, **kwargs) def xy( target_0: QubitIdentifierType, target_1: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """XY gates @@ -476,11 +557,12 @@ def xy( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("xy", [target_0, target_1], angle) + _qubit_instruction("xy", [target_0, target_1], angle, **kwargs) def y( target: QubitIdentifierType, + **kwargs, ) -> None: """Pauli-Y gate. @@ -488,13 +570,14 @@ def y( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("y", [target]) + _qubit_instruction("y", [target], **kwargs) def yy( target_0: QubitIdentifierType, target_1: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Ising YY coupling gate. @@ -504,11 +587,12 @@ def yy( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("yy", [target_0, target_1], angle) + _qubit_instruction("yy", [target_0, target_1], angle, **kwargs) def z( target: QubitIdentifierType, + **kwargs, ) -> None: """Pauli-Z gate. @@ -516,13 +600,14 @@ def z( target (QubitIdentifierType): Target qubit. """ - _qubit_instruction("z", [target]) + _qubit_instruction("z", [target], **kwargs) def zz( target_0: QubitIdentifierType, target_1: QubitIdentifierType, angle: GateParameterType, + **kwargs, ) -> None: """Ising ZZ coupling gate. @@ -532,4 +617,4 @@ def zz( angle (GateParameterType): Rotation angle in radians. """ - _qubit_instruction("zz", [target_0, target_1], angle) + _qubit_instruction("zz", [target_0, target_1], angle, **kwargs) diff --git a/src/braket/experimental/autoqasm/instructions/instructions.py b/src/braket/experimental/autoqasm/instructions/instructions.py index 63e3524d5..818465e91 100644 --- a/src/braket/experimental/autoqasm/instructions/instructions.py +++ b/src/braket/experimental/autoqasm/instructions/instructions.py @@ -14,16 +14,51 @@ """Non-unitary instructions that apply to qubits.""" +from __future__ import annotations + +from collections.abc import Iterable from typing import Any +import oqpy + +from braket.circuits.basis_state import BasisState, BasisStateInput from braket.experimental.autoqasm import program as aq_program +from braket.experimental.autoqasm import types as aq_types from braket.experimental.autoqasm.instructions.qubits import _qubit from braket.experimental.autoqasm.types import QubitIdentifierType def _qubit_instruction( - name: str, qubits: list[QubitIdentifierType], *args: Any, is_unitary: bool = True + name: str, + qubits: Iterable[QubitIdentifierType], + *args: Any, + is_unitary: bool = True, + control: QubitIdentifierType | Iterable[QubitIdentifierType] | None = None, + control_state: BasisStateInput | None = None, + power: float | None = None, ) -> None: + """Adds an instruction to the program which acts on a specified set of qubits. + + Args: + name (str): The name of the instruction. + qubits (Iterable[QubitIdentifierType]): The qubits on which the instruction acts. + is_unitary (bool): Whether the instruction represents a unitary operation. Defaults to True. + control (QubitIdentifierType | Iterable[QubitIdentifierType] | None): The qubit or + list of qubits which are being used as the control qubits. If None, an empty list is + used, and the gate will not be controlled on any qubits. Defaults to None. + control_state (BasisStateInput | None): The basis state of the control qubits + that is required in order for the controlled gate to be performed. The order of the + bits in this basis state corresponds to the order of the qubits provided in `control`. + If None, the control state is assumed to be a bitstring of all 1s. Defaults to None. + power (float | None): The power to which the gate should be raised. If None, the gate + will not be raised to any power. Defaults to None. + + Note: + The params `control`, `control_state`, and `power` are frequently passed as kwargs + to this function from built-in gates defined in the `gates` module. This allows the + signatures and docstrings for each built-in gate to be more concise, though at the cost + of not having these gate modifier params explicitly documented for each gate. + """ program_conversion_context = aq_program.get_program_conversion_context() program_conversion_context.validate_gate_targets(qubits, args) @@ -31,8 +66,54 @@ def _qubit_instruction( program_conversion_context.register_gate(name) program_conversion_context.register_args(args) program_mode = aq_program.ProgramMode.UNITARY if is_unitary else aq_program.ProgramMode.NONE + pos_control, neg_control = _get_pos_neg_control(control, control_state) oqpy_program = program_conversion_context.get_oqpy_program(mode=program_mode) - oqpy_program.gate([_qubit(q) for q in qubits], name, *args) + oqpy_program.gate( + [_qubit(q) for q in qubits], + name, + *args, + control=pos_control, + neg_control=neg_control, + exp=power, + ) + + +def _get_pos_neg_control( + control: QubitIdentifierType | Iterable[QubitIdentifierType] | None = None, + control_state: BasisStateInput | None = None, +) -> tuple[list[oqpy.Qubit], list[oqpy.Qubit]]: + """Constructs the list of positive-control and negative-control qubits given + the list of control qubits and the corresponding control state. For the controlled gate + to be performed, all of the positive-control qubits must be in the 1 state, and all of the + negative-control qubits must be in the 0 state. + + Args: + control (QubitIdentifierType | Iterable[QubitIdentifierType] | None): The qubit + or list of qubits which are being used as the control qubits. If None, an empty list is + used. Defaults to None. + control_state (BasisStateInput | None): The basis state of the control qubits + that is required in order for the controlled gate to be performed. The order of the + bits in this basis state corresponds to the order of the qubits provided in `control`. + If None, the control state is assumed to be a bitstring of all 1s. Defaults to None. + + Returns: + tuple[list[Qubit], list[Qubit]]: A tuple of lists of `Qubit` objects, where the first list + contains the positive-control qubits, and the second list contains the negative-control + qubits. The union of the two lists is the same as the list of control qubits. + """ + if control is None: + control = [] + elif aq_types.is_qubit_identifier_type(control): + control = [control] + + if control_state is None: + control_state = [1] * len(control) + else: + control_state = BasisState(control_state, len(control)).as_tuple + + pos_control = [_qubit(q) for i, q in enumerate(control) if control_state[i] == 1] + neg_control = [_qubit(q) for i, q in enumerate(control) if control_state[i] == 0] + return pos_control, neg_control def reset(target: QubitIdentifierType) -> None: diff --git a/src/braket/experimental/autoqasm/instructions/measurements.py b/src/braket/experimental/autoqasm/instructions/measurements.py index 8e9d77bcd..0bd33bcef 100644 --- a/src/braket/experimental/autoqasm/instructions/measurements.py +++ b/src/braket/experimental/autoqasm/instructions/measurements.py @@ -28,7 +28,11 @@ def my_program(): from braket.experimental.autoqasm import program from braket.experimental.autoqasm import types as aq_types -from braket.experimental.autoqasm.instructions.qubits import _qubit, global_qubit_register +from braket.experimental.autoqasm.instructions.qubits import ( + GlobalQubitRegister, + _qubit, + global_qubit_register, +) def measure( @@ -47,7 +51,7 @@ def measure( if qubits is None: qubits = global_qubit_register() - if isinstance(qubits, str) or not isinstance(qubits, Iterable): + if aq_types.is_qubit_identifier_type(qubits) and not isinstance(qubits, GlobalQubitRegister): qubits = [qubits] oqpy_program = program.get_program_conversion_context().get_oqpy_program() diff --git a/test/unit_tests/braket/experimental/autoqasm/test_gate_decorator.py b/test/unit_tests/braket/experimental/autoqasm/test_gate_decorator.py index 67aa97db9..2491d5437 100644 --- a/test/unit_tests/braket/experimental/autoqasm/test_gate_decorator.py +++ b/test/unit_tests/braket/experimental/autoqasm/test_gate_decorator.py @@ -358,3 +358,24 @@ def subroutine(int[32] q0, int[32] q1) { subroutine(2, 3);""" assert main.build().to_ir() == expected + + +def test_gate_modifiers() -> None: + @aq.gate + def t(q: aq.Qubit): + rz(q, np.pi / 4) + + @aq.main(num_qubits=4) + def main(): + t(1, control=0) + t(2, control=[0, 1], control_state="10", power=0.123) + + expected = """OPENQASM 3.0; +gate t q { + rz(0.7853981633974483) q; +} +qubit[4] __qubits__; +ctrl @ t __qubits__[0], __qubits__[1]; +ctrl @ negctrl @ pow(0.123) @ t __qubits__[0], __qubits__[1], __qubits__[2];""" + + assert main.build().to_ir() == expected diff --git a/test/unit_tests/braket/experimental/autoqasm/test_gate_definitions.py b/test/unit_tests/braket/experimental/autoqasm/test_gate_definitions.py index 560b3ea32..008d14b83 100644 --- a/test/unit_tests/braket/experimental/autoqasm/test_gate_definitions.py +++ b/test/unit_tests/braket/experimental/autoqasm/test_gate_definitions.py @@ -28,6 +28,7 @@ cy, cz, ecr, + gphase, gpi, gpi2, h, @@ -36,6 +37,7 @@ measure, ms, phaseshift, + prx, pswap, reset, rx, @@ -46,6 +48,7 @@ swap, t, ti, + u, v, vi, x, @@ -109,6 +112,7 @@ def test_bell_with_measure() -> None: (cy, [0, 1], [], "\ncy __qubits__[0], __qubits__[1];"), (cz, [0, 1], [], "\ncz __qubits__[0], __qubits__[1];"), (ecr, [0, 1], [], "\necr __qubits__[0], __qubits__[1];"), + (gphase, [], [0.1], "\ngphase(0.1) ;"), (gpi, [0], [0.1], "\ngpi(0.1) __qubits__[0];"), (gpi2, [0], [0.1], "\ngpi2(0.1) __qubits__[0];"), (h, [0], [], "\nh __qubits__[0];"), @@ -116,6 +120,7 @@ def test_bell_with_measure() -> None: (iswap, [0, 1], [], "\niswap __qubits__[0], __qubits__[1];"), (ms, [0, 1], [0.1, 0.2, 0.3], "\nms(0.1, 0.2, 0.3) __qubits__[0], __qubits__[1];"), (phaseshift, [0], [0.1], "\nphaseshift(0.1) __qubits__[0];"), + (prx, [0], [0.1, 0.2], "\nprx(0.1, 0.2) __qubits__[0];"), (pswap, [0, 1], [0.1], "\npswap(0.1) __qubits__[0], __qubits__[1];"), (rx, [0], [0.1], "\nrx(0.1) __qubits__[0];"), (ry, [0], [0.1], "\nry(0.1) __qubits__[0];"), @@ -125,6 +130,7 @@ def test_bell_with_measure() -> None: (swap, [0, 1], [], "\nswap __qubits__[0], __qubits__[1];"), (t, [0], [], "\nt __qubits__[0];"), (ti, [0], [], "\nti __qubits__[0];"), + (u, [0], [0.1, 0.2, 0.3], "\nu(0.1, 0.2, 0.3) __qubits__[0];"), (v, [0], [], "\nv __qubits__[0];"), (vi, [0], [], "\nvi __qubits__[0];"), (x, [0], [], "\nx __qubits__[0];"), @@ -142,3 +148,72 @@ def test_gates(gate, qubits, params, expected_qasm) -> None: gate(*qubits, *params) assert expected_qasm in program_conversion_context.make_program().to_ir() + + +@pytest.mark.parametrize( + "control,control_state,expected_qasm", + [ + (0, None, "\nctrl @ x __qubits__[0], __qubits__[1];"), + ([0], None, "\nctrl @ x __qubits__[0], __qubits__[1];"), + (0, "1", "\nctrl @ x __qubits__[0], __qubits__[1];"), + ([0], "0", "\nnegctrl @ x __qubits__[0], __qubits__[1];"), + ([0], 0, "\nnegctrl @ x __qubits__[0], __qubits__[1];"), + ([0], [0], "\nnegctrl @ x __qubits__[0], __qubits__[1];"), + ], +) +def test_gate_modifiers_single_control(control, control_state, expected_qasm) -> None: + """Tests quantum gate modifiers to create a singly-controlled X gate.""" + with aq.build_program() as program_conversion_context: + x(1, control=control, control_state=control_state) + + assert expected_qasm in program_conversion_context.make_program().to_ir() + + +@pytest.mark.parametrize( + "control,control_state,expected_qasm", + [ + ([0, 1], "11", "\nctrl(2) @ x __qubits__[0], __qubits__[1], __qubits__[2];"), + ([0, 1], [1, 1], "\nctrl(2) @ x __qubits__[0], __qubits__[1], __qubits__[2];"), + ([0, 1], 3, "\nctrl(2) @ x __qubits__[0], __qubits__[1], __qubits__[2];"), + ([0, 1], "10", "\nctrl @ negctrl @ x __qubits__[0], __qubits__[1], __qubits__[2];"), + ], +) +def test_gate_modifiers_multi_control(control, control_state, expected_qasm) -> None: + """Tests quantum gate modifiers to create a multiply-controlled X gate.""" + with aq.build_program() as program_conversion_context: + x(2, control=control, control_state=control_state) + + assert expected_qasm in program_conversion_context.make_program().to_ir() + + +@pytest.mark.parametrize( + "control,control_state,power,expected_qasm", + [ + (None, None, -2.0, "\npow(-2.0) @ x __qubits__[1];"), + ([0], "1", 0.5, "\nctrl @ pow(0.5) @ x __qubits__[0], __qubits__[1];"), + ], +) +def test_gate_modifiers_power(control, control_state, power, expected_qasm) -> None: + """Tests quantum gate modifiers to create gates raised to powers.""" + with aq.build_program() as program_conversion_context: + x(1, control=control, control_state=control_state, power=power) + + assert expected_qasm in program_conversion_context.make_program().to_ir() + + +def test_gate_modifiers_physical_qubits() -> None: + with aq.build_program() as program_conversion_context: + x("$1", control="$0") + + assert "\nctrl @ x $0, $1;" in program_conversion_context.make_program().to_ir() + + +def test_invalid_gate_modifiers() -> None: + """Tests invalid quantum gate modifiers.""" + with aq.build_program() as _: + with pytest.raises(ValueError, match="length greater than the specified number of qubits"): + x(1, control=None, control_state="00") + with pytest.raises(ValueError, match="length greater than the specified number of qubits"): + x(1, control=0, control_state="00") + with pytest.raises(ValueError, match="length greater than the specified number of qubits"): + x(1, control=0, control_state=3)