From 7c72ce2f096911d08639d584103b26ae6039e301 Mon Sep 17 00:00:00 2001 From: albi3ro Date: Wed, 24 Jul 2024 12:03:56 -0400 Subject: [PATCH 01/12] multiple controlled, trainable special unitary decomp --- pennylane/ops/op_math/controlled.py | 5 +- .../ops/op_math/controlled_decompositions.py | 106 ++++++++++++------ 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index b065d99df74..3282f4a9906 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -19,6 +19,7 @@ from copy import copy from functools import wraps from inspect import signature +from typing import Optional import numpy as np from scipy import sparse @@ -788,7 +789,7 @@ def _decompose_custom_ops(op: Controlled) -> list["operation.Operator"]: return None -def _decompose_no_control_values(op: Controlled) -> list["operation.Operator"]: +def _decompose_no_control_values(op: Controlled) -> Optional[list["operation.Operator"]]: """Decompose without considering control values. Returns None if no decomposition.""" decomp = _decompose_custom_ops(op) @@ -798,7 +799,7 @@ def _decompose_no_control_values(op: Controlled) -> list["operation.Operator"]: 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) - return ctrl_decomp_zyz(op.base, op.control_wires) + return ctrl_decomp_zyz(op.base, op.control_wires, work_wires=op.work_wires) if not op.base.has_decomposition: return None diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 17b8405c0a8..ca047f7c00e 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -16,6 +16,7 @@ """ from copy import copy +from typing import Optional import numpy as np import numpy.linalg as npl @@ -134,7 +135,72 @@ def _bisect_compute_b(u: np.ndarray): return _param_su2(c, d, b, 0) -def ctrl_decomp_zyz(target_operation: Operator, control_wires: Wires): +# pylint: disable=too-many-arguments +def _multicontrolled_zyz( + phi, theta, omega, target_wire: Wires, control_wires: Wires, work_wires: Optional[Wires] = None +) -> list[Operator]: + decomp = [] + + cop_wires = (control_wires[-1], target_wire[0]) + + # Add A operator + if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0): + decomp.append(qml.CRZ(phi, wires=cop_wires)) + if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): + decomp.append(qml.CRY(theta / 2, wires=cop_wires)) + + decomp.append(qml.ctrl(qml.X(target_wire), control=control_wires[:-1], work_wires=work_wires)) + + # Add B operator + if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): + decomp.append(qml.CRY(-theta / 2, wires=cop_wires)) + if not qml.math.allclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0): + decomp.append(qml.CRZ(-(phi + omega) / 2, wires=cop_wires)) + + decomp.append(qml.ctrl(qml.X(target_wire), control=control_wires[:-1], work_wires=work_wires)) + + # Add C operator + if not qml.math.allclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0): + decomp.append(qml.CRZ((omega - phi) / 2, wires=cop_wires)) + + return decomp + + +# pylint: disable=too-many-arguments +def _single_control_zyz(phi, theta, omega, global_phase, target_wire, control_wires: Wires): + # We use the conditional statements to account when decomposition is ran within a queue + decomp = [] + # Add negative of global phase. Compare definition of qml.GlobalPhase and Ph(delta) from section 4.1 of Barenco et al. + if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): + decomp.append( + qml.ctrl(qml.GlobalPhase(phi=-global_phase, wires=target_wire), control=control_wires) + ) + # Add A operator + if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0): + decomp.append(qml.RZ(phi, wires=target_wire)) + if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): + decomp.append(qml.RY(theta / 2, wires=target_wire)) + + decomp.append(qml.ctrl(qml.X(target_wire), control=control_wires)) + + # Add B operator + if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): + decomp.append(qml.RY(-theta / 2, wires=target_wire)) + if not qml.math.allclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0): + decomp.append(qml.RZ(-(phi + omega) / 2, wires=target_wire)) + + decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) + + # Add C operator + if not qml.math.allclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0): + decomp.append(qml.RZ((omega - phi) / 2, wires=target_wire)) + + return decomp + + +def ctrl_decomp_zyz( + target_operation: Operator, control_wires: Wires, work_wires: Optional[Wires] = None +) -> list[Operator]: """Decompose the controlled version of a target single-qubit operation This function decomposes a controlled single-qubit target operation with one @@ -190,10 +256,6 @@ def decomp_circuit(op): f"got {target_operation.__class__.__name__}." ) control_wires = Wires(control_wires) - if len(control_wires) > 1: - raise ValueError( - f"The control_wires should be a single wire, instead got: {len(control_wires)} wires." - ) target_wire = target_operation.wires @@ -209,34 +271,14 @@ def decomp_circuit(op): _, global_phase = _convert_to_su2(qml.matrix(target_operation), return_global_phase=True) - # We use the conditional statements to account when decomposition is ran within a queue - decomp = [] - # Add negative of global phase. Compare definition of qml.GlobalPhase and Ph(delta) from section 4.1 of Barenco et al. - if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): - decomp.append( - qml.ctrl(qml.GlobalPhase(phi=-global_phase, wires=target_wire), control=control_wires) - ) - # Add A operator - if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0): - decomp.append(qml.RZ(phi, wires=target_wire)) - if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): - decomp.append(qml.RY(theta / 2, wires=target_wire)) - - decomp.append(qml.ctrl(qml.X(target_wire), control=control_wires)) - - # Add B operator - if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0): - decomp.append(qml.RY(-theta / 2, wires=target_wire)) - if not qml.math.allclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0): - decomp.append(qml.RZ(-(phi + omega) / 2, wires=target_wire)) - - decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) - - # Add C operator - if not qml.math.allclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0): - decomp.append(qml.RZ((omega - phi) / 2, wires=target_wire)) + if len(control_wires) > 1: + if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): + raise ValueError( + f"The control_wires should be a single wire, instead got: {len(control_wires)} wires." + ) + return _multicontrolled_zyz(phi, theta, omega, target_wire, control_wires, work_wires) - return decomp + return _single_control_zyz(phi, theta, omega, global_phase, target_wire, control_wires) def _ctrl_decomp_bisect_od( From 217fa96da5c01de6e52694c3f4fa97799e49f426 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:32:07 -0400 Subject: [PATCH 02/12] Update ctrl_decomp_zyz --- .../ops/op_math/controlled_decompositions.py | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index ca047f7c00e..79a258d050f 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -137,8 +137,24 @@ def _bisect_compute_b(u: np.ndarray): # pylint: disable=too-many-arguments def _multicontrolled_zyz( - phi, theta, omega, target_wire: Wires, control_wires: Wires, work_wires: Optional[Wires] = None + rot_angles, + global_phase, + target_wire: Wires, + control_wires: Wires, + work_wires: Optional[Wires] = None, ) -> list[Operator]: + # The decomposition of special zyz with multiple control wires + # defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016 + + if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): + raise ValueError( + f"The control_wires should be a single wire, instead got: {len(control_wires)} wires." + ) + + # Unpack the rotation angles + phi, theta, omega = rot_angles + + # We use the conditional statements to account when decomposition is ran within a queue decomp = [] cop_wires = (control_wires[-1], target_wire[0]) @@ -167,7 +183,12 @@ def _multicontrolled_zyz( # pylint: disable=too-many-arguments -def _single_control_zyz(phi, theta, omega, global_phase, target_wire, control_wires: Wires): +def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: Wires): + # The decomposition of special zyz with multiple control wires + # defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016 + + # Unpack the rotation angles + phi, theta, omega = rot_angles # We use the conditional statements to account when decomposition is ran within a queue decomp = [] # Add negative of global phase. Compare definition of qml.GlobalPhase and Ph(delta) from section 4.1 of Barenco et al. @@ -204,7 +225,8 @@ def ctrl_decomp_zyz( """Decompose the controlled version of a target single-qubit operation This function decomposes a controlled single-qubit target operation with one - single control using the decomposition defined in Lemma 4.3 and Lemma 5.1 of + single control using the decomposition defined in Lemma 4.3 and Lemma 5.1, + and multiple control using the decomposition defined in Lemma 7.9 of `Barenco et al. (1995) `_. Args: @@ -261,24 +283,19 @@ def decomp_circuit(op): if isinstance(target_operation, Operation): try: - phi, theta, omega = target_operation.single_qubit_rot_angles() + rot_angles = target_operation.single_qubit_rot_angles() except NotImplementedError: - phi, theta, omega = _get_single_qubit_rot_angles_via_matrix( - qml.matrix(target_operation) - ) + rot_angles = _get_single_qubit_rot_angles_via_matrix(qml.matrix(target_operation)) else: - phi, theta, omega = _get_single_qubit_rot_angles_via_matrix(qml.matrix(target_operation)) + rot_angles = _get_single_qubit_rot_angles_via_matrix(qml.matrix(target_operation)) _, global_phase = _convert_to_su2(qml.matrix(target_operation), return_global_phase=True) - if len(control_wires) > 1: - if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): - raise ValueError( - f"The control_wires should be a single wire, instead got: {len(control_wires)} wires." - ) - return _multicontrolled_zyz(phi, theta, omega, target_wire, control_wires, work_wires) - - return _single_control_zyz(phi, theta, omega, global_phase, target_wire, control_wires) + return ( + _multicontrolled_zyz(rot_angles, global_phase, target_wire, control_wires, work_wires) + if len(control_wires) > 1 + else _single_control_zyz(rot_angles, global_phase, target_wire, control_wires) + ) def _ctrl_decomp_bisect_od( From 568aec2f19f60238cb8bbbecc32f94dfe8963a55 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:44:34 -0400 Subject: [PATCH 03/12] Update _multi_controlled_zyz --- .../ops/op_math/controlled_decompositions.py | 13 ++++++++---- tests/ops/op_math/test_controlled.py | 20 ++++++------------- .../op_math/test_controlled_decompositions.py | 16 +++------------ 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 79a258d050f..ef6b9c39eb4 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -136,7 +136,7 @@ def _bisect_compute_b(u: np.ndarray): # pylint: disable=too-many-arguments -def _multicontrolled_zyz( +def _multi_controlled_zyz( rot_angles, global_phase, target_wire: Wires, @@ -148,7 +148,12 @@ def _multicontrolled_zyz( if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): raise ValueError( - f"The control_wires should be a single wire, instead got: {len(control_wires)} wires." + f"The global_phase should be zero, instead got: {global_phase}." + ) + + if len(work_wires) > 1: + raise ValueError( + f"The work_wires should be a single wire, instead got: {len(work_wires)} wires." ) # Unpack the rotation angles @@ -194,7 +199,7 @@ def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: Wi # Add negative of global phase. Compare definition of qml.GlobalPhase and Ph(delta) from section 4.1 of Barenco et al. if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): decomp.append( - qml.ctrl(qml.GlobalPhase(phi=-global_phase, wires=target_wire), control=control_wires) + qml.ctrl(qml.GlobalPhase(phi=-global_phase, wires=target_wire), control=control_wires) ) # Add A operator if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0): @@ -292,7 +297,7 @@ def decomp_circuit(op): _, global_phase = _convert_to_su2(qml.matrix(target_operation), return_global_phase=True) return ( - _multicontrolled_zyz(rot_angles, global_phase, target_wire, control_wires, work_wires) + _multi_controlled_zyz(rot_angles, global_phase, target_wire, control_wires, work_wires) if len(control_wires) > 1 else _single_control_zyz(rot_angles, global_phase, target_wire, control_wires) ) diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py index d73616a292d..bb9170e74fe 100644 --- a/tests/ops/op_math/test_controlled.py +++ b/tests/ops/op_math/test_controlled.py @@ -1053,13 +1053,14 @@ def test_non_differentiable_one_qubit_special_unitary(self): def test_differentiable_one_qubit_special_unitary(self): """Assert that a differentiable qubit special unitary uses the zyz decomposition.""" - op = qml.ctrl(qml.RZ(qml.numpy.array(1.2), 0), (1)) + op = qml.ctrl(qml.RZ(qml.numpy.array(1.2), 0), (1,2,3,4)) decomp = op.decomposition() - qml.assert_equal(decomp[0], qml.PhaseShift(qml.numpy.array(1.2 / 2), 0)) - qml.assert_equal(decomp[1], qml.CNOT(wires=(1, 0))) - qml.assert_equal(decomp[2], qml.PhaseShift(qml.numpy.array(-1.2 / 2), 0)) - qml.assert_equal(decomp[3], qml.CNOT(wires=(1, 0))) + assert qml.equal(decomp[0], qml.CRZ(qml.numpy.array(1.2), [4, 0])) + assert qml.equal(decomp[1], qml.MultiControlledX(wires=[1, 2, 3, 0])) + assert qml.equal(decomp[2], qml.CRZ(qml.numpy.array(-0.6), wires=[4, 0])) + assert qml.equal(decomp[3], qml.MultiControlledX(wires=[1, 2, 3, 0])) + assert qml.equal(decomp[4], qml.CRZ(qml.numpy.array(-0.6), wires=[4, 0])) decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)() assert qml.math.allclose(op.matrix(), decomp_mat) @@ -1151,14 +1152,6 @@ def test_decomposition_undefined(self): with pytest.raises(DecompositionUndefinedError): op.decomposition() - def test_global_phase_decomp_raises_warning(self): - """Test that ctrl(GlobalPhase).decomposition() raises a warning with more than one control.""" - op = qml.ctrl(qml.GlobalPhase(1.23), control=[0, 1]) - with pytest.warns( - UserWarning, match="Controlled-GlobalPhase currently decomposes to nothing" - ): - assert op.decomposition() == [] - def test_control_on_zero(self): """Test decomposition applies PauliX gates to flip any control-on-zero wires.""" @@ -1730,7 +1723,6 @@ def test_custom_controlled_ops_wrong_wires(self, op, ctrl_wires, _): if isinstance(op, qml.QubitUnitary): pytest.skip("ControlledQubitUnitary can accept any number of control wires.") - expected = None # to pass pylint(possibly-used-before-assignment) error elif isinstance(op, Controlled): expected = Controlled( op.base, diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index c8cd1e4e30c..9d9044cecef 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -102,25 +102,15 @@ def test_invalid_num_controls(self): qml.Rot(0.123, 0.456, 0.789, wires=0), ] - unitary_ops = [ + special_unitary_ops = [ qml.Hadamard(0), qml.PauliZ(0), qml.S(0), qml.PhaseShift(1.5, wires=0), - qml.QubitUnitary( - np.array( - [ - [-0.28829348 - 0.78829734j, 0.30364367 + 0.45085995j], - [0.53396245 - 0.10177564j, 0.76279558 - 0.35024096j], - ] - ), - wires=0, - ), - qml.DiagonalQubitUnitary(np.array([1, -1]), wires=0), ] @pytest.mark.parametrize("op", su2_ops + unitary_ops) - @pytest.mark.parametrize("control_wires", ([1], [2], [3])) + @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3])) def test_decomposition_circuit(self, op, control_wires, tol): """Tests that the controlled decomposition of a single-qubit operation behaves as expected in a quantum circuit""" @@ -143,7 +133,7 @@ def expected_circuit(): assert np.allclose(res, expected, atol=tol, rtol=0) - @pytest.mark.parametrize("control_wires", ([1], [2], [3])) + @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3])) def test_decomposition_circuit_gradient(self, control_wires, tol): """Tests that the controlled decomposition of a single-qubit operation behaves as expected in a quantum circuit""" From 7dc7523fbdb8f638d9970813adf09ce3490ac163 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:03:19 -0400 Subject: [PATCH 04/12] Update tests --- .../ops/op_math/controlled_decompositions.py | 2 +- .../op_math/test_controlled_decompositions.py | 66 +++++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index ef6b9c39eb4..6d84cf54e8c 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -151,7 +151,7 @@ def _multi_controlled_zyz( f"The global_phase should be zero, instead got: {global_phase}." ) - if len(work_wires) > 1: + if work_wires and len(work_wires) > 1: raise ValueError( f"The work_wires should be a single wire, instead got: {len(work_wires)} wires." ) diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index 9d9044cecef..dd7d6999e47 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -87,14 +87,6 @@ def test_invalid_op_error(self): ): _ = ctrl_decomp_zyz(qml.CNOT([0, 1]), [2]) - def test_invalid_num_controls(self): - """Tests that an error is raised when an invalid number of control wires is passed""" - with pytest.raises( - ValueError, - match="The control_wires should be a single wire, instead got: 2", - ): - _ = ctrl_decomp_zyz(qml.X([1]), [0, 1]) - su2_ops = [ qml.RX(0.123, wires=0), qml.RY(0.123, wires=0), @@ -109,11 +101,24 @@ def test_invalid_num_controls(self): qml.PhaseShift(1.5, wires=0), ] - @pytest.mark.parametrize("op", su2_ops + unitary_ops) - @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3])) - def test_decomposition_circuit(self, op, control_wires, tol): + general_unitary_ops = [ + qml.QubitUnitary( + np.array( + [ + [-0.28829348 - 0.78829734j, 0.30364367 + 0.45085995j], + [0.53396245 - 0.10177564j, 0.76279558 - 0.35024096j], + ] + ), + wires=0, + ), + qml.DiagonalQubitUnitary(np.array([1, -1]), wires=0), + ] + + @pytest.mark.parametrize("op", su2_ops + special_unitary_ops + general_unitary_ops) + @pytest.mark.parametrize("control_wires", ([1], [2], [3])) + def test_decomposition_circuit_general_ops(self, op, control_wires, tol): """Tests that the controlled decomposition of a single-qubit operation - behaves as expected in a quantum circuit""" + behaves as expected in a quantum circuit for general_unitary_ops""" dev = qml.device("default.qubit", wires=4) @qml.qnode(dev) @@ -133,6 +138,25 @@ def expected_circuit(): assert np.allclose(res, expected, atol=tol, rtol=0) + @pytest.mark.parametrize("op", general_unitary_ops) + @pytest.mark.parametrize("control_wires", ([1, 2],[1,2,3])) + def test_decomposition_circuit_general_ops_error(self, op, control_wires, tol): + """Tests that the controlled decomposition of a single-qubit operation + with multiple controlled wires raises a ValueError for general_unitary_ops""" + dev = qml.device("default.qubit", wires=4) + + @qml.qnode(dev) + def decomp_circuit(): + qml.broadcast(unitary=qml.Hadamard, pattern="single", wires=control_wires) + ctrl_decomp_zyz(op, Wires(control_wires)) + return qml.probs() + + with pytest.raises( + ValueError, + match="The global_phase should be zero", + ): + decomp_circuit() + @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3])) def test_decomposition_circuit_gradient(self, control_wires, tol): """Tests that the controlled decomposition of a single-qubit operation @@ -183,23 +207,23 @@ def test_correct_decomp(self): """Test that the operations in the decomposition are correct.""" phi, theta, omega = 0.123, 0.456, 0.789 op = qml.Rot(phi, theta, omega, wires=0) - control_wires = [1] + control_wires = [1, 2, 3] decomps = ctrl_decomp_zyz(op, Wires(control_wires)) expected_ops = [ - qml.RZ(0.123, wires=0), - qml.RY(0.456 / 2, wires=0), - qml.CNOT(wires=control_wires + [0]), - qml.RY(-0.456 / 2, wires=0), - qml.RZ(-(0.123 + 0.789) / 2, wires=0), - qml.CNOT(wires=control_wires + [0]), - qml.RZ((0.789 - 0.123) / 2, wires=0), + qml.CRZ(0.123, wires=[3, 0]), + qml.CRY(0.456 / 2, wires=[3, 0]), + qml.Toffoli(wires=control_wires[:-1] + [0]), + qml.CRY(-0.456 / 2, wires=[3, 0]), + qml.CRZ(-(0.123 + 0.789) / 2, wires=[3, 0]), + qml.Toffoli(wires=control_wires[:-1] + [0]), + qml.CRZ((0.789 - 0.123) / 2, wires=[3, 0]), ] for decomp_op, expected_op in zip(decomps, expected_ops): qml.assert_equal(decomp_op, expected_op) assert len(decomps) == 7 - @pytest.mark.parametrize("op", su2_ops + unitary_ops) + @pytest.mark.parametrize("op", su2_ops + special_unitary_ops + general_unitary_ops) @pytest.mark.parametrize("control_wires", ([1], [2], [3])) def test_decomp_queues_correctly(self, op, control_wires, tol): """Test that any incorrect operations aren't queued when using From bfaed330a4c3dd583917715d07e39588c1ace768 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:09:28 -0400 Subject: [PATCH 05/12] Raise an error for len(work_wires) > 1 --- pennylane/ops/op_math/controlled_decompositions.py | 8 +++----- tests/ops/op_math/test_controlled.py | 2 +- tests/ops/op_math/test_controlled_decompositions.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 6d84cf54e8c..4cbcd4ee04d 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -147,9 +147,7 @@ def _multi_controlled_zyz( # defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016 if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): - raise ValueError( - f"The global_phase should be zero, instead got: {global_phase}." - ) + raise ValueError(f"The global_phase should be zero, instead got: {global_phase}.") if work_wires and len(work_wires) > 1: raise ValueError( @@ -199,7 +197,7 @@ def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: Wi # Add negative of global phase. Compare definition of qml.GlobalPhase and Ph(delta) from section 4.1 of Barenco et al. if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): decomp.append( - qml.ctrl(qml.GlobalPhase(phi=-global_phase, wires=target_wire), control=control_wires) + qml.ctrl(qml.GlobalPhase(phi=-global_phase, wires=target_wire), control=control_wires) ) # Add A operator if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0): @@ -215,7 +213,7 @@ def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: Wi if not qml.math.allclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0): decomp.append(qml.RZ(-(phi + omega) / 2, wires=target_wire)) - decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires)) + decomp.append(qml.ctrl(qml.X(target_wire), control=control_wires)) # Add C operator if not qml.math.allclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0): diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py index bb9170e74fe..d93d1723f4d 100644 --- a/tests/ops/op_math/test_controlled.py +++ b/tests/ops/op_math/test_controlled.py @@ -1053,7 +1053,7 @@ def test_non_differentiable_one_qubit_special_unitary(self): def test_differentiable_one_qubit_special_unitary(self): """Assert that a differentiable qubit special unitary uses the zyz decomposition.""" - op = qml.ctrl(qml.RZ(qml.numpy.array(1.2), 0), (1,2,3,4)) + op = qml.ctrl(qml.RZ(qml.numpy.array(1.2), 0), (1, 2, 3, 4)) decomp = op.decomposition() assert qml.equal(decomp[0], qml.CRZ(qml.numpy.array(1.2), [4, 0])) diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index dd7d6999e47..0dcf1f37b67 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -139,7 +139,7 @@ def expected_circuit(): assert np.allclose(res, expected, atol=tol, rtol=0) @pytest.mark.parametrize("op", general_unitary_ops) - @pytest.mark.parametrize("control_wires", ([1, 2],[1,2,3])) + @pytest.mark.parametrize("control_wires", ([1, 2], [1, 2, 3])) def test_decomposition_circuit_general_ops_error(self, op, control_wires, tol): """Tests that the controlled decomposition of a single-qubit operation with multiple controlled wires raises a ValueError for general_unitary_ops""" From 8111ed8dd4e199dd223aed6392949929b07ece66 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:57:10 -0400 Subject: [PATCH 06/12] Update format --- pennylane/ops/op_math/controlled_decompositions.py | 2 -- tests/ops/op_math/test_controlled_decompositions.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 4cbcd4ee04d..0d55f01988d 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -135,7 +135,6 @@ def _bisect_compute_b(u: np.ndarray): return _param_su2(c, d, b, 0) -# pylint: disable=too-many-arguments def _multi_controlled_zyz( rot_angles, global_phase, @@ -185,7 +184,6 @@ def _multi_controlled_zyz( return decomp -# pylint: disable=too-many-arguments def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: Wires): # The decomposition of special zyz with multiple control wires # defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016 diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index 0dcf1f37b67..b773e0f3b08 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -140,7 +140,7 @@ def expected_circuit(): @pytest.mark.parametrize("op", general_unitary_ops) @pytest.mark.parametrize("control_wires", ([1, 2], [1, 2, 3])) - def test_decomposition_circuit_general_ops_error(self, op, control_wires, tol): + def test_decomposition_circuit_general_ops_error(self, op, control_wires): """Tests that the controlled decomposition of a single-qubit operation with multiple controlled wires raises a ValueError for general_unitary_ops""" dev = qml.device("default.qubit", wires=4) From 27432b7ddae51145ea93ae789afb67cd62acfd57 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:58:54 -0400 Subject: [PATCH 07/12] Update changelog --- doc/releases/changelog-dev.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 6b99fcbe0c0..72d5358e429 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -33,6 +33,9 @@

