Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multi-controlled zyz #6042

Merged
merged 24 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7c72ce2
multiple controlled, trainable special unitary decomp
albi3ro Jul 24, 2024
217fa96
Update ctrl_decomp_zyz
maliasadi Jul 25, 2024
568aec2
Update _multi_controlled_zyz
maliasadi Jul 25, 2024
7dc7523
Update tests
maliasadi Jul 25, 2024
bfaed33
Raise an error for len(work_wires) > 1
maliasadi Jul 25, 2024
bb9a257
Merge branch 'master' into multicontrolled-zyz
maliasadi Jul 26, 2024
8111ed8
Update format
maliasadi Jul 26, 2024
27432b7
Update changelog
maliasadi Jul 26, 2024
b6e5d01
Add tests with multiple working wires
maliasadi Jul 26, 2024
9fa9d70
Update changelog
maliasadi Jul 30, 2024
5e52210
Merge with master
maliasadi Jul 30, 2024
7f74a25
Update docs
maliasadi Aug 2, 2024
43c72d6
Merge with master
maliasadi Aug 2, 2024
5710549
Merge branch 'master' into multicontrolled-zyz
maliasadi Aug 2, 2024
e951938
Apply suggestions from code review
maliasadi Aug 8, 2024
a004948
Merge branch 'master' into multicontrolled-zyz
maliasadi Aug 8, 2024
1558bf4
Merge with master
maliasadi Aug 9, 2024
aff595d
Update support
maliasadi Aug 20, 2024
65ea9ad
Apply suggestions from code reviews
maliasadi Aug 20, 2024
4d8dfda
Update test_controlled_decompositions.py
maliasadi Aug 20, 2024
f145db6
Merge branch 'master' into multicontrolled-zyz
maliasadi Aug 20, 2024
575b457
Merge branch 'master' into multicontrolled-zyz
maliasadi Aug 21, 2024
1b2e67a
Merge branch 'master' into multicontrolled-zyz
maliasadi Aug 21, 2024
e8fe649
Merge branch 'master' into multicontrolled-zyz
maliasadi Aug 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@

<h3>Improvements 🛠</h3>

* Added the decomposition of zyz for special unitaries with multiple control wires.
[(#6042)](https://github.com/PennyLaneAI/pennylane/pull/6042)
maliasadi marked this conversation as resolved.
Show resolved Hide resolved

* During experimental program capture, the qnode can now use closure variables.
[(#6052)](https://github.com/PennyLaneAI/pennylane/pull/6052)

Expand Down Expand Up @@ -269,8 +272,13 @@ This release contains contributions from (in alphabetical order):

Tarun Kumar Allamsetty,
Guillermo Alonso,
<<<<<<< HEAD
Ali Asadi,
Utkarsh Azad
=======
Utkarsh Azad,
Ahmed Darwish,
>>>>>>> 74543ed865706b5b3f700084819e657013a55c88
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
Astral Cai,
Yushao Chen,
Gabriel Bottrill,
Expand Down
4 changes: 2 additions & 2 deletions pennylane/ops/op_math/controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,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)
Expand All @@ -873,7 +873,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
Expand Down
133 changes: 94 additions & 39 deletions pennylane/ops/op_math/controlled_decompositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"""

from copy import copy
from typing import Optional

import numpy as np
import numpy.linalg as npl
Expand Down Expand Up @@ -134,11 +135,94 @@ 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 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-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 decomposition of special zyz with multiple control wires
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
# defined in Lemma 7.9 of https://arxiv.org/pdf/quant-ph/9503016
maliasadi marked this conversation as resolved.
Show resolved Hide resolved

# 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
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) <https://arxiv.org/abs/quant-ph/9503016>`_.

Args:
Expand Down Expand Up @@ -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()
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
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)
)
maliasadi marked this conversation as resolved.
Show resolved Hide resolved


def _ctrl_decomp_bisect_od(
Expand Down
20 changes: 6 additions & 14 deletions tests/ops/op_math/test_controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down Expand Up @@ -1151,14 +1152,6 @@ def test_decomposition_undefined(self):
with pytest.raises(DecompositionUndefinedError):
op.decomposition()

def test_global_phase_decomp_raises_warning(self):
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
"""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."""

Expand Down Expand Up @@ -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,
Expand Down
82 changes: 60 additions & 22 deletions tests/ops/op_math/test_controlled_decompositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,21 @@ 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),
qml.RZ(0.123, wires=0),
qml.Rot(0.123, 0.456, 0.789, wires=0),
]

unitary_ops = [
special_unitary_ops = [
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
qml.Hadamard(0),
qml.PauliZ(0),
maliasadi marked this conversation as resolved.
Show resolved Hide resolved
qml.S(0),
qml.PhaseShift(1.5, wires=0),
]

general_unitary_ops = [
qml.QubitUnitary(
np.array(
[
Expand All @@ -119,11 +114,11 @@ def test_invalid_num_controls(self):
qml.DiagonalQubitUnitary(np.array([1, -1]), wires=0),
]

@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_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)
Expand All @@ -143,7 +138,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"""
Expand Down Expand Up @@ -193,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
Expand Down Expand Up @@ -896,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)
Expand Down
Loading