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/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 1e313abe114..2084b0599e4 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -216,6 +216,9 @@
Other improvements
+* Added the decomposition of zyz for special unitaries with multiple control wires.
+ [(#6042)](https://github.com/PennyLaneAI/pennylane/pull/6042)
+
* A new method `process_density_matrix` has been added to the `ProbabilityMP` and `DensityMatrixMP`
classes, allowing for more efficient handling of quantum density matrices, particularly with batch
processing support. This method simplifies the calculation of probabilities from quantum states
@@ -395,6 +398,7 @@ This release contains contributions from (in alphabetical order):
Tarun Kumar Allamsetty,
Guillermo Alonso,
+Ali Asadi,
Utkarsh Azad,
Tonmoy T. Bhattacharya,
Gabriel Bottrill,
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
index df95dcd8342..4bc0690a9aa 100644
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -864,7 +864,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)
@@ -874,7 +874,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..26de3761cf6 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,12 +135,95 @@ def _bisect_compute_b(u: np.ndarray):
return _param_su2(c, d, b, 0)
-def ctrl_decomp_zyz(target_operation: Operator, control_wires: Wires):
+def _multi_controlled_zyz(
+ rot_angles,
+ global_phase,
+ target_wire: Wires,
+ control_wires: Wires,
+ work_wires: Optional[Wires] = None,
+) -> list[Operator]:
+ # 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):
+ raise ValueError(f"The global_phase should be zero, instead got: {global_phase}.")
+
+ # 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])
+
+ # 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
+
+
+def _single_control_zyz(rot_angles, global_phase, target_wire, control_wires: 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
+ 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.
+ 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.X(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
- single control using the decomposition defined in Lemma 4.3 and Lemma 5.1 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`, and Lemma 7.9 for multiple `controlled_wires`
+ from `Barenco et al. (1995) `_.
Args:
target_operation (~.operation.Operator): the target operation to decompose
@@ -190,53 +274,24 @@ 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
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)
- # 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
+ return (
+ _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)
+ )
def _ctrl_decomp_bisect_od(
diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py
index d73616a292d..40e315449b1 100644
--- a/tests/ops/op_math/test_controlled.py
+++ b/tests/ops/op_math/test_controlled.py
@@ -1050,20 +1050,39 @@ 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))
+ theta = 1.2
+ op = qml.ctrl(qml.RZ(qml.numpy.array(theta), 0), (1))
decomp = op.decomposition()
- qml.assert_equal(decomp[0], qml.PhaseShift(qml.numpy.array(1.2 / 2), 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(-1.2 / 2), 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(-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(-theta / 2), wires=[4, 0]))
+
+ 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,
@@ -1730,7 +1749,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..6c1adda9f92 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),
@@ -102,11 +94,7 @@ def test_invalid_num_controls(self):
qml.Rot(0.123, 0.456, 0.789, wires=0),
]
- unitary_ops = [
- qml.Hadamard(0),
- qml.PauliZ(0),
- qml.S(0),
- qml.PhaseShift(1.5, wires=0),
+ general_unitary_ops = [
qml.QubitUnitary(
np.array(
[
@@ -117,13 +105,17 @@ def test_invalid_num_controls(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 + unitary_ops)
+ @pytest.mark.parametrize("op", su2_ops + general_unitary_ops)
@pytest.mark.parametrize("control_wires", ([1], [2], [3]))
- def test_decomposition_circuit(self, op, control_wires, tol):
+ 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)
@@ -143,7 +135,26 @@ def expected_circuit():
assert np.allclose(res, expected, atol=tol, rtol=0)
- @pytest.mark.parametrize("control_wires", ([1], [2], [3]))
+ @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):
+ """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
behaves as expected in a quantum circuit"""
@@ -193,23 +204,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 + 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
@@ -896,6 +907,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)