Improvements 🛠

+* Added the decomposition of zyz for special unitaries with multiple control wires. + [(#6042)](https://github.com/PennyLaneAI/pennylane/pull/6042) + * Added the `compute_sparse_matrix` method for `qml.ops.qubit.BasisStateProjector`. [(#5790)](https://github.com/PennyLaneAI/pennylane/pull/5790) @@ -218,6 +221,7 @@ This release contains contributions from (in alphabetical order): Guillermo Alonso, +Ali Asadi, Utkarsh Azad Astral Cai, Yushao Chen, From b6e5d01cc05a0c9f7d1faa803828dcc709be81f6 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:04:24 -0400 Subject: [PATCH 08/12] Add tests with multiple working wires --- .../ops/op_math/controlled_decompositions.py | 5 ---- .../op_math/test_controlled_decompositions.py | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 0d55f01988d..48acca936db 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -148,11 +148,6 @@ def _multi_controlled_zyz( if not qml.math.allclose(0.0, global_phase, atol=1e-8, rtol=0): raise ValueError(f"The global_phase should be zero, instead got: {global_phase}.") - if work_wires and len(work_wires) > 1: - raise ValueError( - f"The work_wires should be a single wire, instead got: {len(work_wires)} wires." - ) - # Unpack the rotation angles phi, theta, omega = rot_angles diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index b773e0f3b08..8d8104099a3 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -910,6 +910,30 @@ def test_auto_select_wires(self, op, control_wires): res = _decompose_multicontrolled_unitary(op, Wires(control_wires)) assert equal_list(res, expected) + @pytest.mark.parametrize( + "op, controlled_wires, work_wires", + [ + (qml.RX(0.123, wires=1), [0, 2], [3, 4, 5]), + (qml.Rot(0.123, 0.456, 0.789, wires=0), [1, 2, 3], [4, 5]), + ], + ) + def test_with_many_workers(self, op, controlled_wires, work_wires): + """Tests ctrl_decomp_zyz with multiple workers""" + + dev = qml.device("default.qubit", wires=6) + + @qml.qnode(dev) + def decomp_circuit(op): + ctrl_decomp_zyz(op, controlled_wires, work_wires=work_wires) + return qml.probs() + + @qml.qnode(dev) + def expected_circuit(op): + qml.ctrl(op, controlled_wires, work_wires=work_wires) + return qml.probs() + + assert np.allclose(decomp_circuit(op), expected_circuit(op)) + controlled_wires = tuple(list(range(2, 1 + n)) for n in range(3, 7)) @pytest.mark.parametrize("op", gen_ops + su2_gen_ops) From 7f74a2590ac9774b1114af3ca16cae588c0fa924 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:43:36 -0400 Subject: [PATCH 09/12] Update docs --- doc/releases/changelog-dev.md | 4 ---- pennylane/ops/op_math/controlled_decompositions.py | 10 +++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 31da1912599..684aea739fa 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -272,13 +272,9 @@ This release contains contributions from (in alphabetical order): Tarun Kumar Allamsetty, Guillermo Alonso, -<<<<<<< HEAD Ali Asadi, -Utkarsh Azad -======= Utkarsh Azad, Ahmed Darwish, ->>>>>>> 74543ed865706b5b3f700084819e657013a55c88 Astral Cai, Yushao Chen, Gabriel Bottrill, diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index eaf31d7f739..6716c593456 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -142,7 +142,7 @@ def _multi_controlled_zyz( control_wires: Wires, work_wires: Optional[Wires] = None, ) -> list[Operator]: - # The decomposition of special zyz with multiple control wires + # The decomposition of zyz for special unitaries with multiple control wires # defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016 if not qml.math.allclose(0.0, global_phase, atol=1e-6, rtol=0): @@ -220,10 +220,10 @@ def ctrl_decomp_zyz( ) -> list[Operator]: """Decompose the controlled version of a target single-qubit operation - This function decomposes a controlled single-qubit target operation with one - single control using the decomposition defined in Lemma 4.3 and Lemma 5.1, - and multiple control using the decomposition defined in Lemma 7.9 of - `Barenco et al. (1995) `_. + This function decomposes both single and multiple controlled single-qubit + target operations using the decomposition defined in Lemma 4.3 and Lemma 5.1 + for single `controlled_wires`, Lemma 7.9 for multiple `controlled_wires` + from `Barenco et al. (1995) `_. Args: target_operation (~.operation.Operator): the target operation to decompose From e951938abf220cb22dbe75e00e20e1b20d3e774d Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:57:34 -0400 Subject: [PATCH 10/12] Apply suggestions from code review Co-authored-by: Cristian Emiliano Godinez Ramirez <57567043+EmilianoG-byte@users.noreply.github.com> --- pennylane/ops/op_math/controlled_decompositions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index 6716c593456..d42ba792faa 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -180,7 +180,7 @@ def _multi_controlled_zyz( def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: Wires): - # The decomposition of special zyz with multiple control wires + # The zyz decomposition of a general unitary with single control wire # defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016 # Unpack the rotation angles From 65ea9ada584aabca81898f2e5142356c34007e9d Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:49:27 -0400 Subject: [PATCH 11/12] Apply suggestions from code reviews --- Makefile | 12 +++--- .../ops/op_math/controlled_decompositions.py | 2 +- tests/ops/op_math/test_controlled.py | 38 ++++++++++++++++--- .../op_math/test_controlled_decompositions.py | 7 +--- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 5f7186e4549..7f36c24c698 100644 --- a/Makefile +++ b/Makefile @@ -70,17 +70,17 @@ coverage: .PHONY:format format: ifdef check - isort --py 311 --profile black -l 100 -o autoray -p ./pennylane --skip __init__.py --filter-files ./pennylane ./tests --check - black -t py39 -t py310 -t py311 -l 100 ./pennylane ./tests --check + $(PYTHON) -m isort --py 311 --profile black -l 100 -o autoray -p ./pennylane --skip __init__.py --filter-files ./pennylane ./tests --check + $(PYTHON) -m black -t py39 -t py310 -t py311 -l 100 ./pennylane ./tests --check else - isort --py 311 --profile black -l 100 -o autoray -p ./pennylane --skip __init__.py --filter-files ./pennylane ./tests - black -t py39 -t py310 -t py311 -l 100 ./pennylane ./tests + $(PYTHON) -m isort --py 311 --profile black -l 100 -o autoray -p ./pennylane --skip __init__.py --filter-files ./pennylane ./tests + $(PYTHON) -m black -t py39 -t py310 -t py311 -l 100 ./pennylane ./tests endif .PHONY: lint lint: - pylint pennylane --rcfile .pylintrc + $(PYTHON) -m pylint pennylane --rcfile .pylintrc .PHONY: lint-test lint-test: - pylint tests pennylane/devices/tests --rcfile tests/.pylintrc + $(PYTHON) -m pylint tests pennylane/devices/tests --rcfile tests/.pylintrc diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py index d42ba792faa..26de3761cf6 100644 --- a/pennylane/ops/op_math/controlled_decompositions.py +++ b/pennylane/ops/op_math/controlled_decompositions.py @@ -222,7 +222,7 @@ def ctrl_decomp_zyz( This function decomposes both single and multiple controlled single-qubit target operations using the decomposition defined in Lemma 4.3 and Lemma 5.1 - for single `controlled_wires`, Lemma 7.9 for multiple `controlled_wires` + for single `controlled_wires`, and Lemma 7.9 for multiple `controlled_wires` from `Barenco et al. (1995) `_. Args: diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py index d93d1723f4d..40e315449b1 100644 --- a/tests/ops/op_math/test_controlled.py +++ b/tests/ops/op_math/test_controlled.py @@ -1050,17 +1050,35 @@ def test_non_differentiable_one_qubit_special_unitary(self): decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)() assert qml.math.allclose(op.matrix(), decomp_mat) - def test_differentiable_one_qubit_special_unitary(self): - """Assert that a differentiable qubit special unitary uses the zyz decomposition.""" + def test_differentiable_one_qubit_special_unitary_single_ctrl(self): + """ + Assert that a differentiable qubit special unitary uses the zyz decomposition with a single controlled wire. + """ - op = qml.ctrl(qml.RZ(qml.numpy.array(1.2), 0), (1, 2, 3, 4)) + theta = 1.2 + op = qml.ctrl(qml.RZ(qml.numpy.array(theta), 0), (1)) decomp = op.decomposition() - assert qml.equal(decomp[0], qml.CRZ(qml.numpy.array(1.2), [4, 0])) + qml.assert_equal(decomp[0], qml.PhaseShift(qml.numpy.array(theta / 2), 0)) + qml.assert_equal(decomp[1], qml.CNOT(wires=(1, 0))) + qml.assert_equal(decomp[2], qml.PhaseShift(qml.numpy.array(-theta / 2), 0)) + qml.assert_equal(decomp[3], qml.CNOT(wires=(1, 0))) + + decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)() + assert qml.math.allclose(op.matrix(), decomp_mat) + + def test_differentiable_one_qubit_special_unitary_multiple_ctrl(self): + """Assert that a differentiable qubit special unitary uses the zyz decomposition with multiple controlled wires.""" + + theta = 1.2 + op = qml.ctrl(qml.RZ(qml.numpy.array(theta), 0), (1, 2, 3, 4)) + decomp = op.decomposition() + + assert qml.equal(decomp[0], qml.CRZ(qml.numpy.array(theta), [4, 0])) assert qml.equal(decomp[1], qml.MultiControlledX(wires=[1, 2, 3, 0])) - assert qml.equal(decomp[2], qml.CRZ(qml.numpy.array(-0.6), wires=[4, 0])) + assert qml.equal(decomp[2], qml.CRZ(qml.numpy.array(-theta / 2), wires=[4, 0])) assert qml.equal(decomp[3], qml.MultiControlledX(wires=[1, 2, 3, 0])) - assert qml.equal(decomp[4], qml.CRZ(qml.numpy.array(-0.6), wires=[4, 0])) + assert qml.equal(decomp[4], qml.CRZ(qml.numpy.array(-theta / 2), wires=[4, 0])) decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)() assert qml.math.allclose(op.matrix(), decomp_mat) @@ -1152,6 +1170,14 @@ def test_decomposition_undefined(self): with pytest.raises(DecompositionUndefinedError): op.decomposition() + def test_global_phase_decomp_raises_warning(self): + """Test that ctrl(GlobalPhase).decomposition() raises a warning with more than one control.""" + op = qml.ctrl(qml.GlobalPhase(1.23), control=[0, 1]) + with pytest.warns( + UserWarning, match="Controlled-GlobalPhase currently decomposes to nothing" + ): + assert op.decomposition() == [] + def test_control_on_zero(self): """Test decomposition applies PauliX gates to flip any control-on-zero wires.""" diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index 8d8104099a3..4064453ae3e 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -92,9 +92,6 @@ def test_invalid_op_error(self): qml.RY(0.123, wires=0), qml.RZ(0.123, wires=0), qml.Rot(0.123, 0.456, 0.789, wires=0), - ] - - special_unitary_ops = [ qml.Hadamard(0), qml.PauliZ(0), qml.S(0), @@ -114,7 +111,7 @@ def test_invalid_op_error(self): qml.DiagonalQubitUnitary(np.array([1, -1]), wires=0), ] - @pytest.mark.parametrize("op", su2_ops + special_unitary_ops + general_unitary_ops) + @pytest.mark.parametrize("op", su2_ops + general_unitary_ops) @pytest.mark.parametrize("control_wires", ([1], [2], [3])) def test_decomposition_circuit_general_ops(self, op, control_wires, tol): """Tests that the controlled decomposition of a single-qubit operation @@ -223,7 +220,7 @@ def test_correct_decomp(self): qml.assert_equal(decomp_op, expected_op) assert len(decomps) == 7 - @pytest.mark.parametrize("op", su2_ops + special_unitary_ops + general_unitary_ops) + @pytest.mark.parametrize("op", su2_ops + general_unitary_ops) @pytest.mark.parametrize("control_wires", ([1], [2], [3])) def test_decomp_queues_correctly(self, op, control_wires, tol): """Test that any incorrect operations aren't queued when using From 4d8dfda095d518b95a23ca35c018268c5077e1dc Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:41:22 -0400 Subject: [PATCH 12/12] Update test_controlled_decompositions.py --- tests/ops/op_math/test_controlled_decompositions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py index 4064453ae3e..6c1adda9f92 100644 --- a/tests/ops/op_math/test_controlled_decompositions.py +++ b/tests/ops/op_math/test_controlled_decompositions.py @@ -92,10 +92,6 @@ def test_invalid_op_error(self): qml.RY(0.123, wires=0), qml.RZ(0.123, wires=0), qml.Rot(0.123, 0.456, 0.789, wires=0), - qml.Hadamard(0), - qml.PauliZ(0), - qml.S(0), - qml.PhaseShift(1.5, wires=0), ] general_unitary_ops = [ @@ -109,6 +105,10 @@ def test_invalid_op_error(self): wires=0, ), qml.DiagonalQubitUnitary(np.array([1, -1]), wires=0), + qml.Hadamard(0), + qml.PauliZ(0), + qml.S(0), + qml.PhaseShift(1.5, wires=0), ] @pytest.mark.parametrize("op", su2_ops + general_unitary_ops)