diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 16ea707920d..9a33628a92d 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -165,9 +165,17 @@ and the codecov check itself would never execute. [(#5101)](https://github.com/PennyLaneAI/pennylane/pull/5101) +* `qml.ctrl` called on operators with custom controlled versions will return instances + of the custom class, and it will also flatten nested controlled operators to a single + multi-controlled operation. For `PauliX`, `CNOT`, `Toffoli`, and `MultiControlledX`, + calling `qml.ctrl` will always resolve to the best option in `CNOT`, `Toffoli`, or + `MultiControlledX` depending on the number of control wires and control values. + [(#5125)](https://github.com/PennyLaneAI/pennylane/pull/5125/) + * `qml.Identity()` can be initialized without wires. Measuring it is currently not possible though. [(#5106)](https://github.com/PennyLaneAI/pennylane/pull/5106) +

Community contributions 🥳

* The transform `split_non_commuting` now accepts measurements of type `probs`, `sample` and `counts` which accept both wires and observables. @@ -203,7 +211,12 @@ (with potentially negative eigenvalues) has been implemented. [(#5048)](https://github.com/PennyLaneAI/pennylane/pull/5048) -* The decomposition of an operator created with calling `qml.ctrl` on a parametric operator (specifically `RX`, `RY`, `RZ`, `Rot`, `PhaseShift`) with a single control wire will now be the full decomposition instead of a single controlled gate. For example: +* Controlled operators with a custom controlled version decomposes like how their + controlled counterpart decomposes, as opposed to decomposing into their controlled version. + [(#5069)](https://github.com/PennyLaneAI/pennylane/pull/5069) + [(#5125)](https://github.com/PennyLaneAI/pennylane/pull/5125/) + + For example: ``` >>> qml.ctrl(qml.RX(0.123, wires=1), control=0).decomposition() [ @@ -215,7 +228,6 @@ RZ(-1.5707963267948966, wires=[1]) ] ``` - [(#5069)](https://github.com/PennyLaneAI/pennylane/pull/5069) * `QuantumScript.is_sampled` and `QuantumScript.all_sampled` have been removed. Users should now validate these properties manually. diff --git a/pennylane/devices/qubit/apply_operation.py b/pennylane/devices/qubit/apply_operation.py index cd6e4949e86..8f7c9a7a122 100644 --- a/pennylane/devices/qubit/apply_operation.py +++ b/pennylane/devices/qubit/apply_operation.py @@ -274,9 +274,7 @@ def apply_multicontrolledx( return _apply_operation_default(op, state, is_state_batched, debugger) ctrl_wires = [w + is_state_batched for w in op.control_wires] # apply x on all control wires with control value 0 - roll_axes = [ - w for val, w in zip(op.hyperparameters["control_values"], ctrl_wires) if val == "0" - ] + roll_axes = [w for val, w in zip(op.control_values, ctrl_wires) if val is False] for ax in roll_axes: state = math.roll(state, 1, ax) diff --git a/pennylane/drawer/tape_mpl.py b/pennylane/drawer/tape_mpl.py index ce8200a4edf..97464edad85 100644 --- a/pennylane/drawer/tape_mpl.py +++ b/pennylane/drawer/tape_mpl.py @@ -107,8 +107,7 @@ def _(op: ops.Toffoli, drawer, layer, _): @_add_operation_to_drawer.register def _(op: ops.MultiControlledX, drawer, layer, _): - control_values = [(i == "1") for i in op.hyperparameters["control_values"]] - drawer.CNOT(layer, op.wires, control_values=control_values) + drawer.CNOT(layer, op.active_wires, control_values=op.control_values) @_add_operation_to_drawer.register diff --git a/pennylane/ops/functions/bind_new_parameters.py b/pennylane/ops/functions/bind_new_parameters.py index 96b93b29c7d..dd8bff6f55d 100644 --- a/pennylane/ops/functions/bind_new_parameters.py +++ b/pennylane/ops/functions/bind_new_parameters.py @@ -113,8 +113,9 @@ def bind_new_parameters_composite_op(op: CompositeOp, params: Sequence[TensorLik @bind_new_parameters.register(qml.CY) @bind_new_parameters.register(qml.CZ) +@bind_new_parameters.register(qml.MultiControlledX) def bind_new_parameters_copy( - op: Union[qml.CY, qml.CZ], params: Sequence[TensorLike] + op: Union[qml.CY, qml.CZ, qml.MultiControlledX], params: Sequence[TensorLike] ): # pylint:disable=unused-argument return copy.copy(op) diff --git a/pennylane/ops/op_math/__init__.py b/pennylane/ops/op_math/__init__.py index e15ab1cf6c9..20089d30df7 100644 --- a/pennylane/ops/op_math/__init__.py +++ b/pennylane/ops/op_math/__init__.py @@ -109,6 +109,7 @@ CRZ, CY, CZ, + MultiControlledX, ) from .decompositions import one_qubit_decomposition, two_qubit_decomposition, sk_decomposition from .evolution import Evolution @@ -124,6 +125,7 @@ "ControlledQubitUnitary", "CY", "CZ", + "MultiControlledX", "CRX", "CRY", "CRZ", diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 37bd06f96e2..b0ba87b20b6 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -15,6 +15,7 @@ This submodule defines the symbolic operation that indicates the control of an operator. """ import warnings +import functools from copy import copy from functools import wraps from inspect import signature @@ -127,36 +128,41 @@ def cond_fn(): ops_loader = available_eps[active_jit]["ops"].load() return ops_loader.ctrl(op, control, control_values=control_values, work_wires=work_wires) - custom_ops = { - (qml.PauliZ, 1): qml.CZ, - (qml.PauliY, 1): qml.CY, - (qml.PauliX, 1): qml.CNOT, - (qml.PauliX, 2): qml.Toffoli, - (qml.RX, 1): qml.CRX, - (qml.RY, 1): qml.CRY, - (qml.RZ, 1): qml.CRZ, - (qml.Rot, 1): qml.CRot, - (qml.PhaseShift, 1): qml.ControlledPhaseShift, - } - control_values = [control_values] if isinstance(control_values, (int, bool)) else control_values control = qml.wires.Wires(control) - custom_key = (type(op), len(control)) + if isinstance(control_values, (int, bool)): + control_values = [control_values] + elif control_values is None: + control_values = [True] * len(control) - if custom_key in custom_ops and (control_values is None or all(control_values)): - qml.QueuingManager.remove(op) - return custom_ops[custom_key](*op.data, control + op.wires) - if isinstance(op, qml.PauliX): + ctrl_op = _try_wrap_in_custom_ctrl_op( + op, control, control_values=control_values, work_wires=work_wires + ) + if ctrl_op is not None: + return ctrl_op + + pauli_x_based_ctrl_ops = _get_pauli_x_based_ops() + + # Special handling for PauliX-based controlled operations + if isinstance(op, pauli_x_based_ctrl_ops): qml.QueuingManager.remove(op) - control_string = ( - None if control_values is None else "".join([str(int(v)) for v in control_values]) - ) - return qml.MultiControlledX( - wires=control + op.wires, control_values=control_string, work_wires=work_wires + return _handle_pauli_x_based_controlled_ops(op, control, control_values, work_wires) + + # Flatten nested controlled operations to a multi-controlled operation for better + # decomposition algorithms. This includes special cases like CRX, CRot, etc. + if isinstance(op, Controlled): + work_wires = work_wires or [] + return ctrl( + op.base, + control=control + op.control_wires, + control_values=control_values + op.control_values, + work_wires=work_wires + op.work_wires, ) + if isinstance(op, Operator): return Controlled( op, control_wires=control, control_values=control_values, work_wires=work_wires ) + if not callable(op): raise ValueError( f"The object {op} of type {type(op)} is not an Operator or callable. " @@ -190,6 +196,115 @@ def wrapper(*args, **kwargs): return wrapper +@functools.lru_cache() +def _get_special_ops(): + """Gets a list of special operations with custom controlled versions. + + This is placed inside a function to avoid circular imports. + + """ + + ops_with_custom_ctrl_ops = { + (qml.PauliZ, 1): qml.CZ, + (qml.PauliZ, 2): qml.CCZ, + (qml.PauliY, 1): qml.CY, + (qml.CZ, 1): qml.CCZ, + (qml.SWAP, 1): qml.CSWAP, + (qml.Hadamard, 1): qml.CH, + (qml.RX, 1): qml.CRX, + (qml.RY, 1): qml.CRY, + (qml.RZ, 1): qml.CRZ, + (qml.Rot, 1): qml.CRot, + (qml.PhaseShift, 1): qml.ControlledPhaseShift, + } + return ops_with_custom_ctrl_ops + + +@functools.lru_cache() +def _get_pauli_x_based_ops(): + """Gets a list of pauli-x based operations + + This is placed inside a function to avoid circular imports. + + """ + return qml.PauliX, qml.CNOT, qml.Toffoli, qml.MultiControlledX + + +def _try_wrap_in_custom_ctrl_op(op, control, control_values=None, work_wires=None): + """Wraps a controlled operation in custom ControlledOp, returns None if not applicable.""" + + ops_with_custom_ctrl_ops = _get_special_ops() + custom_key = (type(op), len(control)) + + if custom_key in ops_with_custom_ctrl_ops and all(control_values): + qml.QueuingManager.remove(op) + return ops_with_custom_ctrl_ops[custom_key](*op.data, control + op.wires) + + if isinstance(op, qml.QubitUnitary): + return qml.ControlledQubitUnitary( + op, control_wires=control, control_values=control_values, work_wires=work_wires + ) + + # A controlled ControlledPhaseShift should not be compressed to a multi controlled + # PhaseShift because the decomposition of PhaseShift contains a GlobalPhase that we + # do not have a controlled version of. + # TODO: remove this special case when we support ControlledGlobalPhase (sc-44933) + if isinstance(op, qml.ControlledPhaseShift): + return Controlled( + op, control_wires=control, control_values=control_values, work_wires=work_wires + ) + # Similarly, compress the bottom levels of a multi-controlled PhaseShift to a + # ControlledPhaseShift if possible to avoid dealing with a controlled GlobalPhase + # during decomposition. This should also be removed in the future. + if isinstance(op, qml.PhaseShift) and control_values[-1]: + op = qml.ControlledPhaseShift(*op.data, wires=control[-1:] + op.wires) + return Controlled( + op, + control_wires=control[:-1], + control_values=control_values[:-1], + work_wires=work_wires, + ) + + return None + + +def _handle_pauli_x_based_controlled_ops(op, control, control_values, work_wires): + """Handles PauliX-based controlled operations.""" + + op_map = { + (qml.PauliX, 1): qml.CNOT, + (qml.PauliX, 2): qml.Toffoli, + (qml.CNOT, 1): qml.Toffoli, + } + + custom_key = (type(op), len(control)) + if custom_key in op_map and all(control_values): + qml.QueuingManager.remove(op) + return op_map[custom_key](wires=control + op.wires) + + if isinstance(op, qml.PauliX): + return qml.MultiControlledX( + wires=control + op.wires, control_values=control_values, work_wires=work_wires + ) + + # TODO: remove special handling of CNOT and Toffoli when they inherit from Controlled + if isinstance(op, qml.CNOT): + return qml.MultiControlledX( + wires=control + op.wires, control_values=control_values + [1], work_wires=work_wires + ) + if isinstance(op, qml.Toffoli): + return qml.MultiControlledX( + wires=control + op.wires, control_values=control_values + [1, 1], work_wires=work_wires + ) + + work_wires = work_wires or [] + return qml.MultiControlledX( + wires=control + op.wires, + control_values=control_values + op.control_values, + work_wires=work_wires + op.work_wires, + ) + + # pylint: disable=too-many-arguments, too-many-public-methods class Controlled(SymbolicOp): """Symbolic operator denoting a controlled operator. @@ -531,7 +646,7 @@ def has_decomposition(self): return True if len(self.control_wires) == 1 and hasattr(self.base, "_controlled"): return True - if isinstance(self.base, qml.PauliX): + if isinstance(self.base, _get_pauli_x_based_ops()): return True if _is_single_qubit_special_unitary(self.base): return True @@ -631,21 +746,57 @@ def _is_single_qubit_special_unitary(op): return qmlmath.allclose(det, 1) -# pylint: disable=protected-access -def _decompose_no_control_values(op: "operation.Operator") -> List["operation.Operator"]: - """Provides a decomposition without considering control values. Returns None if - no decomposition. - """ +def _decompose_pauli_x_based_no_control_values(op: Controlled): + """Decomposes a PauliX-based operation""" + + if isinstance(op.base, qml.PauliX) and len(op.control_wires) == 1: + return [qml.CNOT(wires=op.active_wires)] + + if isinstance(op.base, qml.PauliX) and len(op.control_wires) == 2: + return qml.Toffoli.compute_decomposition(wires=op.active_wires) + + if isinstance(op.base, qml.CNOT) and len(op.control_wires) == 1: + return qml.Toffoli.compute_decomposition(wires=op.active_wires) + + return qml.MultiControlledX.compute_decomposition( + wires=op.active_wires, + work_wires=op.work_wires, + ) + + +def _decompose_custom_ops(op: Controlled) -> List["operation.Operator"]: + """Custom handling for decomposing a controlled operation""" + + pauli_x_based_ctrl_ops = _get_pauli_x_based_ops() + ops_with_custom_ctrl_ops = _get_special_ops() + + custom_key = (type(op.base), len(op.control_wires)) + if custom_key in ops_with_custom_ctrl_ops: + custom_op_cls = ops_with_custom_ctrl_ops[custom_key] + return custom_op_cls.compute_decomposition(*op.data, op.active_wires) + if type(op.base) in pauli_x_based_ctrl_ops: + # has some special case handling of its own for further decomposition + return _decompose_pauli_x_based_no_control_values(op) + + # TODO: will be removed in the second part of the controlled rework if len(op.control_wires) == 1 and hasattr(op.base, "_controlled"): - result = op.base._controlled(op.control_wires[0]) + result = op.base._controlled(op.control_wires[0]) # pylint: disable=protected-access # disallow decomposing to itself # pylint: disable=unidiomatic-typecheck if type(result) != type(op): return [result] qml.QueuingManager.remove(result) - if isinstance(op.base, qml.PauliX): - # has some special case handling of its own for further decomposition - return [qml.MultiControlledX(wires=op.active_wires, work_wires=op.work_wires)] + + return None + + +def _decompose_no_control_values(op: Controlled) -> List["operation.Operator"]: + """Decompose without considering control values. Returns None if no decomposition.""" + + decomp = _decompose_custom_ops(op) + if decomp is not None: + return decomp + if _is_single_qubit_special_unitary(op.base): if len(op.control_wires) >= 2 and qmlmath.get_interface(*op.data) == "numpy": return ctrl_decomp_bisect(op.base, op.control_wires) @@ -663,7 +814,7 @@ def _decompose_no_control_values(op: "operation.Operator") -> List["operation.Op UserWarning, ) - return [Controlled(newop, op.control_wires, work_wires=op.work_wires) for newop in base_decomp] + return [ctrl(newop, op.control_wires, work_wires=op.work_wires) for newop in base_decomp] class ControlledOp(Controlled, operation.Operation): diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index bbf14575a57..b66a71386dd 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -199,15 +199,15 @@ def decomp_circuit(op): decomp.extend( [ qml.RY(theta / 2, wires=target_wire), - qml.MultiControlledX(wires=control_wires + target_wire), + qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires), qml.RY(-theta / 2, wires=target_wire), ] ) else: - decomp.append(qml.MultiControlledX(wires=control_wires + target_wire)) + decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) if not qml.math.isclose(-(phi + omega) / 2, 0.0, atol=1e-6, rtol=0): decomp.append(qml.RZ(-(phi + omega) / 2, wires=target_wire)) - decomp.append(qml.MultiControlledX(wires=control_wires + target_wire)) + decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) if not qml.math.isclose((omega - phi) / 2, 0.0, atol=1e-8, rtol=0): decomp.append(qml.RZ((omega - phi) / 2, wires=target_wire)) @@ -256,9 +256,9 @@ def _ctrl_decomp_bisect_od( def component(): return [ - qml.MultiControlledX(wires=control_k1 + target_wire, work_wires=control_k2), + qml.ctrl(qml.PauliX(wires=target_wire), control=control_k1, work_wires=control_k2), qml.QubitUnitary(a, target_wire), - qml.MultiControlledX(wires=control_k2 + target_wire, work_wires=control_k1), + qml.ctrl(qml.PauliX(wires=target_wire), control=control_k2, work_wires=control_k1), qml.adjoint(qml.QubitUnitary(a, target_wire)), ] @@ -350,9 +350,9 @@ def _ctrl_decomp_bisect_general( component = [ qml.QubitUnitary(c2t, target_wire), - qml.MultiControlledX(wires=control_k2 + target_wire, work_wires=control_k1), + qml.ctrl(qml.PauliX(wires=target_wire), control=control_k2, work_wires=control_k1), qml.adjoint(qml.QubitUnitary(c1, target_wire)), - qml.MultiControlledX(wires=control_k1 + target_wire, work_wires=control_k2), + qml.ctrl(qml.PauliX(wires=target_wire), control=control_k1, work_wires=control_k2), ] od_decomp = _ctrl_decomp_bisect_od(d, target_wire, control_wires) @@ -439,3 +439,76 @@ def ctrl_decomp_bisect( return _ctrl_decomp_bisect_md(target_matrix, target_wire, control_wires) # General algorithm - 20n+O(1) CNOTs return _ctrl_decomp_bisect_general(target_matrix, target_wire, control_wires) + + +def decompose_mcx(control_wires, target_wire, work_wires): + """Decomposes the multi-controlled PauliX gate""" + + num_work_wires_needed = len(control_wires) - 2 + + if len(work_wires) >= num_work_wires_needed: + return _decompose_mcx_with_many_workers(control_wires, target_wire, work_wires) + + return _decompose_mcx_with_one_worker(control_wires, target_wire, work_wires[0]) + + +def _decompose_mcx_with_many_workers(control_wires, target_wire, work_wires): + """Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.2 of + https://arxiv.org/abs/quant-ph/9503016, which requires a suitably large register of + work wires""" + num_work_wires_needed = len(control_wires) - 2 + work_wires = work_wires[:num_work_wires_needed] + + work_wires_reversed = list(reversed(work_wires)) + control_wires_reversed = list(reversed(control_wires)) + + gates = [] + + for i in range(len(work_wires)): + ctrl1 = control_wires_reversed[i] + ctrl2 = work_wires_reversed[i] + t = target_wire if i == 0 else work_wires_reversed[i - 1] + gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) + + gates.append(qml.Toffoli(wires=[*control_wires[:2], work_wires[0]])) + + for i in reversed(range(len(work_wires))): + ctrl1 = control_wires_reversed[i] + ctrl2 = work_wires_reversed[i] + t = target_wire if i == 0 else work_wires_reversed[i - 1] + gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) + + for i in range(len(work_wires) - 1): + ctrl1 = control_wires_reversed[i + 1] + ctrl2 = work_wires_reversed[i + 1] + t = work_wires_reversed[i] + gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) + + gates.append(qml.Toffoli(wires=[*control_wires[:2], work_wires[0]])) + + for i in reversed(range(len(work_wires) - 1)): + ctrl1 = control_wires_reversed[i + 1] + ctrl2 = work_wires_reversed[i + 1] + t = work_wires_reversed[i] + gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) + + return gates + + +def _decompose_mcx_with_one_worker(control_wires, target_wire, work_wire): + """Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.3 of + https://arxiv.org/abs/quant-ph/9503016, which requires a single work wire""" + tot_wires = len(control_wires) + 2 + partition = int(np.ceil(tot_wires / 2)) + + first_part = control_wires[:partition] + second_part = control_wires[partition:] + + gates = [ + qml.ctrl(qml.PauliX(work_wire), control=first_part, work_wires=second_part + target_wire), + qml.ctrl(qml.PauliX(target_wire), control=second_part + work_wire, work_wires=first_part), + qml.ctrl(qml.PauliX(work_wire), control=first_part, work_wires=second_part + target_wire), + qml.ctrl(qml.PauliX(target_wire), control=second_part + work_wire, work_wires=first_part), + ] + + return gates diff --git a/pennylane/ops/op_math/controlled_ops.py b/pennylane/ops/op_math/controlled_ops.py index 31ab1c6076e..5601351bb61 100644 --- a/pennylane/ops/op_math/controlled_ops.py +++ b/pennylane/ops/op_math/controlled_ops.py @@ -20,12 +20,17 @@ from functools import lru_cache import numpy as np +from scipy.linalg import block_diag import pennylane as qml -from pennylane.operation import AnyWires +from pennylane.operation import AnyWires, Wires from pennylane.ops.qubit.matrix_ops import QubitUnitary from pennylane.ops.qubit.parametric_ops_single_qubit import stack_last from .controlled import ControlledOp +from .controlled_decompositions import decompose_mcx + + +INV_SQRT2 = 1 / qml.math.sqrt(2) # pylint: disable=too-few-public-methods @@ -141,8 +146,8 @@ def __init__( self._name = "ControlledQubitUnitary" def _controlled(self, wire): - ctrl_wires = self.control_wires + wire - values = None if self.control_values is None else self.control_values + [True] + ctrl_wires = wire + self.control_wires + values = None if self.control_values is None else [True] + self.control_values return ControlledQubitUnitary( self.base, control_wires=ctrl_wires, @@ -339,6 +344,242 @@ def compute_matrix(): # pylint: disable=arguments-differ def _controlled(self, wire): return qml.CCZ(wires=wire + self.wires) + @staticmethod + def compute_decomposition(wires): # pylint: disable=arguments-differ + return [qml.ControlledPhaseShift(np.pi, wires=wires)] + + def decomposition(self): + return self.compute_decomposition(self.wires) + + +def _check_and_convert_control_values(control_values, control_wires): + + if isinstance(control_values, str): + # Make sure all values are either 0 or 1 + if not set(control_values).issubset({"1", "0"}): + raise ValueError("String of control values can contain only '0' or '1'.") + + control_values = [int(x) for x in control_values] + + if control_values is None: + return [1] * len(control_wires) + + if len(control_values) != len(control_wires): + raise ValueError("Length of control values must equal number of control wires.") + + return control_values + + +class MultiControlledX(ControlledOp): + r"""MultiControlledX(control_wires, wires, control_values) + Apply a Pauli X gate controlled on an arbitrary computational basis state. + + **Details:** + + * Number of wires: Any (the operation can act on any number of wires) + * Number of parameters: 0 + * Gradient recipe: None + + Args: + control_wires (Union[Wires, Sequence[int], or int]): Deprecated way to indicate the control wires. + Now users should use "wires" to indicate both the control wires and the target wire. + wires (Union[Wires, Sequence[int], or int]): control wire(s) followed by a single target wire where + the operation acts on + control_values (Union[bool, list[bool], int, list[int]]): The value(s) the control wire(s) + should take. Integers other than 0 or 1 will be treated as ``int(bool(x))``. + work_wires (Union[Wires, Sequence[int], or int]): optional work wires used to decompose + the operation into a series of Toffoli gates + + + .. note:: + + If ``MultiControlledX`` is not supported on the targeted device, PennyLane will decompose + the operation into :class:`~.Toffoli` and/or :class:`~.CNOT` gates. When controlling on + three or more wires, the Toffoli-based decompositions described in Lemmas 7.2 and 7.3 of + `Barenco et al. `__ will be used. These methods + require at least one work wire. + + The number of work wires provided determines the decomposition method used and the resulting + number of Toffoli gates required. When ``MultiControlledX`` is controlling on :math:`n` + wires: + + #. If at least :math:`n - 2` work wires are provided, the decomposition in Lemma 7.2 will be + applied using the first :math:`n - 2` work wires. + #. If fewer than :math:`n - 2` work wires are provided, a combination of Lemmas 7.3 and 7.2 + will be applied using only the first work wire. + + These methods present a tradeoff between qubit number and depth. The method in point 1 + requires fewer Toffoli gates but a greater number of qubits. + + Note that the state of the work wires before and after the decomposition takes place is + unchanged. + + """ + + is_self_inverse = True + """bool: Whether or not the operator is self-inverse.""" + + num_wires = AnyWires + """int: Number of wires the operation acts on.""" + + num_params = 0 + """int: Number of trainable parameters that the operator depends on.""" + + ndim_params = () + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + + name = "MultiControlledX" + + def _flatten(self): + return (), (self.active_wires, tuple(self.control_values), self.work_wires) + + @classmethod + def _unflatten(cls, _, metadata): + return cls(wires=metadata[0], control_values=metadata[1], work_wires=metadata[2]) + + # pylint: disable=too-many-arguments + def __init__(self, control_wires=None, wires=None, control_values=None, work_wires=None): + if wires is None: + raise ValueError("Must specify the wires where the operation acts on") + wires = wires if isinstance(wires, Wires) else Wires(wires) + if control_wires is not None: + warnings.warn( + "The control_wires keyword will be removed soon. Use wires = (control_wires, " + "target_wire) instead. See the documentation for more information.", + UserWarning, + ) + if len(wires) != 1: + raise ValueError("MultiControlledX accepts a single target wire.") + else: + if len(wires) < 2: + raise ValueError( + f"MultiControlledX: wrong number of wires. {len(wires)} wire(s) given. Need at least 2." + ) + control_wires = wires[:-1] + wires = wires[-1:] + + control_values = _check_and_convert_control_values(control_values, control_wires) + + super().__init__( + qml.PauliX(wires=wires), + control_wires=control_wires, + control_values=control_values, + work_wires=work_wires, + ) + + def __repr__(self): + return f"MultiControlledX(wires={self.active_wires.tolist()}, control_values={self.control_values})" + + @property + def wires(self): + return self.active_wires + + # pylint: disable=unused-argument, arguments-differ + @staticmethod + def compute_matrix(control_wires, control_values=None, **kwargs): + r"""Representation of the operator as a canonical matrix in the computational basis (static method). + + The canonical matrix is the textbook matrix representation that does not consider wires. + Implicitly, this assumes that the wires of the operator correspond to the global wire order. + + .. seealso:: :meth:`~.MultiControlledX.matrix` + + Args: + control_wires (Any or Iterable[Any]): wires to place controls on + control_values (Union[bool, list[bool], int, list[int]]): The value(s) the control wire(s) + should take. Integers other than 0 or 1 will be treated as ``int(bool(x))``. + + Returns: + tensor_like: matrix representation + + **Example** + + >>> print(qml.MultiControlledX.compute_matrix([0], 1)) + [[1. 0. 0. 0.] + [0. 1. 0. 0.] + [0. 0. 0. 1.] + [0. 0. 1. 0.]] + >>> print(qml.MultiControlledX.compute_matrix([1], 0)) + [[0. 1. 0. 0.] + [1. 0. 0. 0.] + [0. 0. 1. 0.] + [0. 0. 0. 1.]] + + """ + + control_values = _check_and_convert_control_values(control_values, control_wires) + padding_left = sum(2**i * int(val) for i, val in enumerate(reversed(control_values))) * 2 + padding_right = 2 ** (len(control_wires) + 1) - 2 - padding_left + return block_diag(np.eye(padding_left), qml.PauliX.compute_matrix(), np.eye(padding_right)) + + def matrix(self, wire_order=None): + canonical_matrix = self.compute_matrix(self.control_wires, self.control_values) + wire_order = wire_order or self.wires + return qml.math.expand_matrix( + canonical_matrix, wires=self.active_wires, wire_order=wire_order + ) + + # pylint: disable=unused-argument, arguments-differ + @staticmethod + def compute_decomposition(wires=None, work_wires=None, control_values=None, **kwargs): + r"""Representation of the operator as a product of other operators (static method). + + .. math:: O = O_1 O_2 \dots O_n. + + .. seealso:: :meth:`~.MultiControlledX.decomposition`. + + Args: + wires (Iterable[Any] or Wires): wires that the operation acts on + work_wires (Wires): optional work wires used to decompose + the operation into a series of Toffoli gates. + control_values (Union[bool, list[bool], int, list[int]]): The value(s) the control wire(s) + should take. Integers other than 0 or 1 will be treated as ``int(bool(x))``. + + Returns: + list[Operator]: decomposition into lower level operations + + **Example:** + + >>> print(qml.MultiControlledX.compute_decomposition( + ... wires=[0,1,2,3], control_values=[1,1,1], work_wires=qml.wires.Wires("aux"))) + [Toffoli(wires=[2, 'aux', 3]), + Toffoli(wires=[0, 1, 'aux']), + Toffoli(wires=[2, 'aux', 3]), + Toffoli(wires=[0, 1, 'aux'])] + + """ + + if len(wires) < 2: + raise ValueError(f"Wrong number of wires. {len(wires)} given. Need at least 2.") + + target_wire = wires[-1] + control_wires = wires[:-1] + + if control_values is None: + control_values = [True] * len(control_wires) + + work_wires = work_wires or [] + if len(control_wires) > 2 and len(work_wires) == 0: + raise ValueError( + "At least one work wire is required to decompose operation: MultiControlledX" + ) + + flips1 = [qml.PauliX(wires=w) for w, val in zip(control_wires, control_values) if not val] + + if len(control_wires) == 1: + decomp = [qml.CNOT(wires=wires)] + elif len(control_wires) == 2: + decomp = qml.Toffoli.compute_decomposition(wires=wires) + else: + decomp = decompose_mcx(control_wires, target_wire, work_wires) + + flips2 = [qml.PauliX(wires=w) for w, val in zip(control_wires, control_values) if not val] + + return flips1 + decomp + flips2 + + def decomposition(self): + return self.compute_decomposition(self.active_wires, self.work_wires, self.control_values) + class CRX(ControlledOp): r"""The controlled-RX operator @@ -423,6 +664,7 @@ def compute_matrix(theta): # pylint: disable=arguments-differ [0.0+0.0j, 0.0+0.0j, 0.9689+0.0j, 0.0-0.2474j], [0.0+0.0j, 0.0+0.0j, 0.0-0.2474j, 0.9689+0.0j]]) """ + interface = qml.math.get_interface(theta) c = qml.math.cos(theta / 2) diff --git a/pennylane/ops/qubit/__init__.py b/pennylane/ops/qubit/__init__.py index d9484a05c33..147f4100988 100644 --- a/pennylane/ops/qubit/__init__.py +++ b/pennylane/ops/qubit/__init__.py @@ -88,7 +88,6 @@ "QubitUnitary", "BlockEncode", "SpecialUnitary", - "MultiControlledX", "IntegerComparator", "DiagonalQubitUnitary", "SingleExcitation", diff --git a/pennylane/ops/qubit/arithmetic_ops.py b/pennylane/ops/qubit/arithmetic_ops.py index 27236b78221..fa84ab73c5d 100644 --- a/pennylane/ops/qubit/arithmetic_ops.py +++ b/pennylane/ops/qubit/arithmetic_ops.py @@ -24,7 +24,6 @@ from pennylane.operation import AnyWires, Operation from pennylane.wires import Wires from pennylane.ops import Identity -from pennylane.ops.qubit.non_parametric_ops import MultiControlledX class QubitCarry(Operation): @@ -465,7 +464,7 @@ def compute_matrix( control_values_list = [format(n, binary) for n in values] mat = np.eye(2 ** (len(control_wires) + 1)) for control_values in control_values_list: - mat = mat @ MultiControlledX.compute_matrix( + mat = mat @ qml.MultiControlledX.compute_matrix( control_wires, control_values=control_values ) @@ -523,7 +522,7 @@ def compute_decomposition(value, geq=True, wires=None, work_wires=None, **kwargs gates = [] for control_values in control_values_list: gates.append( - MultiControlledX( + qml.MultiControlledX( wires=control_wires + wires, control_values=control_values, work_wires=work_wires, diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 6233444ee5f..4cc695f2df5 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -17,17 +17,15 @@ """ # pylint:disable=abstract-method,arguments-differ,protected-access,invalid-overridden-method, no-member import cmath -import warnings from copy import copy from functools import lru_cache import numpy as np from scipy import sparse -from scipy.linalg import block_diag import pennylane as qml -from pennylane.operation import AnyWires, Observable, Operation +from pennylane.operation import Observable, Operation from pennylane.utils import pauli_eigs from pennylane.wires import Wires @@ -158,12 +156,11 @@ def compute_decomposition(wires): PhaseShift(1.5707963267948966, wires=[0])] """ - decomp_ops = [ + return [ qml.PhaseShift(np.pi / 2, wires=wires), qml.RX(np.pi / 2, wires=wires), qml.PhaseShift(np.pi / 2, wires=wires), ] - return decomp_ops def _controlled(self, wire): return CH(wires=Wires(wire) + self.wires) @@ -311,12 +308,11 @@ def compute_decomposition(wires): PhaseShift(1.5707963267948966, wires=[0])] """ - decomp_ops = [ + return [ qml.PhaseShift(np.pi / 2, wires=wires), qml.RX(np.pi, wires=wires), qml.PhaseShift(np.pi / 2, wires=wires), ] - return decomp_ops def adjoint(self): return PauliX(wires=self.wires) @@ -469,12 +465,11 @@ def compute_decomposition(wires): PhaseShift(1.5707963267948966, wires=[0])] """ - decomp_ops = [ + return [ qml.PhaseShift(np.pi / 2, wires=wires), qml.RY(np.pi, wires=wires), qml.PhaseShift(np.pi / 2, wires=wires), ] - return decomp_ops def adjoint(self): return PauliY(wires=self.wires) @@ -959,13 +954,12 @@ def compute_decomposition(wires): PhaseShift(1.5707963267948966, wires=[0])] """ - decomp_ops = [ + return [ qml.RZ(np.pi / 2, wires=wires), qml.RY(np.pi / 2, wires=wires), qml.RZ(-np.pi, wires=wires), qml.PhaseShift(np.pi / 2, wires=wires), ] - return decomp_ops def pow(self, z): z_mod4 = z % 4 @@ -1224,12 +1218,11 @@ def compute_decomposition(wires): [CNOT(wires=[0, 1]), CNOT(wires=[1, 0]), CNOT(wires=[0, 1])] """ - decomp_ops = [ + return [ qml.CNOT(wires=[wires[0], wires[1]]), qml.CNOT(wires=[wires[1], wires[0]]), qml.CNOT(wires=[wires[0], wires[1]]), ] - return decomp_ops def pow(self, z): return super().pow(z % 2) @@ -1471,7 +1464,7 @@ def compute_decomposition(wires): Hadamard(wires=[1])] """ - decomp_ops = [ + return [ S(wires=wires[0]), S(wires=wires[1]), Hadamard(wires=wires[0]), @@ -1479,7 +1472,6 @@ def compute_decomposition(wires): CNOT(wires=[wires[1], wires[0]]), Hadamard(wires=wires[1]), ] - return decomp_ops def pow(self, z): z_mod2 = z % 2 @@ -1601,7 +1593,7 @@ def compute_decomposition(wires): SX(wires=[1])] """ - decomp_ops = [ + return [ SX(wires=wires[0]), qml.RZ(np.pi / 2, wires=wires[0]), CNOT(wires=[wires[0], wires[1]]), @@ -1615,7 +1607,6 @@ def compute_decomposition(wires): SX(wires=wires[0]), SX(wires=wires[1]), ] - return decomp_ops def pow(self, z): z_mod4 = z % 4 @@ -1865,7 +1856,7 @@ def compute_decomposition(wires): T(wires=wires[0]), qml.adjoint(T(wires=wires[1])), CNOT(wires=[wires[0], wires[1]]), - Hadamard(wires=[2]), + Hadamard(wires=wires[2]), ] def adjoint(self): @@ -2024,329 +2015,3 @@ def control_wires(self): @property def is_hermitian(self): return True - - -class MultiControlledX(Operation): - r"""MultiControlledX(control_wires, wires, control_values) - Apply a Pauli X gate controlled on an arbitrary computational basis state. - - **Details:** - - * Number of wires: Any (the operation can act on any number of wires) - * Number of parameters: 0 - * Gradient recipe: None - - Args: - control_wires (Union[Wires, Sequence[int], or int]): Deprecated way to indicate the control wires. - Now users should use "wires" to indicate both the control wires and the target wire. - wires (Union[Wires, Sequence[int], or int]): control wire(s) followed by a single target wire where - the operation acts on - control_values (str): a string of bits representing the state of the control - wires to control on (default is the all 1s state) - work_wires (Union[Wires, Sequence[int], or int]): optional work wires used to decompose - the operation into a series of Toffoli gates - - - .. note:: - - If ``MultiControlledX`` is not supported on the targeted device, PennyLane will decompose - the operation into :class:`~.Toffoli` and/or :class:`~.CNOT` gates. When controlling on - three or more wires, the Toffoli-based decompositions described in Lemmas 7.2 and 7.3 of - `Barenco et al. `__ will be used. These methods - require at least one work wire. - - The number of work wires provided determines the decomposition method used and the resulting - number of Toffoli gates required. When ``MultiControlledX`` is controlling on :math:`n` - wires: - - #. If at least :math:`n - 2` work wires are provided, the decomposition in Lemma 7.2 will be - applied using the first :math:`n - 2` work wires. - #. If fewer than :math:`n - 2` work wires are provided, a combination of Lemmas 7.3 and 7.2 - will be applied using only the first work wire. - - These methods present a tradeoff between qubit number and depth. The method in point 1 - requires fewer Toffoli gates but a greater number of qubits. - - Note that the state of the work wires before and after the decomposition takes place is - unchanged. - - """ - - is_self_inverse = True - num_wires = AnyWires - num_params = 0 - """int: Number of trainable parameters that the operator depends on.""" - - grad_method = None - - def _flatten(self): - hyperparameters = ( - ("wires", self.wires), - ("control_values", self.hyperparameters["control_values"]), - ("work_wires", self.hyperparameters["work_wires"]), - ) - return tuple(), hyperparameters - - @classmethod - def _unflatten(cls, _, metadata): - return cls(**dict(metadata)) - - # pylint: disable=too-many-arguments - def __init__(self, control_wires=None, wires=None, control_values=None, work_wires=None): - if wires is None: - raise ValueError("Must specify the wires where the operation acts on") - if control_wires is None: - if len(wires) > 1: - control_wires = Wires(wires[:-1]) - wires = Wires(wires[-1]) - else: - raise ValueError( - "MultiControlledX: wrong number of wires. " - f"{len(wires)} wire(s) given. Need at least 2." - ) - else: - wires = Wires(wires) - control_wires = Wires(control_wires) - - warnings.warn( - "The control_wires keyword will be removed soon. " - "Use wires = (control_wires, target_wire) instead. " - "See the documentation for more information.", - category=UserWarning, - ) - - if len(wires) != 1: - raise ValueError("MultiControlledX accepts a single target wire.") - - work_wires = Wires([]) if work_wires is None else Wires(work_wires) - total_wires = control_wires + wires - - if Wires.shared_wires([total_wires, work_wires]): - raise ValueError("The work wires must be different from the control and target wires") - - if not control_values: - control_values = "1" * len(control_wires) - - self.hyperparameters["control_wires"] = control_wires - self.hyperparameters["work_wires"] = work_wires - self.hyperparameters["control_values"] = control_values - self.total_wires = total_wires - - super().__init__(wires=self.total_wires) - - def __repr__(self): - return f'MultiControlledX(wires={list(self.total_wires._labels)}, control_values="{self.hyperparameters["control_values"]}")' - - def label(self, decimals=None, base_label=None, cache=None): - return base_label or "X" - - # pylint: disable=unused-argument - @staticmethod - def compute_matrix( - control_wires, control_values=None, **kwargs - ): # pylint: disable=arguments-differ - r"""Representation of the operator as a canonical matrix in the computational basis (static method). - - The canonical matrix is the textbook matrix representation that does not consider wires. - Implicitly, this assumes that the wires of the operator correspond to the global wire order. - - .. seealso:: :meth:`~.MultiControlledX.matrix` - - Args: - control_wires (Any or Iterable[Any]): wires to place controls on - control_values (str): string of bits determining the controls - - Returns: - tensor_like: matrix representation - - **Example** - - >>> print(qml.MultiControlledX.compute_matrix([0], '1')) - [[1. 0. 0. 0.] - [0. 1. 0. 0.] - [0. 0. 0. 1.] - [0. 0. 1. 0.]] - >>> print(qml.MultiControlledX.compute_matrix([1], '0')) - [[0. 1. 0. 0.] - [1. 0. 0. 0.] - [0. 0. 1. 0.] - [0. 0. 0. 1.]] - - """ - if control_values is None: - control_values = "1" * len(control_wires) - - if isinstance(control_values, str): - if len(control_values) != len(control_wires): - raise ValueError("Length of control bit string must equal number of control wires.") - - # Make sure all values are either 0 or 1 - if not set(control_values).issubset({"1", "0"}): - raise ValueError("String of control values can contain only '0' or '1'.") - - control_int = int(control_values, 2) - else: - raise ValueError("Control values must be passed as a string.") - - padding_left = control_int * 2 - padding_right = 2 ** (len(control_wires) + 1) - 2 - padding_left - cx = block_diag(np.eye(padding_left), PauliX.compute_matrix(), np.eye(padding_right)) - return cx - - @property - def control_wires(self): - return self.wires[:~0] - - def adjoint(self): - return MultiControlledX( - wires=self.wires, - control_values=self.hyperparameters["control_values"], - ) - - def pow(self, z): - return super().pow(z % 2) - - @staticmethod - def compute_decomposition(wires=None, work_wires=None, control_values=None, **kwargs): - r"""Representation of the operator as a product of other operators (static method). - - .. math:: O = O_1 O_2 \dots O_n. - - - .. seealso:: :meth:`~.MultiControlledX.decomposition`. - - Args: - wires (Iterable[Any] or Wires): wires that the operation acts on - work_wires (Wires): optional work wires used to decompose - the operation into a series of Toffoli gates. - control_values (str): a string of bits representing the state of the control - wires to control on (default is the all 1s state) - - Returns: - list[Operator]: decomposition into lower level operations - - **Example:** - - >>> print(qml.MultiControlledX.compute_decomposition(wires=[0,1,2,3],control_values="111", work_wires=qml.wires.Wires("aux"))) - [Toffoli(wires=[2, 'aux', 3]), - Toffoli(wires=[0, 1, 'aux']), - Toffoli(wires=[2, 'aux', 3]), - Toffoli(wires=[0, 1, 'aux'])] - - """ - - target_wire = wires[~0] - control_wires = wires[:~0] - - if control_values is None: - control_values = "1" * len(control_wires) - - if len(control_wires) > 2 and len(work_wires) == 0: - raise ValueError( - "At least one work wire is required to decompose operation: MultiControlledX" - ) - - flips1 = [ - qml.PauliX(control_wires[i]) for i, val in enumerate(control_values) if val == "0" - ] - - if len(control_wires) == 1: - decomp = [qml.CNOT(wires=[control_wires[0], target_wire])] - elif len(control_wires) == 2: - decomp = [qml.Toffoli(wires=[*control_wires, target_wire])] - else: - num_work_wires_needed = len(control_wires) - 2 - - if len(work_wires) >= num_work_wires_needed: - decomp = MultiControlledX._decomposition_with_many_workers( - control_wires, target_wire, work_wires - ) - else: - work_wire = work_wires[0] - decomp = MultiControlledX._decomposition_with_one_worker( - control_wires, target_wire, work_wire - ) - - flips2 = [ - qml.PauliX(control_wires[i]) for i, val in enumerate(control_values) if val == "0" - ] - - return flips1 + decomp + flips2 - - @staticmethod - def _decomposition_with_many_workers(control_wires, target_wire, work_wires): - """Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.2 of - https://arxiv.org/abs/quant-ph/9503016, which requires a suitably large register of - work wires""" - num_work_wires_needed = len(control_wires) - 2 - work_wires = work_wires[:num_work_wires_needed] - - work_wires_reversed = list(reversed(work_wires)) - control_wires_reversed = list(reversed(control_wires)) - - gates = [] - - for i in range(len(work_wires)): - ctrl1 = control_wires_reversed[i] - ctrl2 = work_wires_reversed[i] - t = target_wire if i == 0 else work_wires_reversed[i - 1] - gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) - - gates.append(qml.Toffoli(wires=[*control_wires[:2], work_wires[0]])) - - for i in reversed(range(len(work_wires))): - ctrl1 = control_wires_reversed[i] - ctrl2 = work_wires_reversed[i] - t = target_wire if i == 0 else work_wires_reversed[i - 1] - gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) - - for i in range(len(work_wires) - 1): - ctrl1 = control_wires_reversed[i + 1] - ctrl2 = work_wires_reversed[i + 1] - t = work_wires_reversed[i] - gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) - - gates.append(qml.Toffoli(wires=[*control_wires[:2], work_wires[0]])) - - for i in reversed(range(len(work_wires) - 1)): - ctrl1 = control_wires_reversed[i + 1] - ctrl2 = work_wires_reversed[i + 1] - t = work_wires_reversed[i] - gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t])) - - return gates - - @staticmethod - def _decomposition_with_one_worker(control_wires, target_wire, work_wire): - """Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.3 of - https://arxiv.org/abs/quant-ph/9503016, which requires a single work wire""" - tot_wires = len(control_wires) + 2 - partition = int(np.ceil(tot_wires / 2)) - - first_part = control_wires[:partition] - second_part = control_wires[partition:] - - gates = [ - MultiControlledX( - wires=first_part + work_wire, - work_wires=second_part + target_wire, - ), - MultiControlledX( - wires=second_part + work_wire + target_wire, - work_wires=first_part, - ), - MultiControlledX( - wires=first_part + work_wire, - work_wires=second_part + target_wire, - ), - MultiControlledX( - wires=second_part + work_wire + target_wire, - work_wires=first_part, - ), - ] - - return gates - - @property - def is_hermitian(self): - return True diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index 0bde74d6dcc..5ca6ff93f37 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -655,12 +655,11 @@ def compute_decomposition(phi, theta, omega, wires): [RZ(1.2, wires=[0]), RY(2.3, wires=[0]), RZ(3.4, wires=[0])] """ - decomp_ops = [ + return [ RZ(phi, wires=wires), RY(theta, wires=wires), RZ(omega, wires=wires), ] - return decomp_ops def adjoint(self): phi, theta, omega = self.parameters @@ -927,12 +926,11 @@ def compute_decomposition(phi, delta, wires): """ pi_half = qml.math.ones_like(delta) * (np.pi / 2) - decomp_ops = [ + return [ Rot(delta, pi_half, -delta, wires=wires), PhaseShift(delta, wires=wires), PhaseShift(phi, wires=wires), ] - return decomp_ops def adjoint(self): phi, delta = self.parameters @@ -1084,12 +1082,11 @@ def compute_decomposition(theta, phi, delta, wires): PhaseShift(2.34, wires=[0])] """ - decomp_ops = [ + return [ Rot(delta, theta, -delta, wires=wires), PhaseShift(delta, wires=wires), PhaseShift(phi, wires=wires), ] - return decomp_ops def adjoint(self): theta, phi, delta = self.parameters diff --git a/pennylane/transforms/qmc.py b/pennylane/transforms/qmc.py index 995f894759e..1f3405dafaa 100644 --- a/pennylane/transforms/qmc.py +++ b/pennylane/transforms/qmc.py @@ -122,7 +122,7 @@ def apply_controlled_Q( wires = Wires(wires) target_wire = Wires(target_wire) control_wire = Wires(control_wire) - work_wires = Wires(work_wires) + work_wires = Wires(work_wires) if work_wires is not None else Wires([]) if not wires.contains_wires(target_wire): raise ValueError("The target wire must be contained within wires") diff --git a/tests/drawer/test_tape_mpl.py b/tests/drawer/test_tape_mpl.py index 2baab80b48c..8af4d169799 100644 --- a/tests/drawer/test_tape_mpl.py +++ b/tests/drawer/test_tape_mpl.py @@ -23,6 +23,7 @@ from pennylane.drawer import tape_mpl from pennylane.tape import QuantumScript +from pennylane.ops.op_math import Controlled mpl = pytest.importorskip("matplotlib") plt = pytest.importorskip("matplotlib.pyplot") @@ -544,9 +545,9 @@ def test_nested_control_values_bool(self): when they are provided as a list of bools.""" with qml.queuing.AnnotatedQueue() as q_tape: - qml.ctrl( - qml.ctrl(qml.PauliX(wires=4), control=[2, 3], control_values=[1, 0]), - control=[0, 1], + Controlled( + qml.ctrl(qml.PauliY(wires=4), control=[2, 3], control_values=[1, 0]), + control_wires=[0, 1], control_values=[1, 0], ) diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py index 54d3a3ddc27..3f804d15334 100644 --- a/tests/ops/op_math/test_controlled.py +++ b/tests/ops/op_math/test_controlled.py @@ -16,18 +16,30 @@ from copy import copy from functools import partial -import numpy as onp +import numpy as np import pytest -from gate_data import CNOT, CSWAP, CZ, CRot3, CRotx, CRoty, CRotz, Toffoli +from gate_data import ( + CNOT, + CSWAP, + CY, + CZ, + CCZ, + CH, + CRot3, + CRotx, + CRoty, + CRotz, + Toffoli, + ControlledPhaseShift, +) from scipy import sparse import pennylane as qml -from pennylane import numpy as np +from pennylane import numpy as pnp from pennylane.operation import DecompositionUndefinedError, Operation, Operator from pennylane.ops.op_math.controlled import ( Controlled, ControlledOp, - _decompose_no_control_values, ctrl, ) from pennylane.tape import QuantumScript @@ -38,6 +50,7 @@ # pylint: disable=protected-access # pylint: disable=pointless-statement # pylint: disable=expression-not-assigned +# pylint: disable=too-many-arguments def equal_list(lhs, rhs): @@ -48,18 +61,6 @@ def equal_list(lhs, rhs): return len(lhs) == len(rhs) and all(qml.equal(l, r) for l, r in zip(lhs, rhs)) -base_num_control_mats = [ - (qml.PauliX("a"), 1, CNOT), - (qml.PauliZ("a"), 1, CZ), - (qml.SWAP(("a", "b")), 1, CSWAP), - (qml.PauliX("a"), 2, Toffoli), - (qml.RX(1.234, "b"), 1, CRotx(1.234)), - (qml.RY(-0.432, "a"), 1, CRoty(-0.432)), - (qml.RZ(6.78, "a"), 1, CRotz(6.78)), - (qml.Rot(1.234, -0.432, 9.0, "a"), 1, CRot3(1.234, -0.432, 9.0)), -] - - class TempOperator(Operator): num_wires = 1 @@ -68,6 +69,16 @@ class TempOperation(Operation): num_wires = 1 +class OpWithDecomposition(Operation): + @staticmethod + def compute_decomposition(*params, wires=None, **_): + return [ + qml.Hadamard(wires=wires[0]), + qml.S(wires=wires[1]), + qml.RX(params[0], wires=wires[0]), + ] + + class TestControlledInheritance: """Test the inheritance structure modified through dynamic __new__ method.""" @@ -110,7 +121,7 @@ def test_controlledop_new(self): assert type(op) is ControlledOp # pylint: disable=unidiomatic-typecheck -class TestInitialization: +class TestControlledInit: """Test the initialization process and standard properties.""" temp_op = TempOperator("a") @@ -182,25 +193,25 @@ def test_work_wires_overlap_control(self): Controlled(self.temp_op, control_wires="b", work_wires="b") -class TestProperties: +class TestControlledProperties: """Test the properties of the ``Controlled`` symbolic operator.""" def test_data(self): """Test that the base data can be get and set through Controlled class.""" - x = np.array(1.234) + x = pnp.array(1.234) base = qml.RX(x, wires="a") op = Controlled(base, (0, 1)) assert op.data == (x,) - x_new = (np.array(2.3454),) + x_new = (pnp.array(2.3454),) op.data = x_new assert op.data == (x_new,) assert base.data == (x_new,) - x_new2 = (np.array(3.456),) + x_new2 = (pnp.array(3.456),) base.data = x_new2 assert op.data == (x_new2,) assert op.parameters == [x_new2] @@ -346,7 +357,7 @@ def test_map_wires(self): assert op.work_wires == Wires(("extra")) -class TestMiscMethods: +class TestControlledMiscMethods: """Test miscellaneous minor Controlled methods.""" def test_repr(self): @@ -416,7 +427,7 @@ def test_label(self): def test_label_matrix_param(self): """Test that the label method simply returns the label of the base and updates the cache.""" - U = np.eye(2) + U = pnp.eye(2) base = qml.QubitUnitary(U, wires=0) op = Controlled(base, ["a", "b"]) @@ -430,10 +441,10 @@ def test_eigvals(self): op = Controlled(base, (2, 3)) mat = op.matrix() - mat_eigvals = np.sort(qml.math.linalg.eigvals(mat)) + mat_eigvals = pnp.sort(qml.math.linalg.eigvals(mat)) eigs = op.eigvals() - sort_eigs = np.sort(eigs) + sort_eigs = pnp.sort(eigs) assert qml.math.allclose(mat_eigvals, sort_eigs) @@ -524,7 +535,7 @@ def test_hash(self): assert op7.hash != op1.hash -class TestOperationProperties: +class TestControlledOperationProperties: """Test ControlledOp specific properties.""" # pylint:disable=no-member @@ -590,7 +601,7 @@ def test_parameter_frequencies_multiple_params_error(self): op.parameter_frequencies -class TestSimplify: +class TestControlledSimplify: """Test qml.sum simplify method and depth property.""" def test_depth_property(self): @@ -634,7 +645,7 @@ def test_simplify_nested_controlled_ops(self): assert simplified_op.arithmetic_depth == final_op.arithmetic_depth -class TestQueuing: +class TestControlledQueuing: """Test that Controlled operators queue and update base metadata.""" def test_queuing(self): @@ -659,27 +670,31 @@ def test_queuing_base_defined_outside(self): base_num_control_mats = [ (qml.PauliX("a"), 1, CNOT), + (qml.PauliX("a"), 2, Toffoli), + (qml.CNOT(["a", "b"]), 1, Toffoli), + (qml.PauliY("a"), 1, CY), (qml.PauliZ("a"), 1, CZ), + (qml.PauliZ("a"), 2, CCZ), (qml.SWAP(("a", "b")), 1, CSWAP), - (qml.PauliX("a"), 2, Toffoli), + (qml.Hadamard("a"), 1, CH), (qml.RX(1.234, "b"), 1, CRotx(1.234)), (qml.RY(-0.432, "a"), 1, CRoty(-0.432)), (qml.RZ(6.78, "a"), 1, CRotz(6.78)), (qml.Rot(1.234, -0.432, 9.0, "a"), 1, CRot3(1.234, -0.432, 9.0)), + (qml.PhaseShift(1.234, wires="a"), 1, ControlledPhaseShift(1.234)), ] class TestMatrix: """Tests of Controlled.matrix and Controlled.sparse_matrix""" - def test_correct_matrix_dimenions_with_batching(self): + def test_correct_matrix_dimensions_with_batching(self): """Test batching returns a matrix of the correct dimensions""" - x = np.array([1.0, 2.0, 3.0]) + + x = pnp.array([1.0, 2.0, 3.0]) base = qml.RX(x, 0) op = Controlled(base, 1) - matrix = op.matrix() - assert matrix.shape == (3, 4, 4) @pytest.mark.parametrize("base, num_control, mat", base_num_control_mats) @@ -781,63 +796,228 @@ def test_sparse_matrix_format(self): assert isinstance(lil_mat, sparse.lil_matrix) -class TestHelperMethod: - """Unittests for the _decompose_no_control_values helper function.""" +special_non_par_op_decomps = [ + (qml.PauliY, [], [0], [1], qml.CY, [qml.CRY(np.pi, wires=[1, 0]), qml.S(1)]), + (qml.PauliZ, [], [1], [0], qml.CZ, [qml.ControlledPhaseShift(np.pi, wires=[0, 1])]), + ( + qml.Hadamard, + [], + [1], + [0], + qml.CH, + [qml.RY(-np.pi / 4, wires=1), qml.CZ(wires=[0, 1]), qml.RY(np.pi / 4, wires=1)], + ), + ( + qml.PauliZ, + [], + [0], + [2, 1], + qml.CCZ, + [ + qml.CNOT(wires=[1, 0]), + qml.adjoint(qml.T(wires=0)), + qml.CNOT(wires=[2, 0]), + qml.T(wires=0), + qml.CNOT(wires=[1, 0]), + qml.adjoint(qml.T(wires=0)), + qml.CNOT(wires=[2, 0]), + qml.T(wires=0), + qml.T(wires=1), + qml.CNOT(wires=[2, 1]), + qml.Hadamard(wires=0), + qml.T(wires=2), + qml.adjoint(qml.T(wires=1)), + qml.CNOT(wires=[2, 1]), + qml.Hadamard(wires=0), + ], + ), + ( + qml.CZ, + [], + [1, 2], + [0], + qml.CCZ, + [ + qml.CNOT(wires=[1, 2]), + qml.adjoint(qml.T(wires=2)), + qml.CNOT(wires=[0, 2]), + qml.T(wires=2), + qml.CNOT(wires=[1, 2]), + qml.adjoint(qml.T(wires=2)), + qml.CNOT(wires=[0, 2]), + qml.T(wires=2), + qml.T(wires=1), + qml.CNOT(wires=[0, 1]), + qml.Hadamard(wires=2), + qml.T(wires=0), + qml.adjoint(qml.T(wires=1)), + qml.CNOT(wires=[0, 1]), + qml.Hadamard(wires=[2]), + ], + ), + ( + qml.SWAP, + [], + [1, 2], + [0], + qml.CSWAP, + [qml.Toffoli(wires=[0, 2, 1]), qml.Toffoli(wires=[0, 1, 2]), qml.Toffoli(wires=[0, 2, 1])], + ), +] - def test_crx(self): - """Test case with single control wire and defined _controlled""" - base = qml.RX(1.0, wires=0) - op = Controlled(base, 1) - decomp = _decompose_no_control_values(op) - assert len(decomp) == 1 - assert qml.equal(decomp[0], qml.CRX(1.0, wires=(1, 0))) - - def test_toffoli(self): - """Test case when PauliX with two controls.""" - op = Controlled(qml.PauliX("c"), ("a", 2)) - decomp = _decompose_no_control_values(op) - assert len(decomp) == 1 - assert equal_list(decomp, qml.MultiControlledX(wires=("a", 2, "c"))) - - def test_multicontrolledx(self): - """Test case when PauliX has many controls.""" - op = Controlled(qml.PauliX(4), (0, 1, 2, 3)) - decomp = _decompose_no_control_values(op) - assert len(decomp) == 1 - assert qml.equal(decomp[0], qml.MultiControlledX(wires=(0, 1, 2, 3, 4))) - - def test_decomposes_target(self): - """Test that we decompose the target if we don't have a special case.""" - target = qml.IsingXX(1.0, wires=(0, 1)) - op = Controlled(target, (3, 4)) - - decomp = _decompose_no_control_values(op) - assert len(decomp) == 3 - - target_decomp = target.expand().circuit - for op1, target in zip(decomp, target_decomp): - assert isinstance(op1, Controlled) - assert op1.control_wires == (3, 4) - - assert qml.equal(op1.base, target) - - def test_None_default(self): - """Test that helper returns None if no special decomposition.""" - op = Controlled(TempOperator(0), (1, 2)) - assert _decompose_no_control_values(op) is None +special_par_op_decomps = [ + ( + qml.RX, + [0.123], + [1], + [0], + qml.CRX, + [ + qml.RZ(np.pi / 2, wires=1), + qml.RY(0.123 / 2, wires=1), + qml.CNOT(wires=[0, 1]), + qml.RY(-0.123 / 2, wires=1), + qml.CNOT(wires=[0, 1]), + qml.RZ(-np.pi / 2, wires=1), + ], + ), + ( + qml.RY, + [0.123], + [1], + [0], + qml.CRY, + [ + qml.RY(0.123 / 2, 1), + qml.CNOT(wires=(0, 1)), + qml.RY(-0.123 / 2, 1), + qml.CNOT(wires=(0, 1)), + ], + ), + ( + qml.RZ, + [0.123], + [0], + [1], + qml.CRZ, + [ + qml.PhaseShift(0.123 / 2, wires=0), + qml.CNOT(wires=[1, 0]), + qml.PhaseShift(-0.123 / 2, wires=0), + qml.CNOT(wires=[1, 0]), + ], + ), + ( + qml.Rot, + [0.1, 0.2, 0.3], + [1], + [0], + qml.CRot, + [ + qml.RZ((0.1 - 0.3) / 2, wires=1), + qml.CNOT(wires=[0, 1]), + qml.RZ(-(0.1 + 0.3) / 2, wires=1), + qml.RY(-0.2 / 2, wires=1), + qml.CNOT(wires=[0, 1]), + qml.RY(0.2 / 2, wires=1), + qml.RZ(0.3, wires=1), + ], + ), + ( + qml.PhaseShift, + [0.123], + [1], + [0], + qml.ControlledPhaseShift, + [ + qml.PhaseShift(0.123 / 2, wires=0), + qml.CNOT(wires=[0, 1]), + qml.PhaseShift(-0.123 / 2, wires=1), + qml.CNOT(wires=[0, 1]), + qml.PhaseShift(0.123 / 2, wires=1), + ], + ), +] + +custom_ctrl_op_decomps = special_non_par_op_decomps + special_par_op_decomps + +pauli_x_based_op_decomps = [ + (qml.PauliX, [0], [1], [qml.CNOT([1, 0])]), + ( + qml.PauliX, + [2], + [0, 1], + qml.Toffoli.compute_decomposition(wires=[0, 1, 2]), + ), + ( + qml.CNOT, + [1, 2], + [0], + qml.Toffoli.compute_decomposition(wires=[0, 1, 2]), + ), + ( + qml.PauliX, + [3], + [0, 1, 2], + qml.MultiControlledX.compute_decomposition(wires=[0, 1, 2, 3], work_wires=Wires("aux")), + ), + ( + qml.CNOT, + [2, 3], + [0, 1], + qml.MultiControlledX.compute_decomposition(wires=[0, 1, 2, 3], work_wires=Wires("aux")), + ), + ( + qml.Toffoli, + [1, 2, 3], + [0], + qml.MultiControlledX.compute_decomposition(wires=[0, 1, 2, 3], work_wires=Wires("aux")), + ), +] + + +class TestDecomposition: + """Test decomposition of Controlled.""" + + @pytest.mark.parametrize( + "target, decomp", + [ + ( + OpWithDecomposition(0.123, wires=[0, 1]), + [ + qml.CH(wires=[2, 0]), + Controlled(qml.S(wires=1), control_wires=2), + qml.CRX(0.123, wires=[2, 0]), + ], + ), + ( + qml.IsingXX(0.123, wires=[0, 1]), + [ + qml.Toffoli(wires=[2, 0, 1]), + qml.CRX(0.123, wires=[2, 0]), + qml.Toffoli(wires=[2, 0, 1]), + ], + ), + ], + ) + def test_decomposition(self, target, decomp): + """Test that we decompose a normal controlled operation""" + op = Controlled(target, 2) + assert op.decomposition() == decomp def test_non_differentiable_one_qubit_special_unitary(self): - """Assert that a non differentiable on qubit special unitary uses the bisect decomposition.""" + """Assert that a non-differentiable on qubit special unitary uses the bisect decomposition.""" + op = qml.ctrl(qml.RZ(1.2, wires=0), (1, 2, 3, 4)) decomp = op.decomposition() - assert qml.equal(decomp[0], qml.MultiControlledX(wires=(1, 2, 0), work_wires=(3, 4))) + assert qml.equal(decomp[0], qml.Toffoli(wires=(1, 2, 0))) assert isinstance(decomp[1], qml.QubitUnitary) - assert qml.equal(decomp[2], qml.MultiControlledX(wires=(3, 4, 0), work_wires=(1, 2))) + assert qml.equal(decomp[2], qml.Toffoli(wires=(3, 4, 0))) assert isinstance(decomp[3].base, qml.QubitUnitary) - assert qml.equal(decomp[4], qml.MultiControlledX(wires=(1, 2, 0), work_wires=(3, 4))) + assert qml.equal(decomp[4], qml.Toffoli(wires=(1, 2, 0))) assert isinstance(decomp[5], qml.QubitUnitary) - assert qml.equal(decomp[6], qml.MultiControlledX(wires=(3, 4, 0), work_wires=(1, 2))) + assert qml.equal(decomp[6], qml.Toffoli(wires=(3, 4, 0))) assert isinstance(decomp[7].base, qml.QubitUnitary) decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)() @@ -858,6 +1038,93 @@ def test_differentiable_one_qubit_special_unitary(self): decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)() assert qml.math.allclose(op.matrix(), decomp_mat) + @pytest.mark.parametrize( + "base_cls, params, base_wires, ctrl_wires, custom_ctrl_cls, expected", + custom_ctrl_op_decomps, + ) + def test_decomposition_custom_ops( + self, + base_cls, + params, + base_wires, + ctrl_wires, + custom_ctrl_cls, + expected, + tol, + ): + """Tests decompositions of custom operations""" + + active_wires = ctrl_wires + base_wires + base_op = base_cls(*params, wires=base_wires) + ctrl_op = Controlled(base_op, control_wires=ctrl_wires) + custom_ctrl_op = custom_ctrl_cls(*params, active_wires) + + assert ctrl_op.decomposition() == expected + assert ctrl_op.expand().circuit == expected + assert custom_ctrl_op.decomposition() == expected + assert custom_ctrl_cls.compute_decomposition(*params, active_wires) == expected + + mat = qml.matrix(ctrl_op.decomposition, wire_order=active_wires)() + assert np.allclose(mat, custom_ctrl_op.matrix(), atol=tol, rtol=0) + + @pytest.mark.parametrize( + "base_cls, params, base_wires, ctrl_wires, custom_ctrl_cls, expected", + special_par_op_decomps, + ) + def test_decomposition_custom_par_ops_broadcasted( + self, + base_cls, + params, + base_wires, + ctrl_wires, + custom_ctrl_cls, + expected, + tol, + ): + """Tests broadcasted decompositions of custom controlled ops""" + broad_casted_params = [np.array([p, p]) for p in params] + self.test_decomposition_custom_ops( + base_cls, + broad_casted_params, + base_wires, + ctrl_wires, + custom_ctrl_cls, + expected, + tol, + ) + + @pytest.mark.parametrize( + "base_cls, base_wires, ctrl_wires, expected", + pauli_x_based_op_decomps, + ) + def test_decomposition_pauli_x(self, base_cls, base_wires, ctrl_wires, expected): + """Tests decompositions where the base is PauliX""" + + base_op = base_cls(wires=base_wires) + ctrl_op = Controlled(base_op, control_wires=ctrl_wires, work_wires=Wires("aux")) + + assert ctrl_op.decomposition() == expected + assert ctrl_op.expand().circuit == expected + + def test_decomposition_nested(self): + """Tests decompositions of nested controlled operations""" + + ctrl_op = Controlled(Controlled(qml.RZ(0.123, wires=0), control_wires=1), control_wires=2) + expected = [ + qml.ControlledPhaseShift(0.123 / 2, wires=[2, 0]), + qml.Toffoli(wires=[2, 1, 0]), + qml.ControlledPhaseShift(-0.123 / 2, wires=[2, 0]), + qml.Toffoli(wires=[2, 1, 0]), + ] + assert ctrl_op.decomposition() == expected + assert ctrl_op.expand().circuit == expected + + def test_decomposition_undefined(self): + """Tests error raised when decomposition is undefined""" + op = Controlled(TempOperator(0), (1, 2)) + with pytest.raises(DecompositionUndefinedError): + op.decomposition() + def test_global_phase_decomp_raises_warning(self): """Test that ctrl(GlobalPhase).decomposition() raises a warning.""" op = qml.ctrl(qml.GlobalPhase(1.23), control=[0]) @@ -866,12 +1133,7 @@ def test_global_phase_decomp_raises_warning(self): ): assert op.decomposition() == [] - -@pytest.mark.parametrize("test_expand", (False, True)) -class TestDecomposition: - """Test controlled's decomposition method.""" - - def test_control_values_no_special_decomp(self, test_expand): + def test_control_on_zero(self): """Test decomposition applies PauliX gates to flip any control-on-zero wires.""" control_wires = (0, 1, 2) @@ -880,57 +1142,45 @@ def test_control_values_no_special_decomp(self, test_expand): base = TempOperator("a") op = Controlled(base, control_wires, control_values) - decomp = op.expand().circuit if test_expand else op.decomposition() - - assert qml.equal(decomp[0], qml.PauliX(1)) - assert qml.equal(decomp[1], qml.PauliX(2)) + decomp1 = op.decomposition() + decomp2 = op.expand().circuit - assert isinstance(decomp[2], Controlled) - assert decomp[2].control_values == [True, True, True] + for decomp in [decomp1, decomp2]: + assert qml.equal(decomp[0], qml.PauliX(1)) + assert qml.equal(decomp[1], qml.PauliX(2)) - assert qml.equal(decomp[3], qml.PauliX(1)) - assert qml.equal(decomp[4], qml.PauliX(2)) + assert isinstance(decomp[2], Controlled) + assert decomp[2].control_values == [True, True, True] - def test_control_values_special_decomp(self, test_expand): - """Test decomposition when needs control_values flips and special decomp exists.""" + assert qml.equal(decomp[3], qml.PauliX(1)) + assert qml.equal(decomp[4], qml.PauliX(2)) - base = qml.PauliX(2) - op = Controlled(base, (0, 1), (True, False)) - - decomp = op.expand().circuit if test_expand else op.decomposition() - expected = [qml.PauliX(1), qml.MultiControlledX(wires=(0, 1, 2)), qml.PauliX(1)] - assert equal_list(decomp, expected) + @pytest.mark.parametrize( + "base_cls, params, base_wires, ctrl_wires, _, expected", + custom_ctrl_op_decomps, + ) + def test_control_on_zero_custom_ops( + self, base_cls, params, base_wires, ctrl_wires, _, expected + ): + """Tests that custom ops are not converted when wires are control-on-zero.""" - def test_no_control_values_special_decomp(self, test_expand): - """Test a case with no control values but a special decomposition.""" - base = qml.RX(1.0, 2) - op = Controlled(base, 1) - decomp = op.expand().circuit if test_expand else op.decomposition() - assert len(decomp) == 1 - assert qml.equal(decomp[0], qml.CRX(1.0, (1, 2))) - - def test_no_control_values_target_decomposition(self, test_expand): - """Tests a case with no control values and no special decomposition but - the ability to decompose the target.""" - base = qml.IsingXX(1.23, wires=(0, 1)) - op = Controlled(base, "a") + base_op = base_cls(*params, wires=base_wires) + op = Controlled(base_op, control_wires=ctrl_wires, control_values=[False] * len(ctrl_wires)) - decomp = op.expand().circuit if test_expand else op.decomposition() - base_decomp = base.decomposition() - for cop, base_op in zip(decomp, base_decomp): - assert isinstance(cop, Controlled) - assert qml.equal(cop.base, base_op) + decomp = op.decomposition() - def test_no_control_values_no_special_decomp(self, test_expand): - """Test if all control_values are true and no special decomposition exists, - the method raises a DecompositionUndefinedError.""" + i = 0 + for ctrl_wire in ctrl_wires: + assert decomp[i] == qml.PauliX(wires=ctrl_wire) + i += 1 - base = TempOperator("a") - op = Controlled(base, (0, 1, 2)) + for exp in expected: + assert decomp[i] == exp + i += 1 - with pytest.raises(DecompositionUndefinedError): - # pylint: disable=unused-variable - decomp = op.expand().circuit if test_expand else op.decomposition() + for ctrl_wire in ctrl_wires: + assert decomp[i] == qml.PauliX(wires=ctrl_wire) + i += 1 class TestArithmetic: @@ -991,7 +1241,7 @@ def test_autograd(self, diff_method): """Test differentiation using autograd""" dev = qml.device("default.qubit", wires=2) - init_state = np.array([1.0, -1.0], requires_grad=False) / np.sqrt(2) + init_state = pnp.array([1.0, -1.0], requires_grad=False) / pnp.sqrt(2) @qml.qnode(dev, diff_method=diff_method) def circuit(b): @@ -999,11 +1249,11 @@ def circuit(b): Controlled(qml.RY(b, wires=1), control_wires=0) return qml.expval(qml.PauliX(0)) - b = np.array(0.123, requires_grad=True) + b = pnp.array(0.123, requires_grad=True) res = qml.grad(circuit)(b) - expected = np.sin(b / 2) / 2 + expected = pnp.sin(b / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) @pytest.mark.torch def test_torch(self, diff_method): @@ -1013,7 +1263,7 @@ def test_torch(self, diff_method): dev = qml.device("default.qubit", wires=2) init_state = torch.tensor( [1.0, -1.0], requires_grad=False, dtype=torch.complex128 - ) / np.sqrt(2) + ) / pnp.sqrt(2) @qml.qnode(dev, diff_method=diff_method) def circuit(b): @@ -1026,9 +1276,9 @@ def circuit(b): loss.backward() # pylint:disable=no-member res = b.grad.detach() - expected = np.sin(b.detach() / 2) / 2 + expected = pnp.sin(b.detach() / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) @pytest.mark.jax @pytest.mark.parametrize("jax_interface", ["auto", "jax", "jax-python"]) @@ -1045,16 +1295,16 @@ def test_jax(self, diff_method, jax_interface): @qml.qnode(dev, diff_method=diff_method, interface=jax_interface) def circuit(b): - init_state = onp.array([1.0, -1.0]) / np.sqrt(2) + init_state = np.array([1.0, -1.0]) / pnp.sqrt(2) qml.StatePrep(init_state, wires=0) Controlled(qml.RY(b, wires=1), control_wires=0) return qml.expval(qml.PauliX(0)) b = jnp.array(0.123) res = jax.grad(circuit)(b) - expected = np.sin(b / 2) / 2 + expected = pnp.sin(b / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) @pytest.mark.tf def test_tf(self, diff_method): @@ -1062,7 +1312,7 @@ def test_tf(self, diff_method): import tensorflow as tf dev = qml.device("default.qubit", wires=2) - init_state = tf.constant([1.0, -1.0], dtype=tf.complex128) / np.sqrt(2) + init_state = tf.constant([1.0, -1.0], dtype=tf.complex128) / pnp.sqrt(2) @qml.qnode(dev, diff_method=diff_method) def circuit(b): @@ -1076,9 +1326,9 @@ def circuit(b): loss = circuit(b) res = tape.gradient(loss, b) - expected = np.sin(b / 2) / 2 + expected = pnp.sin(b / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) class TestControlledSupportsBroadcasting: @@ -1145,7 +1395,7 @@ def test_controlled_of_single_scalar_single_wire_ops(self, name): """Test that a Controlled operation whose base is a single-scalar-parameter operations on a single wire marked as supporting parameter broadcasting actually do support broadcasting. """ - par = np.array([0.25, 2.1, -0.42]) + par = pnp.array([0.25, 2.1, -0.42]) wires = ["wire0"] cls = getattr(qml, name) @@ -1162,7 +1412,7 @@ def test_controlled_single_scalar_multi_wire_ops(self, name): """Test that a Controlled operation whose base is a single-scalar-parameter operations on multiple wires marked as supporting parameter broadcasting actually do support broadcasting. """ - par = np.array([0.25, 2.1, -0.42]) + par = pnp.array([0.25, 2.1, -0.42]) cls = getattr(qml, name) # Provide up to 6 wires and take as many as the class requires @@ -1181,7 +1431,7 @@ def test_controlled_two_scalar_single_wire_ops(self, name): """Test that a Controlled operation whose base is a two-scalar-parameter operations on a single wire marked as supporting parameter broadcasting actually do support broadcasting. """ - par = (np.array([0.25, 2.1, -0.42]), np.array([-6.2, 0.12, 0.421])) + par = (pnp.array([0.25, 2.1, -0.42]), pnp.array([-6.2, 0.12, 0.421])) wires = ["wire0"] cls = getattr(qml, name) @@ -1200,9 +1450,9 @@ def test_controlled_three_scalar_single_wire_ops(self, name): on a single wire marked as supporting parameter broadcasting actually do support broadcasting. """ par = ( - np.array([0.25, 2.1, -0.42]), - np.array([-6.2, 0.12, 0.421]), - np.array([0.2, 1.1, -5.2]), + pnp.array([0.25, 2.1, -0.42]), + pnp.array([-6.2, 0.12, 0.421]), + pnp.array([0.2, 1.1, -5.2]), ) wires = ["wire0"] @@ -1222,9 +1472,9 @@ def test_controlled_three_scalar_multi_wire_ops(self, name): on multiple wires marked as supporting parameter broadcasting actually do support broadcasting. """ par = ( - np.array([0.25, 2.1, -0.42]), - np.array([-6.2, 0.12, 0.421]), - np.array([0.2, 1.1, -5.2]), + pnp.array([0.25, 2.1, -0.42]), + pnp.array([-6.2, 0.12, 0.421]), + pnp.array([0.2, 1.1, -5.2]), ) wires = ["wire0", 214] @@ -1241,7 +1491,7 @@ def test_controlled_three_scalar_multi_wire_ops(self, name): def test_controlled_diagonal_qubit_unitary(self): """Test that a Controlled operation whose base is a DiagonalQubitUnitary, which is marked as supporting parameter broadcasting, actually does support broadcasting.""" - diag = np.array([[1j, 1, 1, -1j], [-1j, 1j, 1, -1], [1j, -1j, 1.0, -1]]) + diag = pnp.array([[1j, 1, 1, -1j], [-1j, 1j, 1, -1], [1j, -1j, 1.0, -1]]) wires = ["a", 5] base = qml.DiagonalQubitUnitary(diag, wires=wires) @@ -1260,7 +1510,7 @@ def test_controlled_diagonal_qubit_unitary(self): def test_controlled_pauli_rot(self, pauli_word, wires): """Test that a Controlled operation whose base is PauliRot, which is marked as supporting parameter broadcasting, actually does support broadcasting.""" - par = np.array([0.25, 2.1, -0.42]) + par = pnp.array([0.25, 2.1, -0.42]) base = qml.PauliRot(par, pauli_word, wires=wires) op = Controlled(base, "wire1") @@ -1276,7 +1526,7 @@ def test_controlled_pauli_rot(self, pauli_word, wires): def test_controlled_multi_rz(self, wires): """Test that a Controlled operation whose base is MultiRZ, which is marked as supporting parameter broadcasting, actually does support broadcasting.""" - par = np.array([0.25, 2.1, -0.42]) + par = pnp.array([0.25, 2.1, -0.42]) base = qml.MultiRZ(par, wires=wires) op = Controlled(base, "wire1") @@ -1288,13 +1538,13 @@ def test_controlled_multi_rz(self, wires): @pytest.mark.parametrize( "state_, num_wires", - [([1.0, 0.0], 1), ([0.5, -0.5j, 0.5, -0.5], 2), (np.ones(8) / np.sqrt(8), 3)], + [([1.0, 0.0], 1), ([0.5, -0.5j, 0.5, -0.5], 2), (pnp.ones(8) / pnp.sqrt(8), 3)], ) def test_controlled_qubit_state_vector(self, state_, num_wires): """Test that StatePrep, which is marked as supporting parameter broadcasting, actually does support broadcasting.""" - state = np.array([state_]) + state = pnp.array([state_]) base = qml.StatePrep(state, wires=list(range(num_wires))) op = Controlled(base, "wire1") @@ -1302,7 +1552,7 @@ def test_controlled_qubit_state_vector(self, state_, num_wires): qml.StatePrep.compute_decomposition(state, list(range(num_wires))) op.decomposition() - state = np.array([state_] * 3) + state = pnp.array([state_] * 3) base = qml.StatePrep(state, wires=list(range(num_wires))) op = Controlled(base, "wire1") assert op.batch_size == 3 @@ -1311,20 +1561,20 @@ def test_controlled_qubit_state_vector(self, state_, num_wires): @pytest.mark.parametrize( "state, num_wires", - [([1.0, 0.0], 1), ([0.5, -0.5j, 0.5, -0.5], 2), (np.ones(8) / np.sqrt(8), 3)], + [([1.0, 0.0], 1), ([0.5, -0.5j, 0.5, -0.5], 2), (pnp.ones(8) / pnp.sqrt(8), 3)], ) def test_controlled_amplitude_embedding(self, state, num_wires): """Test that AmplitudeEmbedding, which is marked as supporting parameter broadcasting, actually does support broadcasting.""" - features = np.array([state]) + features = pnp.array([state]) base = qml.AmplitudeEmbedding(features, wires=list(range(num_wires))) op = Controlled(base, "wire1") assert op.batch_size == 1 qml.AmplitudeEmbedding.compute_decomposition(features, list(range(num_wires))) op.decomposition() - features = np.array([state] * 3) + features = pnp.array([state] * 3) base = qml.AmplitudeEmbedding(features, wires=list(range(num_wires))) op = Controlled(base, "wire1") assert op.batch_size == 3 @@ -1334,9 +1584,9 @@ def test_controlled_amplitude_embedding(self, state, num_wires): @pytest.mark.parametrize( "angles, num_wires", [ - (np.array([[0.5], [2.1]]), 1), - (np.array([[0.5, -0.5], [0.2, 1.5]]), 2), - (np.ones((2, 5)), 5), + (pnp.array([[0.5], [2.1]]), 1), + (pnp.array([[0.5, -0.5], [0.2, 1.5]]), 2), + (pnp.ones((2, 5)), 5), ], ) def test_controlled_angle_embedding(self, angles, num_wires): @@ -1352,9 +1602,9 @@ def test_controlled_angle_embedding(self, angles, num_wires): @pytest.mark.parametrize( "features, num_wires", [ - (np.array([[0.5], [2.1]]), 1), - (np.array([[0.5, -0.5], [0.2, 1.5]]), 2), - (np.ones((2, 5)), 5), + (pnp.array([[0.5], [2.1]]), 1), + (pnp.array([[0.5, -0.5], [0.2, 1.5]]), 2), + (pnp.ones((2, 5)), 5), ], ) def test_controlled_iqp_embedding(self, features, num_wires): @@ -1375,9 +1625,9 @@ def test_controlled_iqp_embedding(self, features, num_wires): @pytest.mark.parametrize( "features, weights, num_wires, batch_size", [ - (np.array([[0.5], [2.1]]), np.array([[0.61], [0.3]]), 1, 2), - (np.array([[0.5, -0.5], [0.2, 1.5]]), np.ones((2, 4, 3)), 2, 2), - (np.array([0.5, -0.5, 0.2]), np.ones((3, 2, 6)), 3, 3), + (pnp.array([[0.5], [2.1]]), pnp.array([[0.61], [0.3]]), 1, 2), + (pnp.array([[0.5, -0.5], [0.2, 1.5]]), pnp.ones((2, 4, 3)), 2, 2), + (pnp.array([0.5, -0.5, 0.2]), pnp.ones((3, 2, 6)), 3, 3), ], ) def test_controlled_qaoa_embedding(self, features, weights, num_wires, batch_size): @@ -1393,213 +1643,268 @@ def test_controlled_qaoa_embedding(self, features, weights, num_wires, batch_siz op.decomposition() -##### TESTS FOR THE ctrl TRANSFORM ##### +custom_ctrl_ops = [ + (qml.PauliY(wires=0), [1], qml.CY(wires=[1, 0])), + (qml.PauliZ(wires=0), [1], qml.CZ(wires=[1, 0])), + (qml.RX(0.123, wires=0), [1], qml.CRX(0.123, wires=[1, 0])), + (qml.RY(0.123, wires=0), [1], qml.CRY(0.123, wires=[1, 0])), + (qml.RZ(0.123, wires=0), [1], qml.CRZ(0.123, wires=[1, 0])), + ( + qml.Rot(0.123, 0.234, 0.456, wires=0), + [1], + qml.CRot(0.123, 0.234, 0.456, wires=[1, 0]), + ), + (qml.PhaseShift(0.123, wires=0), [1], qml.ControlledPhaseShift(0.123, wires=[1, 0])), + ( + qml.QubitUnitary(np.array([[0, 1], [1, 0]]), wires=0), + [1, 2], + qml.ControlledQubitUnitary(np.array([[0, 1], [1, 0]]), wires=0, control_wires=[1, 2]), + ), +] -def test_invalid_input_error(): - """Test that a ValueError is raised upon invalid inputs.""" - err = r"The object 1 of type is not an Operator or callable." - with pytest.raises(ValueError, match=err): - qml.ctrl(1, control=2) +class TestCtrl: + """Tests for the ctrl transform.""" + + def test_invalid_input_error(self): + """Test that a ValueError is raised upon invalid inputs.""" + with pytest.raises(ValueError, match=r" is not an Operator or callable."): + qml.ctrl(1, control=2) + + @pytest.mark.parametrize("op, ctrl_wires, expected_op", custom_ctrl_ops) + def test_custom_controlled_ops(self, op, ctrl_wires, expected_op): + """Tests custom controlled operations are handled correctly.""" + assert qml.ctrl(op, control=ctrl_wires) == expected_op + + @pytest.mark.parametrize("op, ctrl_wires, _", custom_ctrl_ops) + def test_custom_controlled_ops_ctrl_on_zero(self, op, ctrl_wires, _): + """Tests custom controlled ops with control on zero are handled correctly.""" + + if isinstance(op, qml.QubitUnitary): + pytest.skip("ControlledQubitUnitary can accept any control values.") + + ctrl_values = [False] * len(ctrl_wires) + + if isinstance(op, Controlled): + expected = Controlled( + op.base, + control_wires=ctrl_wires + op.control_wires, + control_values=ctrl_values + op.control_values, + ) + else: + expected = Controlled(op, control_wires=ctrl_wires, control_values=ctrl_values) + + assert qml.ctrl(op, control=ctrl_wires, control_values=ctrl_values) == expected + + @pytest.mark.parametrize("op, ctrl_wires, _", custom_ctrl_ops) + def test_custom_controlled_ops_wrong_wires(self, op, ctrl_wires, _): + """Tests custom controlled ops with wrong number of wires are handled correctly.""" + + ctrl_wires = ctrl_wires + ["a", "b", "c"] + + if isinstance(op, qml.PhaseShift): + # TODO: remove this special case once ControlledGlobalPhase is implemented. + pytest.skip("PhaseShift has its temporary custom logic.") + if isinstance(op, qml.QubitUnitary): + pytest.skip("ControlledQubitUnitary can accept any number of control wires.") + elif isinstance(op, Controlled): + expected = Controlled( + op.base, + control_wires=ctrl_wires + op.control_wires, + ) + else: + expected = Controlled(op, control_wires=ctrl_wires) + + assert qml.ctrl(op, control=ctrl_wires) == expected + + def test_nested_controls(self): + """Tests that nested controls are flattened correctly.""" + + op = qml.ctrl( + Controlled( + Controlled(qml.S(wires=[0]), control_wires=[1]), + control_wires=[2], + control_values=[0], + ), + control=[3], + ) + expected = Controlled( + qml.S(wires=[0]), + control_wires=[3, 2, 1], + control_values=[1, 0, 1], + ) + assert op == expected + @pytest.mark.parametrize("op, ctrl_wires, ctrl_op", custom_ctrl_ops) + def test_nested_custom_controls(self, op, ctrl_wires, ctrl_op): + """Tests that nested controls of custom controlled ops are flattened correctly.""" -def test_ctrl_sanity_check(): - """Test that control works on a very standard usecase.""" + if isinstance(ctrl_op, qml.ControlledQubitUnitary): + pytest.skip("ControlledQubitUnitary has its own logic") - def make_ops(): - qml.RX(0.123, wires=0) - qml.RY(0.456, wires=2) - qml.RX(0.789, wires=0) - qml.Rot(0.111, 0.222, 0.333, wires=2) - qml.PauliX(wires=2) - qml.PauliY(wires=4) - qml.PauliZ(wires=0) + if isinstance(ctrl_op, qml.ControlledPhaseShift): + # TODO: remove this special case once ControlledGlobalPhase is implemented + pytest.skip("ControlledPhaseShift has its own logic") - with qml.queuing.AnnotatedQueue() as q_tape: - cmake_ops = ctrl(make_ops, control=1) - # Execute controlled version. - cmake_ops() + expected_base = op.base if isinstance(op, Controlled) else op + base_ctrl_wires = ( + ctrl_wires + op.control_wires if isinstance(op, Controlled) else ctrl_wires + ) + ctrl_values = [1] * len(ctrl_wires) + base_ctrl_values = ( + ctrl_values + op.control_values if isinstance(op, Controlled) else ctrl_values + ) - tape = QuantumScript.from_queue(q_tape) - expanded_tape = tape.expand() + op = qml.ctrl( + Controlled( + ctrl_op, + control_wires=["b"], + control_values=[0], + ), + control=["a"], + ) + expected = Controlled( + expected_base, + control_wires=["a", "b"] + base_ctrl_wires, + control_values=[1, 0] + base_ctrl_values, + ) + assert op == expected + + def test_nested_ctrl_qubit_unitaries(self): + """Tests that nested controlled qubit unitaries are flattened correctly.""" + + op = qml.ctrl( + Controlled( + qml.ControlledQubitUnitary( + np.array([[0, 1], [1, 0]]), control_wires=[1], wires=[0] + ), + control_wires=[2], + control_values=[0], + ), + control=[3], + ) + expected = qml.ControlledQubitUnitary( + np.array([[0, 1], [1, 0]]), control_wires=[3, 2, 1], wires=[0], control_values=[1, 0, 1] + ) + assert op == expected - expected = [ - *qml.CRX(0.123, wires=[1, 0]).decomposition(), - *qml.CRY(0.456, wires=[1, 2]).decomposition(), - *qml.CRX(0.789, wires=[1, 0]).decomposition(), - *qml.CRot(0.111, 0.222, 0.333, wires=[1, 2]).decomposition(), - qml.CNOT(wires=[1, 2]), - *qml.CY(wires=[1, 4]).decomposition(), - *qml.CZ(wires=[1, 0]).decomposition(), - ] - assert len(tape.operations) == 7 - for op1, op2 in zip(expanded_tape, expected): - assert qml.equal(op1, op2) - - -def test_adjoint_of_ctrl(): - """Test adjoint(ctrl(fn)) and ctrl(adjoint(fn))""" - - def my_op(a, b, c): - qml.RX(a, wires=2) - qml.RY(b, wires=3) - qml.RZ(c, wires=0) - - with qml.queuing.AnnotatedQueue() as q1: - cmy_op_dagger = qml.simplify(qml.adjoint(ctrl(my_op, 5))) - # Execute controlled and adjointed version of my_op. - cmy_op_dagger(0.789, 0.123, c=0.456) - - tape1 = QuantumScript.from_queue(q1) - with qml.queuing.AnnotatedQueue() as q2: - cmy_op_dagger = qml.simplify(ctrl(qml.adjoint(my_op), 5)) - # Execute adjointed and controlled version of my_op. - cmy_op_dagger(0.789, 0.123, c=0.456) - - tape2 = QuantumScript.from_queue(q2) - expected = [ - *qml.CRZ(4 * onp.pi - 0.456, wires=[5, 0]).decomposition(), - *qml.CRY(4 * onp.pi - 0.123, wires=[5, 3]).decomposition(), - *qml.CRX(4 * onp.pi - 0.789, wires=[5, 2]).decomposition(), - ] - for tape in [tape1.expand(depth=1), tape2.expand(depth=1)]: - for op1, op2 in zip(tape, expected): - assert qml.equal(op1, op2) - - -def test_nested_ctrl(): - """Test nested use of control""" - with qml.queuing.AnnotatedQueue() as q_tape: - CCS = ctrl(ctrl(qml.S, 7), 3) - CCS(wires=0) - tape = QuantumScript.from_queue(q_tape) - assert len(tape.operations) == 1 - op = tape.operations[0] - assert isinstance(op, Controlled) - new_tape = tape.expand(depth=2) - assert qml.equal(new_tape[0], Controlled(qml.ControlledPhaseShift(np.pi / 2, [7, 0]), [3])) - - -def test_multi_ctrl(): - """Test control with a list of wires.""" - with qml.queuing.AnnotatedQueue() as q_tape: - CCS = ctrl(qml.S, control=[3, 7]) - CCS(wires=0) - tape = QuantumScript.from_queue(q_tape) - assert len(tape.operations) == 1 - op = tape.operations[0] - assert isinstance(op, Controlled) - new_tape = tape.expand(depth=1) - assert qml.equal(new_tape[0], Controlled(qml.PhaseShift(np.pi / 2, 0), [3, 7])) - - -def test_ctrl_with_qnode(): - """Test ctrl works when in a qnode cotext.""" - dev = qml.device("default.qubit", wires=3) - - def my_ansatz(params): - qml.RY(params[0], wires=0) - qml.RY(params[1], wires=1) - qml.CNOT(wires=[0, 1]) - qml.RX(params[2], wires=1) - qml.RX(params[3], wires=0) - qml.CNOT(wires=[1, 0]) - - def controlled_ansatz(params): - qml.CRY(params[0], wires=[2, 0]) - qml.CRY(params[1], wires=[2, 1]) - qml.Toffoli(wires=[2, 0, 1]) - qml.CRX(params[2], wires=[2, 1]) - qml.CRX(params[3], wires=[2, 0]) - qml.Toffoli(wires=[2, 1, 0]) - - def circuit(ansatz, params): - qml.RX(np.pi / 4.0, wires=2) - ansatz(params) - return qml.state() - - params = [0.123, 0.456, 0.789, 1.345] - circuit1 = qml.qnode(dev)(partial(circuit, ansatz=ctrl(my_ansatz, 2))) - circuit2 = qml.qnode(dev)(partial(circuit, ansatz=controlled_ansatz)) - res1 = circuit1(params=params) - res2 = circuit2(params=params) - assert qml.math.allclose(res1, res2) - - -def test_ctrl_within_ctrl(): - """Test using ctrl on a method that uses ctrl.""" - - def ansatz(params): - qml.RX(params[0], wires=0) - ctrl(qml.PauliX, control=0)(wires=1) - qml.RX(params[1], wires=0) - - controlled_ansatz = ctrl(ansatz, 2) - - with qml.queuing.AnnotatedQueue() as q_tape: - controlled_ansatz([0.123, 0.456]) - - tape = QuantumScript.from_queue(q_tape) - tape = tape.expand(2, stop_at=lambda op: not isinstance(op, Controlled)) - - expected = [ - *qml.CRX(0.123, wires=[2, 0]).decomposition(), - qml.Toffoli(wires=[2, 0, 1]), - *qml.CRX(0.456, wires=[2, 0]).decomposition(), - ] - for op1, op2 in zip(tape, expected): - assert qml.equal(op1, op2) - - -def test_diagonal_ctrl(): - """Test ctrl on diagonal gates.""" - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(qml.DiagonalQubitUnitary, 1)(onp.array([-1.0, 1.0j]), wires=0) - tape = QuantumScript.from_queue(q_tape) - tape = tape.expand(3, stop_at=lambda op: not isinstance(op, Controlled)) - assert qml.equal( - tape[0], qml.DiagonalQubitUnitary(onp.array([1.0, 1.0, -1.0, 1.0j]), wires=[1, 0]) + @pytest.mark.parametrize( + "op, ctrl_wires, ctrl_values, expected_op", + [ + (qml.PauliX(wires=[0]), [1], [1], qml.CNOT([1, 0])), + ( + qml.PauliX(wires=[2]), + [0, 1], + [1, 1], + qml.Toffoli(wires=[0, 1, 2]), + ), + ( + qml.CNOT(wires=[1, 2]), + [0], + [1], + qml.Toffoli(wires=[0, 1, 2]), + ), + ( + qml.PauliX(wires=[0]), + [1], + [0], + qml.MultiControlledX(wires=[1, 0], control_values=[0], work_wires=["aux"]), + ), + ( + qml.PauliX(wires=[2]), + [0, 1], + [1, 0], + qml.MultiControlledX(wires=[0, 1, 2], control_values=[1, 0], work_wires=["aux"]), + ), + ( + qml.CNOT(wires=[1, 2]), + [0], + [0], + qml.MultiControlledX(wires=[0, 1, 2], control_values=[0, 1], work_wires=["aux"]), + ), + ( + qml.PauliX(wires=[3]), + [0, 1, 2], + [1, 1, 1], + qml.MultiControlledX(wires=[0, 1, 2, 3], work_wires=Wires("aux")), + ), + ( + qml.CNOT(wires=[2, 3]), + [0, 1], + [1, 1], + qml.MultiControlledX(wires=[0, 1, 2, 3], work_wires=Wires("aux")), + ), + ( + qml.Toffoli(wires=[1, 2, 3]), + [0], + [1], + qml.MultiControlledX(wires=[0, 1, 2, 3], work_wires=Wires("aux")), + ), + ], ) + def test_pauli_x_based_ctrl_ops(self, op, ctrl_wires, ctrl_values, expected_op): + """Tests that PauliX-based ops are handled correctly.""" + op = qml.ctrl(op, control=ctrl_wires, control_values=ctrl_values, work_wires=["aux"]) + assert op == expected_op + + def test_nested_pauli_x_based_ctrl_ops(self): + """Tests that nested PauliX-based ops are handled correctly.""" + + op = qml.ctrl( + Controlled( + qml.CNOT(wires=[1, 0]), + control_wires=[2], + control_values=[0], + ), + control=[3], + ) + expected = qml.MultiControlledX(wires=[3, 2, 1, 0], control_values=[1, 0, 1]) + assert op == expected + def test_controlled_phase_shift_special_logic(self): + """Tests special logic for PhaseShift""" -@pytest.mark.parametrize( - "M", - [ - qml.PauliX.compute_matrix(), - qml.PauliY.compute_matrix(), - qml.PauliZ.compute_matrix(), - qml.Hadamard.compute_matrix(), - np.array( - [ - [1 + 2j, -3 + 4j], - [3 + 4j, 1 - 2j], - ] + # TODO: remove this special case once ControlledGlobalPhase is implemented. + # Tests special logic that wraps the phase shift in a controlled version. + op1 = qml.ctrl(qml.PhaseShift(0.123, wires=2), control=[0, 1]) + expected = Controlled(qml.ControlledPhaseShift(0.123, wires=[1, 2]), control_wires=[0]) + assert op1 == expected + + op2 = qml.ctrl(qml.ControlledPhaseShift(0.123, wires=[1, 2]), control=[0]) + assert op2 == expected + + # Tests that special logic is not triggered when the control value is False. + op = qml.ctrl(qml.PhaseShift(0.123, wires=2), control=[0, 1], control_values=[True, False]) + expected = Controlled( + qml.PhaseShift(0.123, wires=[2]), control_wires=[0, 1], control_values=[True, False] ) - * 30**-0.5, - ], -) -def test_qubit_unitary(M): - """Test ctrl on QubitUnitary and ControlledQubitUnitary""" - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(qml.QubitUnitary, 1)(M, wires=0) + assert op == expected + + +class _Rot(Operation): + """A rotation operation that is not an instance of Rot + + Used to test the default behaviour of expanding tapes without custom handling + of custom controlled operators (bypass automatic simplification of controlled + Rot to CRot gates in decompositions). - tape = QuantumScript.from_queue(q_tape) - expected = qml.ControlledQubitUnitary(M, control_wires=1, wires=0) - assert equal_list(list(tape), expected) + """ - # causes decomposition into more basic operators - tape = tape.expand(3, stop_at=lambda op: not isinstance(op, Controlled)) - assert not equal_list(list(tape), expected) + @staticmethod + def compute_decomposition(*params, wires=None): + return qml.Rot.compute_decomposition(*params, wires=wires) + def decomposition(self): + return self.compute_decomposition(*self.parameters, wires=self.wires) -@pytest.mark.parametrize( - "M", + +unitaries = ( [ - pytest.param(qml.PauliX.compute_matrix(), marks=pytest.mark.xfail), - pytest.param(qml.PauliY.compute_matrix(), marks=pytest.mark.xfail), - pytest.param(qml.PauliZ.compute_matrix(), marks=pytest.mark.xfail), - pytest.param(qml.Hadamard.compute_matrix(), marks=pytest.mark.xfail), - np.array( + qml.PauliX.compute_matrix(), + qml.PauliY.compute_matrix(), + qml.PauliZ.compute_matrix(), + qml.Hadamard.compute_matrix(), + pnp.array( [ [1 + 2j, -3 + 4j], [3 + 4j, 1 - 2j], @@ -1608,146 +1913,288 @@ def test_qubit_unitary(M): * 30**-0.5, ], ) -def test_controlledqubitunitary(M): - """Test ctrl on ControlledQubitUnitary.""" - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(qml.ControlledQubitUnitary, 1)(M, control_wires=2, wires=0) - tape = QuantumScript.from_queue(q_tape) - # will immediately decompose according to selected decomposition algorithm - tape = tape.expand(3, stop_at=lambda op: not isinstance(op, Controlled)) - expected = qml.ControlledQubitUnitary(M, control_wires=[2, 1], wires=0).decomposition() - assert equal_list(list(tape), expected) +class TestTapeExpansionWithControlled: + """Tests expansion of tapes containing Controlled operations""" + + def test_ctrl_values_sanity_check(self): + """Test that control works with control values on a very standard usecase.""" + + def make_ops(): + qml.RX(0.123, wires=0) + qml.RY(0.456, wires=2) + qml.RX(0.789, wires=0) + qml.Rot(0.111, 0.222, 0.333, wires=2) + qml.PauliX(wires=2) + qml.PauliY(wires=4) + qml.PauliZ(wires=0) + + with qml.queuing.AnnotatedQueue() as q_tape: + ctrl(make_ops, control=1, control_values=0)() + + tape = QuantumScript.from_queue(q_tape) + expected = [ + qml.PauliX(wires=1), + *qml.CRX(0.123, wires=[1, 0]).decomposition(), + *qml.CRY(0.456, wires=[1, 2]).decomposition(), + *qml.CRX(0.789, wires=[1, 0]).decomposition(), + *qml.CRot(0.111, 0.222, 0.333, wires=[1, 2]).decomposition(), + qml.CNOT(wires=[1, 2]), + *qml.CY(wires=[1, 4]).decomposition(), + *qml.CZ(wires=[1, 0]).decomposition(), + qml.PauliX(wires=1), + ] + assert len(tape) == 9 + expanded = tape.expand(stop_at=lambda obj: not isinstance(obj, Controlled)) + assert expanded.circuit == expected + @pytest.mark.parametrize( + "op", + [ + qml.ctrl(qml.ctrl(_Rot, 7), 3), # nested control + qml.ctrl(_Rot, [3, 7]), # multi-wire control + ], + ) + def test_nested_ctrl(self, op, tol): + """Tests that nested controlled ops are expanded correctly""" -def test_no_control_defined(): - """Test a custom operation with no control transform defined.""" - # QFT has no control rule defined. - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(qml.templates.QFT, 2)(wires=[0, 1]) - tape = QuantumScript.from_queue(q_tape) - tape = tape.expand(depth=3, stop_at=lambda op: not isinstance(op, Controlled)) - assert len(tape.operations) == 8 - # Check that all operations are updated to their controlled version. - for op in tape.operations: - assert type(op) in {qml.ControlledPhaseShift, qml.Toffoli, qml.CRX, qml.CSWAP, qml.CH} + with qml.queuing.AnnotatedQueue() as q_tape: + op(0.1, 0.2, 0.3, wires=0) + tape = QuantumScript.from_queue(q_tape) + assert tape.expand(depth=1).circuit == [ + Controlled(qml.RZ(0.1, 0), control_wires=[3, 7]), + Controlled(qml.RY(0.2, 0), control_wires=[3, 7]), + Controlled(qml.RZ(0.3, 0), control_wires=[3, 7]), + ] -def test_decomposition_defined(): - """Test that a controlled gate that has no control transform defined, - and a decomposition transformed defined, still works correctly""" + # Tests that the decomposition of the nested controlled _Rot gate is ultimately + # equivalent to the decomposition of the controlled CRot + with qml.queuing.AnnotatedQueue() as q_tape: + for op_ in qml.CRot.compute_decomposition(0.1, 0.2, 0.3, wires=[7, 0]): + qml.ctrl(op_, control=3) + tape_expected = QuantumScript.from_queue(q_tape) - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(qml.CY, 0)(wires=[1, 2]) + def stopping_condition(o): + return not isinstance(o, Controlled) or not o.has_decomposition - tape = QuantumScript.from_queue(q_tape) - tape = tape.expand() + actual = tape.expand(depth=10, stop_at=stopping_condition) + expected = tape_expected.expand(depth=10, stop_at=stopping_condition) + actual_mat = qml.matrix(actual, wire_order=[3, 7, 0]) + expected_mat = qml.matrix(expected, wire_order=[3, 7, 0]) + assert qml.math.allclose(actual_mat, expected_mat, atol=tol, rtol=0) - assert len(tape.operations) == 2 + @pytest.mark.parametrize( + "op", + [ + qml.ctrl(qml.ctrl(qml.S, 7), 3), # nested control + qml.ctrl(qml.S, [3, 7]), # multi-wire control + ], + ) + def test_nested_ctrl_containing_phase_shift(self, op): + """Test that nested controlled ops are expanded correctly when phase shift is involved - assert tape.operations[0].name == "C(CRY)" - assert tape.operations[1].name == "C(S)" + The decomposition of S gate contains a PhaseShift. In the nested case, we do not want + to apply control to the expanded PhaseShift like how it is typically done for other + operations, because the decomposition of PhaseShift contains a GlobalPhase, the controlled + version of which we do not have handling for. + TODO: remove this special case once ControlledGlobalPhase is implemented. -def test_ctrl_template(): - """Test that a controlled template correctly expands - on a device that doesn't support it""" + """ - weights = np.ones([3, 2]) + with qml.queuing.AnnotatedQueue() as q_tape: + op(wires=0) - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(qml.templates.BasicEntanglerLayers, 0)(weights, wires=[1, 2]) + tape = QuantumScript.from_queue(q_tape) + assert tape.expand(depth=1).circuit == [ + Controlled(qml.ControlledPhaseShift(np.pi / 2, wires=[7, 0]), control_wires=[3]) + ] - tape = QuantumScript.from_queue(q_tape) - tape = expand_tape(tape, depth=2) - assert len(tape) == 9 - assert all(o.name in {"CRX", "Toffoli"} for o in tape.operations) + assert tape.expand(depth=2).circuit == [ + qml.ControlledPhaseShift(np.pi / 4, wires=[3, 7]), + qml.Toffoli(wires=[3, 7, 0]), + qml.ControlledPhaseShift(-np.pi / 4, wires=[3, 0]), + qml.Toffoli(wires=[3, 7, 0]), + qml.ControlledPhaseShift(np.pi / 4, wires=[3, 0]), + ] + def test_adjoint_of_ctrl(self): + """Tests that adjoint(ctrl(fn)) and ctrl(adjoint(fn)) are equivalent""" + + def my_op(a, b, c): + qml.RX(a, wires=2) + qml.RY(b, wires=3) + qml.RZ(c, wires=0) + + with qml.queuing.AnnotatedQueue() as q1: + # Execute controlled and adjoint version of my_op. + cmy_op_dagger = qml.simplify(qml.adjoint(ctrl(my_op, 5))) + cmy_op_dagger(0.789, 0.123, c=0.456) + tape1 = QuantumScript.from_queue(q1) + + with qml.queuing.AnnotatedQueue() as q2: + # Execute adjoint and controlled version of my_op. + cmy_op_dagger = qml.simplify(ctrl(qml.adjoint(my_op), 5)) + cmy_op_dagger(0.789, 0.123, c=0.456) + tape2 = QuantumScript.from_queue(q2) + + expected = [ + *qml.CRZ(4 * np.pi - 0.456, wires=[5, 0]).decomposition(), + *qml.CRY(4 * np.pi - 0.123, wires=[5, 3]).decomposition(), + *qml.CRX(4 * np.pi - 0.789, wires=[5, 2]).decomposition(), + ] + assert tape1.expand(depth=1).circuit == expected + assert tape2.expand(depth=1).circuit == expected + + def test_ctrl_with_qnode(self): + """Test ctrl works when in a qnode cotext.""" + dev = qml.device("default.qubit", wires=3) + + def my_ansatz(params): + qml.RY(params[0], wires=0) + qml.RY(params[1], wires=1) + qml.CNOT(wires=[0, 1]) + qml.RX(params[2], wires=1) + qml.RX(params[3], wires=0) + qml.CNOT(wires=[1, 0]) + + def controlled_ansatz(params): + qml.CRY(params[0], wires=[2, 0]) + qml.CRY(params[1], wires=[2, 1]) + qml.Toffoli(wires=[2, 0, 1]) + qml.CRX(params[2], wires=[2, 1]) + qml.CRX(params[3], wires=[2, 0]) + qml.Toffoli(wires=[2, 1, 0]) + + def circuit(ansatz, params): + qml.RX(pnp.pi / 4.0, wires=2) + ansatz(params) + return qml.state() + + params = [0.123, 0.456, 0.789, 1.345] + circuit1 = qml.qnode(dev)(partial(circuit, ansatz=ctrl(my_ansatz, 2))) + circuit2 = qml.qnode(dev)(partial(circuit, ansatz=controlled_ansatz)) + res1 = circuit1(params=params) + res2 = circuit2(params=params) + assert qml.math.allclose(res1, res2) + + def test_ctrl_within_ctrl(self): + """Test using ctrl on a method that uses ctrl.""" + + def ansatz(params): + qml.RX(params[0], wires=0) + ctrl(qml.PauliX, control=0)(wires=1) + qml.RX(params[1], wires=0) + + controlled_ansatz = ctrl(ansatz, 2) + + with qml.queuing.AnnotatedQueue() as q_tape: + controlled_ansatz([0.123, 0.456]) + + tape = QuantumScript.from_queue(q_tape) + assert tape.expand(1).circuit == [ + *qml.CRX(0.123, wires=[2, 0]).decomposition(), + *qml.Toffoli(wires=[2, 0, 1]).decomposition(), + *qml.CRX(0.456, wires=[2, 0]).decomposition(), + ] -def test_ctrl_template_and_operations(): - """Test that a combination of controlled templates and operations correctly expands - on a device that doesn't support it""" + @pytest.mark.parametrize("ctrl_values", [[0, 0], [0, 1], [1, 0], [1, 1]]) + def test_multi_ctrl_values(self, ctrl_values): + """Test control with a list of wires and control values.""" + + def expected_ops(ctrl_val): + exp_op = [] + ctrl_wires = [3, 7] + for i, j in enumerate(ctrl_val): + if not bool(j): + exp_op.append(qml.PauliX(ctrl_wires[i])) + exp_op.append(Controlled(qml.ControlledPhaseShift(pnp.pi / 2, [7, 0]), 3)) + for i, j in enumerate(ctrl_val): + if not bool(j): + exp_op.append(qml.PauliX(ctrl_wires[i])) + + return exp_op + + with qml.queuing.AnnotatedQueue() as q_tape: + ctrl(qml.S, control=[3, 7], control_values=ctrl_values)(wires=0) + tape = QuantumScript.from_queue(q_tape) + assert len(tape.operations) == 1 + op = tape.operations[0] + assert isinstance(op, Controlled) + new_tape = expand_tape(tape, 1) + assert equal_list(list(new_tape), expected_ops(ctrl_values)) - weights = np.ones([3, 2]) + def test_diagonal_ctrl(self): + """Test ctrl on diagonal gates.""" + with qml.queuing.AnnotatedQueue() as q_tape: + qml.ctrl(qml.DiagonalQubitUnitary, 1)(np.array([-1.0, 1.0j]), wires=0) + tape = QuantumScript.from_queue(q_tape) + tape = tape.expand(3, stop_at=lambda op: not isinstance(op, Controlled)) + assert tape[0] == qml.DiagonalQubitUnitary(np.array([1.0, 1.0, -1.0, 1.0j]), wires=[1, 0]) - def ansatz(weights, wires): - qml.PauliX(wires=wires[0]) - qml.templates.BasicEntanglerLayers(weights, wires=wires) + @pytest.mark.parametrize("M", unitaries) + def test_qubit_unitary(self, M): + """Test ctrl on QubitUnitary""" + with qml.queuing.AnnotatedQueue() as q_tape: + ctrl(qml.QubitUnitary, 1)(M, wires=0) - with qml.queuing.AnnotatedQueue() as q_tape: - ctrl(ansatz, 0)(weights, wires=[1, 2]) + tape = QuantumScript.from_queue(q_tape) + expected = qml.ControlledQubitUnitary(M, control_wires=1, wires=0) + assert equal_list(list(tape), expected) - tape = QuantumScript.from_queue(q_tape) - tape = tape.expand(depth=2, stop_at=lambda obj: not isinstance(obj, Controlled)) - assert len(tape.operations) == 10 - assert all(o.name in {"CNOT", "CRX", "Toffoli"} for o in tape.operations) + # causes decomposition into more basic operators + tape = tape.expand(3, stop_at=lambda op: not isinstance(op, Controlled)) + assert not equal_list(list(tape), expected) + @pytest.mark.parametrize("M", unitaries) + def test_controlled_qubit_unitary(self, M): + """Test ctrl on ControlledQubitUnitary.""" -custom_controlled_ops = [ # operators with their own controlled class - (qml.PauliX, 1, qml.CNOT), - (qml.PauliY, 1, qml.CY), - (qml.PauliZ, 1, qml.CZ), - (qml.PauliX, 2, qml.Toffoli), -] + with qml.queuing.AnnotatedQueue() as q_tape: + ctrl(qml.ControlledQubitUnitary, 1)(M, control_wires=2, wires=0) + tape = QuantumScript.from_queue(q_tape) + # will immediately decompose according to selected decomposition algorithm + tape = tape.expand(1, stop_at=lambda op: not isinstance(op, Controlled)) -class TestCtrlCustomOperator: - @pytest.mark.parametrize("op_cls, num_ctrl_wires, custom_op_cls", custom_controlled_ops) - def test_ctrl_custom_operators(self, op_cls, num_ctrl_wires, custom_op_cls): - """Test that ctrl returns operators with their own controlled class.""" - ctrl_wires = list(range(1, num_ctrl_wires + 1)) - op = op_cls(wires=0) - ctrl_op = qml.ctrl(op, control=ctrl_wires) - custom_op = custom_op_cls(wires=ctrl_wires + [0]) - assert qml.equal(ctrl_op, custom_op) - assert ctrl_op.name == custom_op.name - - @pytest.mark.parametrize("op_cls, _, custom_op_cls", custom_controlled_ops) - def test_no_ctrl_custom_operators_excess_wires(self, op_cls, _, custom_op_cls): - """Test that ctrl returns a `Controlled` class when there is an excess of control wires.""" - if op_cls is qml.PauliX: - pytest.skip("ctrl(PauliX) becomes MultiControlledX, not Controlled") - - control_wires = list(range(1, 6)) - op = op_cls(wires=0) - ctrl_op = qml.ctrl(op, control=control_wires) - expected = Controlled(op, control_wires=control_wires) - assert not isinstance(ctrl_op, custom_op_cls) - assert qml.equal(ctrl_op, expected) - - @pytest.mark.parametrize("op_cls, num_ctrl_wires, custom_op_cls", custom_controlled_ops) - def test_no_ctrl_custom_operators_control_values(self, op_cls, num_ctrl_wires, custom_op_cls): - """Test that ctrl returns a `Controlled` class when the control value is not `True`.""" - if op_cls is qml.PauliX: - pytest.skip("ctrl(PauliX) becomes MultiControlledX, not Controlled") - - ctrl_wires = list(range(1, num_ctrl_wires + 1)) - op = op_cls(wires=0) - ctrl_op = qml.ctrl(op, ctrl_wires, control_values=[False] * num_ctrl_wires) - expected = Controlled(op, ctrl_wires, control_values=[False] * num_ctrl_wires) - assert not isinstance(ctrl_op, custom_op_cls) - assert qml.equal(ctrl_op, expected) + expected = qml.ControlledQubitUnitary(M, control_wires=[1, 2], wires=0).decomposition() + assert tape.circuit == expected @pytest.mark.parametrize( - "control_wires,control_values,expected_values", + "op, params, depth, expected", [ - ([1], (False), "0"), - ([1, 2], (0, 1), "01"), - ([1, 2, 3], (True, True, True), "111"), - ([1, 2, 3], (True, True, False), "110"), - ([1, 2, 3], None, None), + (qml.templates.QFT, [], 2, 14), + (qml.templates.BasicEntanglerLayers, [pnp.ones([3, 2])], 1, 9), ], ) - def test_ctrl_PauliX_MultiControlledX(self, control_wires, control_values, expected_values): - """Tests that ctrl(PauliX) with 3+ control wires or Falsy control values make a MCX""" - with qml.queuing.AnnotatedQueue() as q: - op = qml.ctrl(qml.PauliX(0), control_wires, control_values=control_values) + def test_ctrl_templates(self, op, params, depth, expected): + """Test ctrl on two different templates.""" - expected = qml.MultiControlledX(wires=control_wires + [0], control_values=expected_values) - assert len(q) == 1 - assert qml.equal(op, expected) - assert qml.equal(q.queue[0], expected) + with qml.queuing.AnnotatedQueue() as q_tape: + ctrl(op, 2)(*params, wires=[0, 1]) + tape = QuantumScript.from_queue(q_tape) + expanded_tape = tape.expand(depth=depth) + assert len(expanded_tape.operations) == expected + + def test_ctrl_template_and_operations(self): + """Test that a combination of controlled templates and operations correctly expands + on a device that doesn't support it""" + + weights = pnp.ones([3, 2]) + + def ansatz(weights, wires): + qml.PauliX(wires=wires[0]) + qml.templates.BasicEntanglerLayers(weights, wires=wires) + + with qml.queuing.AnnotatedQueue() as q_tape: + ctrl(ansatz, 0)(weights, wires=[1, 2]) + + tape = QuantumScript.from_queue(q_tape) + tape = tape.expand(depth=1, stop_at=lambda obj: not isinstance(obj, Controlled)) + assert len(tape.operations) == 10 + assert all(o.name in {"CNOT", "CRX", "Toffoli"} for o in tape.operations) @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift", "finite-diff"]) @@ -1759,7 +2206,7 @@ def test_autograd(self, diff_method): """Test differentiation using autograd""" dev = qml.device("default.qubit", wires=2) - init_state = np.array([1.0, -1.0], requires_grad=False) / np.sqrt(2) + init_state = pnp.array([1.0, -1.0], requires_grad=False) / pnp.sqrt(2) @qml.qnode(dev, diff_method=diff_method) def circuit(b): @@ -1767,11 +2214,11 @@ def circuit(b): qml.ctrl(qml.RY, control=0)(b, wires=[1]) return qml.expval(qml.PauliX(0)) - b = np.array(0.123, requires_grad=True) + b = pnp.array(0.123, requires_grad=True) res = qml.grad(circuit)(b) - expected = np.sin(b / 2) / 2 + expected = pnp.sin(b / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) @pytest.mark.torch def test_torch(self, diff_method): @@ -1781,7 +2228,7 @@ def test_torch(self, diff_method): dev = qml.device("default.qubit", wires=2) init_state = torch.tensor( [1.0, -1.0], requires_grad=False, dtype=torch.complex128 - ) / np.sqrt(2) + ) / pnp.sqrt(2) @qml.qnode(dev, diff_method=diff_method) def circuit(b): @@ -1794,9 +2241,9 @@ def circuit(b): loss.backward() # pylint:disable=no-member res = b.grad.detach() - expected = np.sin(b.detach() / 2) / 2 + expected = pnp.sin(b.detach() / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) @pytest.mark.jax @pytest.mark.parametrize("jax_interface", ["auto", "jax", "jax-python"]) @@ -1813,16 +2260,16 @@ def test_jax(self, diff_method, jax_interface): @qml.qnode(dev, diff_method=diff_method, interface=jax_interface) def circuit(b): - init_state = onp.array([1.0, -1.0]) / onp.sqrt(2) + init_state = np.array([1.0, -1.0]) / np.sqrt(2) qml.StatePrep(init_state, wires=0) qml.ctrl(qml.RY, control=0)(b, wires=[1]) return qml.expval(qml.PauliX(0)) b = jnp.array(0.123) res = jax.grad(circuit)(b) - expected = np.sin(b / 2) / 2 + expected = pnp.sin(b / 2) / 2 - assert np.allclose(res, expected) + assert pnp.allclose(res, expected) @pytest.mark.tf def test_tf(self, diff_method): @@ -1830,7 +2277,7 @@ def test_tf(self, diff_method): import tensorflow as tf dev = qml.device("default.qubit", wires=2) - init_state = tf.constant([1.0, -1.0], dtype=tf.complex128) / np.sqrt(2) + init_state = tf.constant([1.0, -1.0], dtype=tf.complex128) / pnp.sqrt(2) @qml.qnode(dev, diff_method=diff_method) def circuit(b): @@ -1844,69 +2291,6 @@ def circuit(b): loss = circuit(b) res = tape.gradient(loss, b) - expected = np.sin(b / 2) / 2 - - assert np.allclose(res, expected) - - -def test_ctrl_values_sanity_check(): - """Test that control works with control values on a very standard usecase.""" - - def make_ops(): - qml.RX(0.123, wires=0) - qml.RY(0.456, wires=2) - qml.RX(0.789, wires=0) - qml.Rot(0.111, 0.222, 0.333, wires=2) - qml.PauliX(wires=2) - qml.PauliY(wires=4) - qml.PauliZ(wires=0) - - with qml.queuing.AnnotatedQueue() as q_tape: - cmake_ops = ctrl(make_ops, control=1, control_values=0) - # Execute controlled version. - cmake_ops() - - tape = QuantumScript.from_queue(q_tape) - expected = [ - qml.PauliX(wires=1), - *qml.CRX(0.123, wires=[1, 0]).decomposition(), - *qml.CRY(0.456, wires=[1, 2]).decomposition(), - *qml.CRX(0.789, wires=[1, 0]).decomposition(), - *qml.CRot(0.111, 0.222, 0.333, wires=[1, 2]).decomposition(), - qml.CNOT(wires=[1, 2]), - *qml.CY(wires=[1, 4]).decomposition(), - *qml.CZ(wires=[1, 0]).decomposition(), - qml.PauliX(wires=1), - ] - assert len(tape) == 9 - expanded = tape.expand(stop_at=lambda obj: not isinstance(obj, Controlled)) - for op1, op2 in zip(expanded, expected): - assert qml.equal(op1, op2) - - -@pytest.mark.parametrize("ctrl_values", [[0, 0], [0, 1], [1, 0], [1, 1]]) -def test_multi_ctrl_values(ctrl_values): - """Test control with a list of wires and control values.""" - - def expected_ops(ctrl_val): - exp_op = [] - ctrl_wires = [3, 7] - for i, j in enumerate(ctrl_val): - if not bool(j): - exp_op.append(qml.PauliX(ctrl_wires[i])) - exp_op.append(Controlled(qml.PhaseShift(np.pi / 2, 0), [3, 7])) - for i, j in enumerate(ctrl_val): - if not bool(j): - exp_op.append(qml.PauliX(ctrl_wires[i])) - - return exp_op - - with qml.queuing.AnnotatedQueue() as q_tape: - CCS = ctrl(qml.S, control=[3, 7], control_values=ctrl_values) - CCS(wires=0) - tape = QuantumScript.from_queue(q_tape) - assert len(tape.operations) == 1 - op = tape.operations[0] - assert isinstance(op, Controlled) - new_tape = expand_tape(tape, 1) - assert equal_list(list(new_tape), expected_ops(ctrl_values)) + expected = pnp.sin(b / 2) / 2 + + assert pnp.allclose(res, expected) diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index 0c64ce991ed..d0b2560c16f 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -14,8 +14,10 @@ """ Tests for the controlled decompositions. """ -import pytest +import itertools + +import pytest import numpy as np import pennylane as qml from pennylane.ops import ctrl_decomp_zyz @@ -28,6 +30,8 @@ _convert_to_su2, _bisect_compute_a, _bisect_compute_b, + _decompose_mcx_with_one_worker, + _decompose_mcx_with_many_workers, ) from pennylane.ops.op_math.controlled import ( Controlled, @@ -193,14 +197,14 @@ def test_trivial_ops_in_decomposition(self): decomp = ctrl_decomp_zyz(op, [1]) expected = [ qml.RZ(np.pi, wires=0), - qml.MultiControlledX(wires=[1, 0]), + qml.CNOT(wires=[1, 0]), qml.RZ(-np.pi / 2, wires=0), - qml.MultiControlledX(wires=[1, 0]), + qml.CNOT(wires=[1, 0]), qml.RZ(-np.pi / 2, wires=0), ] assert len(decomp) == 5 - assert all(qml.equal(o, e) for o, e in zip(decomp, expected)) + assert decomp == expected @pytest.mark.parametrize("test_expand", [False, True]) def test_zyz_decomp_no_control_values(self, test_expand): @@ -372,9 +376,7 @@ def test_decomposed_operators(self, op, tol): assert qml.equal(mcx1, op_seq[0]) assert qml.equal(mcx1, op_seq[4]) - mcx2 = qml.MultiControlledX( - control_wires=Wires([4, 5]), wires=Wires(0), work_wires=Wires([1, 2, 3]) - ) + mcx2 = qml.Toffoli(wires=[4, 5, 0]) assert qml.equal(mcx2, op_seq[2]) assert qml.equal(mcx2, op_seq[6]) @@ -664,6 +666,69 @@ def test_decomposition_matrix(self, op, control_wires, tol): assert np.allclose(res, expected, atol=tol, rtol=tol) +class TestMCXDecomposition: + @pytest.mark.parametrize("n_ctrl_wires", range(3, 6)) + def test_decomposition_with_many_workers(self, n_ctrl_wires): + """Test that the decomposed MultiControlledX gate performs the same unitary as the + matrix-based version by checking if U^dagger U applies the identity to each basis + state. This test focuses on the case where there are many work wires.""" + # pylint: disable=protected-access + control_wires = range(n_ctrl_wires) + target_wire = n_ctrl_wires + work_wires = range(n_ctrl_wires + 1, 2 * n_ctrl_wires + 1) + + dev = qml.device("default.qubit", wires=2 * n_ctrl_wires + 1) + + with qml.queuing.AnnotatedQueue() as q: + _decompose_mcx_with_many_workers(control_wires, target_wire, work_wires) + tape = qml.tape.QuantumScript.from_queue(q) + assert all(isinstance(op, qml.Toffoli) for op in tape.operations) + + @qml.qnode(dev) + def f(bitstring): + qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1)) + qml.MultiControlledX(wires=list(control_wires) + [target_wire]) + for op in tape.operations: + op.queue() + return qml.probs(wires=range(n_ctrl_wires + 1)) + + u = np.array( + [f(np.array(b)) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)] + ).T + assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1))) + + @pytest.mark.parametrize("n_ctrl_wires", range(3, 6)) + def test_decomposition_with_one_worker(self, n_ctrl_wires): + """Test that the decomposed MultiControlledX gate performs the same unitary as the + matrix-based version by checking if U^dagger U applies the identity to each basis + state. This test focuses on the case where there is one work wire.""" + + # pylint: disable=protected-access + control_wires = Wires(range(n_ctrl_wires)) + target_wire = n_ctrl_wires + work_wires = n_ctrl_wires + 1 + + dev = qml.device("default.qubit", wires=n_ctrl_wires + 2) + + with qml.queuing.AnnotatedQueue() as q: + _decompose_mcx_with_one_worker(control_wires, target_wire, work_wires) + tape = qml.tape.QuantumScript.from_queue(q) + tape = tape.expand(depth=1) + + @qml.qnode(dev) + def f(bitstring): + qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1)) + qml.MultiControlledX(wires=list(control_wires) + [target_wire]) + for op in tape.operations: + op.queue() + return qml.probs(wires=range(n_ctrl_wires + 1)) + + u = np.array( + [f(np.array(b)) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)] + ).T + assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1))) + + def test_ControlledQubitUnitary_has_decomposition_correct(): """Test that ControlledQubitUnitary reports has_decomposition=False if it is False""" U = qml.Toffoli(wires=[0, 1, 2]).matrix() diff --git a/tests/ops/op_math/test_controlled_ops.py b/tests/ops/op_math/test_controlled_ops.py index 2e46fa49623..ad325fcc292 100644 --- a/tests/ops/op_math/test_controlled_ops.py +++ b/tests/ops/op_math/test_controlled_ops.py @@ -15,8 +15,6 @@ Unit tests for Operators inheriting from ControlledOp. """ -import functools - import numpy as np import pytest from scipy.linalg import fractional_matrix_power @@ -695,215 +693,6 @@ def test_eigvals_tf(self, tol, op, params, expected_eigvals): ) -def _dot_broadcasted(a, b): - return np.einsum("...ij,...jk->...ik", a, b) - - -def _multi_dot_broadcasted(matrices): - return functools.reduce(_dot_broadcasted, matrices) - - -class TestDecompositions: - @pytest.mark.parametrize("phi", [0.432, np.array([0.1, 2.1])]) - def test_CRX(self, phi): - """Test the decomposition for CRX.""" - - ops1 = qml.CRX.compute_decomposition(phi, wires=[0, 1]) - ops2 = qml.CRX(phi, wires=(0, 1)).decomposition() - - expected_ops = [ - qml.RZ(np.pi / 2, 1), - qml.RY(phi / 2, 1), - qml.CNOT(wires=(0, 1)), - qml.RY(-phi / 2, 1), - qml.CNOT(wires=(0, 1)), - qml.RZ(-np.pi / 2, 1), - ] - - assert ops1 == expected_ops - assert ops2 == expected_ops - - @pytest.mark.parametrize("phi", [0.432, np.array([2.1, 0.2])]) - def test_CRY(self, phi): - """Test the decomposition for CRY.""" - - ops1 = qml.CRY.compute_decomposition(phi, wires=[0, 1]) - ops2 = qml.CRY(phi, wires=(0, 1)).decomposition() - - expected_ops = [ - qml.RY(phi / 2, 1), - qml.CNOT(wires=(0, 1)), - qml.RY(-phi / 2, 1), - qml.CNOT(wires=(0, 1)), - ] - - assert ops1 == expected_ops - assert ops2 == expected_ops - - @pytest.mark.parametrize("phi", [0.321, np.array([0.6, 2.1])]) - def test_CRZ(self, phi): - """Test the decomposition for CRZ.""" - - ops1 = qml.CRZ.compute_decomposition(phi, wires=[1, 0]) - ops2 = qml.CRZ(phi, wires=(1, 0)).decomposition() - - expected_ops = [ - qml.PhaseShift(phi / 2, wires=0), - qml.CNOT(wires=[1, 0]), - qml.PhaseShift(-phi / 2, wires=0), - qml.CNOT(wires=[1, 0]), - ] - - assert ops1 == expected_ops - assert ops2 == expected_ops - - @pytest.mark.parametrize("phi, theta, omega", [[0.5, 0.6, 0.7], [0.1, -0.4, 0.7], [-10, 5, -1]]) - def test_CRot(self, tol, phi, theta, omega): - """Tests that the decomposition of the CRot gate is correct""" - - op = qml.CRot(phi, theta, omega, wires=[0, 1]) - res = op.decomposition() - - mats = [] - for i in reversed(res): - if len(i.wires) == 1: - mats.append(np.kron(np.eye(2), i.matrix())) - else: - mats.append(i.matrix()) - - decomposed_matrix = np.linalg.multi_dot(mats) - assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) - - @pytest.mark.parametrize( - "phi, theta, omega", - [ - [np.array([0.1, 0.2]), np.array([-0.4, 2.19]), np.array([0.7, -0.7])], - [np.array([0.1, 0.2, 0.9]), -0.4, np.array([0.7, 0.0, -0.7])], - ], - ) - def test_CRot_broadcasted(self, tol, phi, theta, omega): - """Tests that the decomposition of the broadcasted CRot gate is correct""" - - op = qml.CRot(phi, theta, omega, wires=[0, 1]) - res = op.decomposition() - - mats = [] - for i in reversed(res): - mat = i.matrix() - if len(i.wires) == 1: - I = np.eye(2)[np.newaxis] if qml.math.ndim(mat) == 3 else np.eye(2) - mats.append(np.kron(I, mat)) - else: - mats.append(mat) - - decomposed_matrix = _multi_dot_broadcasted(mats) - assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) - - @pytest.mark.parametrize("phi", [-0.1, 0.2, 0.5]) - def test_controlled_phase_shift(self, phi): - """Tests that the ControlledPhaseShift calculates the correct decomposition""" - - op = qml.ControlledPhaseShift(phi, wires=[0, 2]) - decomp = op.decomposition() - - mats = [] - for i in reversed(decomp): - if i.wires.tolist() == [0]: - mats.append(np.kron(i.matrix(), np.eye(4))) - elif i.wires.tolist() == [1]: - mats.append(np.kron(np.eye(2), np.kron(i.matrix(), np.eye(2)))) - elif i.wires.tolist() == [2]: - mats.append(np.kron(np.eye(4), i.matrix())) - elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 1]: - mats.append(np.kron(i.matrix(), np.eye(2))) - elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 2]: - mats.append( - np.array( - [ - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 1, 0], - ] - ) - ) - - decomposed_matrix = np.linalg.multi_dot(mats) - lam = np.exp(1j * phi) - exp = np.array( - [ - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, lam, 0, 0], - [0, 0, 0, 0, 0, 0, 1, 0], - [0, 0, 0, 0, 0, 0, 0, lam], - ] - ) - - assert np.allclose(decomposed_matrix, exp) - - def test_controlled_phase_shift_broadcasted(self): - """Tests that the ControlledPhaseShift calculates the correct decomposition""" - - phi = np.array([-0.2, 4.2, 1.8]) - op = qml.ControlledPhaseShift(phi, wires=[0, 2]) - decomp = op.decomposition() - - mats = [] - for i in reversed(decomp): - mat = i.matrix() - eye = np.eye(2)[np.newaxis] if np.ndim(mat) == 3 else np.eye(2) - if i.wires.tolist() == [0]: - mats.append(np.kron(mat, np.kron(eye, eye))) - elif i.wires.tolist() == [1]: - mats.append(np.kron(eye, np.kron(mat, eye))) - elif i.wires.tolist() == [2]: - mats.append(np.kron(np.kron(eye, eye), mat)) - elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 1]: - mats.append(np.kron(mat, eye)) - elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 2]: - mats.append( - np.array( - [ - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 1, 0], - ] - ) - ) - - decomposed_matrix = _multi_dot_broadcasted(mats) - lam = np.exp(1j * phi) - exp = np.array([np.diag([1, 1, 1, 1, 1, el, 1, el]) for el in lam]) - - assert np.allclose(decomposed_matrix, exp) - - @pytest.mark.parametrize("ops, expected_ops", NON_PARAMETRIC_OPS_DECOMPOSITIONS) - def test_non_parametric_op_decompositions(self, ops, expected_ops, tol): - """Tests that decompositions of non-parametrized operations are correct""" - - op = ops(wires=[0, 1]) - decomps = op.decomposition() - decomposed_matrix = qml.matrix(op.decomposition)() - - for gate, expected in zip(decomps, expected_ops): - assert qml.equal(gate, expected, atol=tol, rtol=0) - - assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) - - def test_simplify_crot(): """Simplify CRot operations with different parameters.""" diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py index 975dc53f074..f47ecb7e61c 100644 --- a/tests/ops/op_math/test_pow_op.py +++ b/tests/ops/op_math/test_pow_op.py @@ -465,10 +465,11 @@ def test_simplify_method(self): def test_simplify_method_with_controlled_operation(self): """Test simplify method with controlled operation.""" pow_op = Pow(ControlledOp(base=qml.Hadamard(0), control_wires=1, id=3), z=3) - final_op = ControlledOp(base=qml.Hadamard(0), control_wires=1, id=3) + final_op = qml.CH([1, 0], id=3) simplified_op = pow_op.simplify() - assert isinstance(simplified_op, ControlledOp) + # TODO: uncomment this check when all controlled operations inherit from ControlledOp + # assert isinstance(simplified_op, ControlledOp) assert final_op.data == simplified_op.data assert final_op.wires == simplified_op.wires assert final_op.arithmetic_depth == simplified_op.arithmetic_depth diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 56edb2717c7..ae27f817581 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -574,13 +574,7 @@ def test_warning_depractation_controlwires( None, [0, 1, 2], "011", - "Length of control bit string must equal number of control wires.", - ), - ( - None, - [0, 1, 2], - [0, 1], - "Control values must be passed as a string.", + "Length of control values must equal number of control wires.", ), ( None, @@ -590,8 +584,7 @@ def test_warning_depractation_controlwires( ), ([0], None, "", "Must specify the wires where the operation acts on"), ([0, 1], 2, "ab", "String of control values can contain only '0' or '1'."), - ([0, 1], 2, "011", "Length of control bit string must equal number of control wires."), - ([0, 1], 2, [0, 1], "Control values must be passed as a string."), + ([0, 1], 2, "011", "Length of control values must equal number of control wires."), ([0, 1], [2, 3], "10", "MultiControlledX accepts a single target wire."), ], ) @@ -670,6 +663,11 @@ def circuit_pauli_x(): assert np.allclose(mpmct_state, pauli_x_state) + def test_decomposition_not_enough_wires(self): + """Test that the decomposition raises an error if the number of wires""" + with pytest.raises(ValueError, match="Wrong number of wires"): + qml.MultiControlledX.compute_decomposition((0,), control_values=[1]) + def test_decomposition_no_control_values(self): """Test decomposition has default control values of all ones.""" decomp1 = qml.MultiControlledX.compute_decomposition((0, 1, 2)) @@ -741,71 +739,6 @@ def circuit_pauli_x(): assert np.allclose(mpmct_state, pauli_x_state) - @pytest.mark.parametrize("n_ctrl_wires", range(3, 6)) - def test_decomposition_with_many_workers(self, n_ctrl_wires): - """Test that the decomposed MultiControlledX gate performs the same unitary as the - matrix-based version by checking if U^dagger U applies the identity to each basis - state. This test focuses on the case where there are many work wires.""" - # pylint: disable=protected-access - control_wires = range(n_ctrl_wires) - target_wire = n_ctrl_wires - work_wires = range(n_ctrl_wires + 1, 2 * n_ctrl_wires + 1) - - dev = qml.device("default.qubit", wires=2 * n_ctrl_wires + 1) - - with qml.queuing.AnnotatedQueue() as q: - qml.MultiControlledX._decomposition_with_many_workers( - control_wires, target_wire, work_wires - ) - tape = qml.tape.QuantumScript.from_queue(q) - assert all(isinstance(op, qml.Toffoli) for op in tape.operations) - - @qml.qnode(dev) - def f(bitstring): - qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1)) - qml.MultiControlledX(wires=list(control_wires) + [target_wire]) - for op in tape.operations: - op.queue() - return qml.probs(wires=range(n_ctrl_wires + 1)) - - u = np.array( - [f(np.array(b)) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)] - ).T - assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1))) - - @pytest.mark.parametrize("n_ctrl_wires", range(3, 6)) - def test_decomposition_with_one_worker(self, n_ctrl_wires): - """Test that the decomposed MultiControlledX gate performs the same unitary as the - matrix-based version by checking if U^dagger U applies the identity to each basis - state. This test focuses on the case where there is one work wire.""" - # pylint: disable=protected-access - control_wires = Wires(range(n_ctrl_wires)) - target_wire = n_ctrl_wires - work_wires = n_ctrl_wires + 1 - - dev = qml.device("default.qubit", wires=n_ctrl_wires + 2) - - with qml.queuing.AnnotatedQueue() as q: - qml.MultiControlledX._decomposition_with_one_worker( - control_wires, target_wire, work_wires - ) - tape = qml.tape.QuantumScript.from_queue(q) - tape = tape.expand(depth=1) - assert all(isinstance(op, (qml.Toffoli, qml.CNOT)) for op in tape.operations) - - @qml.qnode(dev) - def f(bitstring): - qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1)) - qml.MultiControlledX(wires=list(control_wires) + [target_wire]) - for op in tape.operations: - op.queue() - return qml.probs(wires=range(n_ctrl_wires + 1)) - - u = np.array( - [f(np.array(b)) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)] - ).T - assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1))) - def test_not_enough_workers(self): """Test that a ValueError is raised when more than 2 control wires are to be decomposed with no work wires supplied""" @@ -821,7 +754,8 @@ def test_not_unique_wires(self): control_target_wires = range(4) work_wires = range(2) with pytest.raises( - ValueError, match="The work wires must be different from the control and target wires" + ValueError, + match="Work wires must be different the control_wires and base operation wires.", ): qml.MultiControlledX(wires=control_target_wires, work_wires=work_wires) @@ -934,9 +868,9 @@ def test_compute_matrix_no_control_values(self): def test_repr(self): """Test ``__repr__`` method that shows ``control_values``""" wires = [0, 1, 2] - control_values = "01" + control_values = [False, True] op_repr = qml.MultiControlledX(wires=wires, control_values=control_values).__repr__() - assert op_repr == f'MultiControlledX(wires={wires}, control_values="{control_values}")' + assert op_repr == f"MultiControlledX(wires={wires}, control_values={control_values})" period_two_ops = ( diff --git a/tests/transforms/test_optimization/test_undo_swaps.py b/tests/transforms/test_optimization/test_undo_swaps.py index a5ff7ec33c5..7e9a1c87830 100644 --- a/tests/transforms/test_optimization/test_undo_swaps.py +++ b/tests/transforms/test_optimization/test_undo_swaps.py @@ -32,7 +32,7 @@ def test_transform_non_standard_operations(self): ops = [ qml.adjoint(qml.S(0)), qml.PauliRot(1.2, "XY", wires=(0, 2)), - qml.ctrl(qml.PauliX(0), 2, control_values=[0, 0]), + qml.ctrl(qml.PauliX(0), [2, 3], control_values=[0, 0]), qml.SWAP((0, 1)), ] @@ -42,7 +42,7 @@ def test_transform_non_standard_operations(self): expected_ops = [ qml.adjoint(qml.S(1)), qml.PauliRot(1.2, "XY", wires=(1, 2)), - qml.ctrl(qml.PauliX(1), 2, control_values=[0, 0]), + qml.ctrl(qml.PauliX(1), [2, 3], control_values=[0, 0]), ] assert len(batch) == 1 assert batch[0].shots == tape.shots