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

Decomposition of arbitrary two-qubit unitaries. #1552

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
82ce28a
Initial commit.
Aug 18, 2021
00e7d39
Add scaffolding and helper function.
Aug 18, 2021
5e5f723
Temp commit.
Aug 19, 2021
53cd6dc
Progress!!
Aug 23, 2021
7420283
Update su2su2 implementation.
Aug 24, 2021
93e1a54
Temp commit.
Aug 24, 2021
3a7b920
Implementation that essentially works. Need testing.
Aug 27, 2021
9d603bc
Remove perms.
Aug 27, 2021
33acb87
Works about half the time.
Aug 30, 2021
7b75d65
Tidy a bit.
Aug 30, 2021
02e45eb
Small tweaks.
Aug 30, 2021
aee732e
Temporary work.
Aug 31, 2021
cf61a09
Working version, minus tests.
Aug 31, 2021
f3a133e
Fix compute matrix in tests. Add check for tensor product structure.
Aug 31, 2021
8a34c83
Move decomposition tests to decomposition.
Aug 31, 2021
81c1fb7
Update some tests. Add to docs.
Aug 31, 2021
2f66c2c
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
glassnotes Aug 31, 2021
285357b
Remove old test file.
Aug 31, 2021
e41ddbc
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
glassnotes Aug 31, 2021
83e617e
Run black.
Aug 31, 2021
8794e1e
Apply suggestions from code review
glassnotes Sep 1, 2021
954e69f
Make E matrix numpy constant.
Sep 1, 2021
a0bb0f0
Vectorize permutation matrix construction.
Sep 1, 2021
b91a2d6
Replace column negation with matrix multiplication.
Sep 1, 2021
d27e195
Fix decomposition to work in all interfaces. Fix tests to match.
Sep 1, 2021
206b79d
qml.math to math
Sep 1, 2021
f008f4e
Remove unneeded variables.
Sep 1, 2021
7cb8095
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
Sep 1, 2021
a3483d7
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
Sep 3, 2021
0a0f78e
Calculate the required number of CNOTs from tr(gammaU).
Sep 7, 2021
50d98c0
Split into multiple cases. 0, 2, and 3-CNOT cases are currently funct…
Sep 7, 2021
4b5fe9f
Add broken version.
Sep 8, 2021
ccfa975
Update with integration tests.
Sep 8, 2021
3815a6e
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
glassnotes Sep 8, 2021
aca20de
Improve documentation.
Sep 8, 2021
bf26a95
Run black.
Sep 8, 2021
ab18e19
Fix matrix ordering.
Sep 8, 2021
a1ce224
RUn black.
Sep 8, 2021
286eb47
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
josh146 Sep 9, 2021
272e2a3
Apply suggestions from code review
glassnotes Sep 9, 2021
d830767
Update error message. Change all qml.math to math.
Sep 9, 2021
2c27901
Fix tests.
Sep 9, 2021
d6139c0
Change error to warning.
Sep 9, 2021
ff657cf
Remove unused warnings.
Sep 9, 2021
fcb9981
Run black.
Sep 9, 2021
9da5930
Test CNOT number computation.
Sep 9, 2021
b35a844
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
glassnotes Sep 9, 2021
b238b02
Fix bug in zyz decomposition and tidy tests.
Sep 10, 2021
64aec27
Update CHANGELOG.
Sep 10, 2021
ed6d55f
Tweak comments.
Sep 10, 2021
0bd7c62
Add single-CNOT case.
Sep 10, 2021
4ca367d
Add testing for single-CNOT case.
Sep 10, 2021
46a4fbd
Separate out utility functions and clean up file.
Sep 10, 2021
58cce92
Cast to 1j to handle square root of negative values.
Sep 13, 2021
bda6a47
Partially working 2-CNOT case.
Sep 13, 2021
50206a2
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
Sep 14, 2021
06736f6
Revert qnode file.
Sep 14, 2021
cf4a456
Fix CHANGELOG.
Sep 14, 2021
c93a2c1
Fix CHANGELOG.
Sep 14, 2021
0b2ca09
Revert test files.
Sep 14, 2021
b40aeca
Revert test_qnode.
Sep 14, 2021
5ae6600
Run black.
Sep 14, 2021
ae22d8f
Temporary commit.
Sep 14, 2021
3b34879
Working 2-CNOT case!
Sep 14, 2021
cd38e2a
Run black.
Sep 14, 2021
5c9c97b
It works!
Sep 15, 2021
84e8e2d
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
Sep 15, 2021
82ff384
Fix test.
Sep 15, 2021
3ff4abd
Documentation and code factor fixes.
Sep 15, 2021
e6074cc
Tweak documentation.
Sep 15, 2021
ce5afaa
Merge branch 'master' into ch7103-implement-differentiable-two-qubit-…
josh146 Sep 16, 2021
d4c2dd1
Apply suggestions from code review
glassnotes Sep 16, 2021
b8b3c88
Move functions back from utils to module.
Sep 16, 2021
93a34fb
Make ASCII circuits in docstrings pretty.
Sep 16, 2021
13afd2b
Make single-qubit constant matrices top-level.
Sep 16, 2021
2441222
Rearrange CNOT check. Move matrix to top level.
Sep 16, 2021
26bb16c
Streamline conversion to SO(4).
Sep 16, 2021
6ff7bd8
Remove LAST_COL_NEG.
Sep 16, 2021
4f59371
Fix conj.
Sep 16, 2021
ec1eef8
Update docs and add developer notes.
Sep 16, 2021
e54ce46
Update CHANGELOG.
Sep 16, 2021
a32806a
Fix CHANGELOG.
Sep 16, 2021
3746937
Fix docstring.
Sep 16, 2021
19a6d04
Fix decomposition so expansion works.
Sep 16, 2021
8930f2c
Fix QMC test.
Sep 16, 2021
2bc2ed5
Fix transform so expand works.
Sep 16, 2021
8a2ec08
Remove unused import.
Sep 16, 2021
2c307e8
Add length checks.
Sep 16, 2021
587160d
Apply suggestions from code review
glassnotes Sep 16, 2021
695a27e
Merge branch 'ch7103-implement-differentiable-two-qubit-qubitunitary'…
Sep 16, 2021
63c11bb
Update changelog.
Sep 16, 2021
ce5510c
Remove parameter shift test case.
Sep 16, 2021
db45dc4
Tweak docstring.
Sep 16, 2021
36bf33b
Apply suggestions from code review
glassnotes Sep 16, 2021
45e76f2
Add SU(4) invalid check.
Sep 16, 2021
d47f894
Merge branch 'ch7103-implement-differentiable-two-qubit-qubitunitary'…
Sep 16, 2021
13832c3
Add test case for 3-qubit QubitUnitary in unitary_to_rot.
Sep 16, 2021
daaff43
Massive simplification.
Sep 16, 2021
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
813 changes: 813 additions & 0 deletions doc/_static/two_qubit_decomposition.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions pennylane/ops/qubit/matrix_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ def decomposition(U, wires):
decomp_ops = qml.transforms.decompositions.zyz_decomposition(U, wire)
return decomp_ops

if qml.math.shape(U) == (4, 4):
wires = Wires(wires)
decomp_ops = qml.transforms.decompositions.two_qubit_decomposition(U, wires)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josh146 so doing this here actually messes up the behaviour when tape.expand() is called, because of the order in which the operations are created (the centre part gets computed before the SU(2) stuff around). However if I call this with invisible around it, nothing happens during expand because nothing gets queue. It seems kind of weird to loop through the decomp_ops and queue them here, is there a different workaround for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, this is a really good question 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      if qml.math.shape(U) == (4, 4):
          wires = Wires(wires)

          decomp_ops = qml.transforms.invisible(
              qml.transforms.two_qubit_decomposition
          )(U, wires)

          current_tape = qml.tape.get_active_tape()

          if current_tape:
              for op in decomp_ops:
                  qml.apply(op, current_tape)

          return decomp_ops

Huh, this feels gross, but it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mind is going blank, all I can think about is

loop through the decomp_ops and queue them here

Also, it means that users won't be able to use qml.transforms.decompositions.two_qubit_decomposition(U, wires) directly either, since the order will be wrong.

I suppose what you could do is call the utility functions invisibly, inside two_qubit_decomposition, in order to generate a list of operators in the correct order. And then, right before returning, apply() each op in order? This is exactly the same as what you suggest above, just 'inside' two_qubit_decomposition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, this feels gross, but it works.

😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ What you suggest is the nicest. It further simplifies unitary_to_rot so that the 2-qubit case looks just like the 1-qubit case.

return decomp_ops
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍 This is super cool.

I must admit though, after writing the tracing ADR, everywhere I look I see things that won't be supported by tracing 😆 If we decide to go down that route, it means we will have to rethink a lot of the code base (or retrace any time argument shapes change).


raise NotImplementedError("Decompositions only supported for single-qubit unitaries")

def adjoint(self):
Expand Down
3 changes: 2 additions & 1 deletion pennylane/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
:toctree: api

~transforms.zyz_decomposition
~transforms.two_qubit_decomposition

Transforms that act on tapes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -106,7 +107,7 @@
from .classical_jacobian import classical_jacobian
from .compile import compile
from .control import ControlledOperation, ctrl
from .decompositions import zyz_decomposition
from .decompositions import zyz_decomposition, two_qubit_decomposition
from .draw import draw
from .hamiltonian_expand import hamiltonian_expand
from .invisible import invisible
Expand Down
1 change: 1 addition & 0 deletions pennylane/transforms/decompositions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
"""

from .single_qubit_unitary import zyz_decomposition
from .two_qubit_unitary import two_qubit_decomposition
346 changes: 346 additions & 0 deletions pennylane/transforms/decompositions/two_qubit_unitary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains transforms and helpers functions for decomposing arbitrary two-qubit
unitary operations into elementary gates.
"""
import pennylane as qml
from pennylane import numpy as np
from pennylane import math

from .single_qubit_unitary import zyz_decomposition

# This gate E is called the "magic basis". It can be used to convert between
# SO(4) and SU(2) x SU(2). For A in SO(4), E A E^\dag is in SU(2) x SU(2).
glassnotes marked this conversation as resolved.
Show resolved Hide resolved
E = qml.math.array([[1, 1j, 0, 0], [0, 0, 1j, 1], [0, 0, 1j, -1], [1, -1j, 0, 0]]) / np.sqrt(2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling that qml.math.array() simply dispatches to np.array? Would it make more sense to just use NumPy here, so that it is clear that these are non-differentiable constants?

Then you would be able to do

E = np.array([[1, 1j, 0, 0], [0, 0, 1j, 1], [0, 0, 1j, -1], [1, -1j, 0, 0]]) / np.sqrt(2)
Et = E.T
Edag = Et.conj()

Et = qml.math.T(E)
Edag = qml.math.conj(Et)

# Helpful to have static copies of these since they are needed in a few places.
CNOT01 = qml.CNOT(wires=[0, 1]).matrix
CNOT10 = qml.math.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]])
SWAP = qml.SWAP(wires=[0, 1]).matrix


def _perm_matrix_from_sequence(seq):
"""Construct a permutation matrix based on a provided permutation of integers.

This is used in the two-qubit unitary decomposition to permute a set of
simultaneous eigenvalues/vectors into the same order.
"""
mat = qml.math.zeros((4, 4))

for row_idx in range(4):
mat[row_idx, seq[row_idx]] = 1

return mat
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be vectorized for performance:

>>> seq = np.array([0, 1, 4, 3, 2])
>>> mat = np.identity(5)
>>> mat[:, seq]
array([[1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 1.],
       [0., 0., 0., 1., 0.],
       [0., 0., 1., 0., 0.]])

I think this is also a case where you can just use plain NumPy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sweet - when streamlined like this, it doesn't even need to be in its own function. Thanks!



def _convert_to_su4(U):
r"""Check unitarity of a 4x4 matrix and convert it to :math:`SU(4)` if possible.
glassnotes marked this conversation as resolved.
Show resolved Hide resolved

Args:
U (array[complex]): A matrix, presumed to be :math:`4 \times 4` and unitary.

Returns:
array[complex]: A :math:`4 \times 4` matrix in :math:`SU(4)` that is
equivalent to U up to a global phase.
"""
# Check unitarity
if not math.allclose(math.dot(U, math.T(math.conj(U))), math.eye(4), atol=1e-7):
raise ValueError("Operator must be unitary.")

# Compute the determinant
det = math.linalg.det(U)

# Convert to SU(4) if it's not close to 1
if not math.allclose(det, 1.0):
exp_angle = -1j * math.cast_like(math.angle(det), 1j) / 4
U = math.cast_like(U, det) * qml.math.exp(exp_angle)

return U


def _select_rotation_angles(U):
r"""Choose the rotation angles of RZ, RY in the two-qubit decomposition.
They are chosen as per Proposition V.1 in quant-ph/0308033 and are based
on the phases of the eigenvalues of :math:`E^\dagger \gamma(U) E`, where

.. math::

\gamma(U) = (E^\dag U E) (E^\dag U E)^T.
"""

gammaU = qml.math.linalg.multi_dot([Edag, U, E, Et, qml.math.T(U), qml.math.T(Edag)])
evs = qml.math.linalg.eigvals(gammaU)

# The rotation angles can be computed as follows (any three eigenvalues can be used)
x, y, z = qml.math.angle(evs[0]), qml.math.angle(evs[1]), qml.math.angle(evs[2])

# Compute functions of the eigenvalues; there are different options in v1
# vs. v3 of the paper, I'm not entirely sure why.
alpha = (x + y) / 2
beta = (x + z) / 2
delta = (z + y) / 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this from version 1 or 3?


return alpha, beta, delta


def _su2su2_to_tensor_products(U):
"""Given a matrix U = A \otimes B in SU(2) x SU(2), extract the two SU(2)
glassnotes marked this conversation as resolved.
Show resolved Hide resolved
operations A and B.

This process has been described in detail in the Appendix of Coffey & Deiotte
https://link.springer.com/article/10.1007/s11128-009-0156-3
"""

# First, write A = [[a1, a2], [-a2*, a1*]], which we can do for any SU(2) element.
# Then, A \otimes B = [[a1 B, a2 B], [-a2*B, a1*B]] = [[C1, C2], [C3, C4]]
# where the Ci are 2x2 matrices.
C1 = U[0:2, 0:2]
C2 = U[0:2, 2:4]
C3 = U[2:4, 0:2]
C4 = U[2:4, 2:4]

# From the definition of A \otimes B, C1 C4^\dag = a1^2 I, so we can extract a1
C14 = qml.math.dot(C1, qml.math.conj(qml.math.T(C4)))
a1 = qml.math.sqrt(C14[0, 0])

# Similarly, -C2 C3^\dag = a2^2 I, so we can extract a2
C23 = qml.math.dot(C2, qml.math.conj(qml.math.T(C3)))
a2 = qml.math.sqrt(-C23[0, 0])

# This gets us a1, a2 up to a sign. To resolve the sign, ensure that
# C1 C2^dag = a1 a2* I
C12 = qml.math.dot(C1, qml.math.conj(qml.math.T(C2)))

if not qml.math.isclose(a1 * np.conj(a2), C12[0, 0]):
a2 *= -1

# Construct A
A = qml.math.stack([[a1, a2], [-qml.math.conj(a2), qml.math.conj(a1)]])

# Next, extract B. Can do from any of the C, just need to be careful in
# case one of the elements of A is 0.
if not qml.math.isclose(A[0, 0], 0.0, atol=1e-8):
B = C1 / A[0, 0]
else:
B = C2 / A[0, 1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something that would cause issues if we support tracing 🤔

Could it be replaced with usage of qml.math.where? Minor though, happy to keep this.


return A, B


def _extract_su2su2_prefactors(U, V):
"""U, V are SU(4) matrices for which there exists A, B, C, D such that
glassnotes marked this conversation as resolved.
Show resolved Hide resolved
(A \otimes B) V (C \otimes D) = U. The problem is to find A, B, C, D in SU(2)
in an analytic and fully differentiable manner.

This decomposition is possible when U and V are in the same double coset of
SU(4), meaning there exists G, H in SO(4) s.t. G (Edag V E) H = (Edag U
E). This is guaranteed here by how V was constructed using the
_select_rotation_angles method. Then, we can use the fact that E SO(4) Edag
gives us something in SU(2) x SU(2) to give A, B, C, D.
"""

# A lot of the work here happens in the magic basis. Essentially, we
# don't look explicitly at some U = G V H, but rather at
# E^\dagger U E = G E^\dagger V E H
# so that we can recover
# U = (E G E^\dagger) V (E H E^\dagger) = (A \otimes B) V (C \otimes D).
# There is some math in the paper explaining how when we define U in this way,
# we can simultaneously diagonalize functions of U and V to ensure they are
# in the same coset and recover the decomposition.

u = qml.math.linalg.multi_dot([Edag, U, E])
v = qml.math.linalg.multi_dot([Edag, V, E])

uuT = qml.math.dot(u, qml.math.T(u))
vvT = qml.math.dot(v, qml.math.T(v))

# First, we find a matrix p (hopefully) in SO(4) s.t. p^T u u^T p is diagonal.
# Since uuT is complex and symmetric, both its real / imag parts share a set
# of real-valued eigenvectors.
ev_p, p = qml.math.linalg.eig(uuT.real)

# We also do this for v, i.e., find q (hopefully) in SO(4) s.t. q^T v v^T q is diagonal.
ev_q, q = qml.math.linalg.eig(vvT.real)
glassnotes marked this conversation as resolved.
Show resolved Hide resolved

# If determinant of p is not 1, it is in O(4) but not SO(4), and has
# determinant -1. We can transform it to SO(4) by simply negating one
# of the columns.
if not qml.math.isclose(qml.math.linalg.det(p), 1.0):
p[:, -1] = -p[:, -1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, in-place array element setting isn't supported in autograd and TF (but does work in Torch!).

This will have to be re-written to avoid the array assignment. One solution is to 'unstack' the p matrix using qml.math.unstack, negating the last column, and then restacking.

Maybe a more elegant way is to do a matrix multiplication, or even just scalar multiplication?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, should just be able to right-multiply by a matrix np.diag([1, 1, 1, -1]) (or something like this) 👍


# Next, we are going to reorder the columns of q so that the order of the
# eigenvalues matches those of p.
p_product = qml.math.linalg.multi_dot([qml.math.T(p), uuT, p])
q_product = qml.math.linalg.multi_dot([qml.math.T(q), vvT, q])

p_diag = qml.math.diag(p_product)
q_diag = qml.math.diag(q_product)

new_q_order = []

for idx, eigval in enumerate(p_diag):
are_close = [qml.math.isclose(x, eigval) for x in q_diag]

if any(are_close):
new_q_order.append(qml.math.argmax(are_close))

# Get the permutation matrix needed to reshuffle the columns
q_perm = _perm_matrix_from_sequence(new_q_order)
q = qml.math.linalg.multi_dot([q, qml.math.T(q_perm)])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, could this part be avoided by simply using indexing to do the re-ordering?

q = q[:, new_q_order]


# Depending on the sign of the permutation, it may be that q is in O(4) but
# not SO(4). Again we can fix this by simply negating a column.
q_in_so4 = qml.math.isclose(qml.math.linalg.det(q), 1.0)
if not q_in_so4:
q[:, -1] = -q[:, -1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue here: re array assignment


# Now, we should have p, q in SO(4) such that p^T u u^T p = q^T v v^T q.
# Then (v^\dag q p^T u)(v^\dag q p^T u)^T = I.
# So we can set G = p q^T, H = v^\dag q p^T u to obtain G v H = u.
G = qml.math.dot(p, qml.math.T(q))
H = qml.math.linalg.multi_dot([qml.math.conj(qml.math.T(v)), q, qml.math.T(p), u])

# These are still in SO(4) though - we want to convert things into SU(2) x SU(2)
# so use the entangler. Since u = E^\dagger U E and v = E^\dagger V E where U, V
# are the target matrices, we can reshuffle as in the docstring above,
# U = (E G E^\dagger) V (E H E^\dagger) = (A \otimes B) V (C \otimes D)
# where A, B, C, D are in SU(2) x SU(2).
AB = qml.math.linalg.multi_dot([E, G, Edag])
CD = qml.math.linalg.multi_dot([E, H, Edag])

# Now, we just need to extract the constituent tensor products.
A, B = _su2su2_to_tensor_products(AB)
C, D = _su2su2_to_tensor_products(CD)

return A, B, C, D


def two_qubit_decomposition(U, wires):
r"""Recover the decomposition of a two-qubit matrix :math:`U` in terms of
elementary operations.

The work of `Shende, Markov, and Bullock (2003)
<https://arxiv.org/abs/quant-ph/0308033>`__ presents a fixed-form
decomposition of :math:`U` in terms of single-qubit gates and
CNOTs. Multiple such decompositions are possible (by choosing two of
{``RX``, ``RY``, ``RZ``}). Here we choose the ``RY``, ``RZ`` case (fig. 2 in
glassnotes marked this conversation as resolved.
Show resolved Hide resolved
the above) to match with the default decomposition of the single-qubit
``Rot`` operations as ``RZ RY RZ``. The form of the decomposition is:

.. figure:: ../../_static/two_qubit_decomposition.svg
:align: center
:width: 100%
:target: javascript:void(0);

where :math:`A, B, C, D` are :math:`SU(2)` gates.

Args:
U (tensor): A 4 x 4 unitary matrix.
wires (Union[Wires, Sequence[int] or int]): The wires on which to apply the operation.

Returns:
list[qml.Operation]: A list of operations that represent the decomposition
glassnotes marked this conversation as resolved.
Show resolved Hide resolved
of the matrix U.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to add a (small) code example here!

"""
# First, test if we have a tensor product of two single-qubit operations. If
# so, we don't actually need to do a decomposition. To test this, we can
# check if Edag U E is in SO(4) because of the isomorphism between SO(4) and
# SU(2) x SU(2).
test_so4 = qml.math.linalg.multi_dot([Edag, U, E])
if qml.math.isclose(qml.math.linalg.det(test_so4), 1.0) and qml.math.allclose(
qml.math.dot(test_so4, qml.math.T(test_so4)), qml.math.eye(4)
):
A, B = _su2su2_to_tensor_products(U)
A_ops = zyz_decomposition(A, wires[0])
B_ops = zyz_decomposition(B, wires[1])
return A_ops + B_ops
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh this is awesome!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: tracing - this reminds me of one of those things we would lose if we removed the conditional. Classical performance improvement at the expense of a (potential) quantum reduction :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I've been finding that the algorithm as detailed below does not work for some special cases, and that's why I've separated this one out. (It also fails if you try to put something like CNOT in). I think it has to do with the number of CNOTs required (it is at most 3, but may be less). I might leave this as a future TODO to improve the transform, though, since I figure most applications of QubitUnitary are for cases where we have some strange SU(4) op we know little enough about that we are using QubitUnitary in the first place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, if you have a failing case, you could always add a test marked as xfail so that we have it for future dev purposes


# The final form of this decomposition is U = (A \otimes B) V (C \otimes D),
# as expressed in the circuit below.
# -U- = -C--X--RZ(d)--C---------X--A-|
# -U- = -D--C--RY(b)--X--RY(a)--C--B-|

# First, we note that this method works only for SU(4) gates, meaning that
# we need to rescale the matrix by its determinant. Furthermore, we add a
# SWAP as per v1 of 0308033, which helps with some rearranging of gates in
# the decomposition (it will cancel out the fact that we need to add a SWAP
# to fix the determinant in another part later).
swap_U = qml.math.exp(1j * np.pi / 4) * qml.math.dot(SWAP, _convert_to_su4(U))
glassnotes marked this conversation as resolved.
Show resolved Hide resolved

# Next, we can choose the angles of the RZ / RY rotations. See the docstring
# within the function used below. This is to ensure U and V somehow maintain
# a relationship between their spectra to ensure we can recover A, B, C, D.
alpha, beta, delta = _select_rotation_angles(swap_U)

# This is the interior portion of the decomposition circuit
interior_decomp = [
qml.CNOT(wires=[wires[1], wires[0]]),
qml.RZ(delta, wires=wires[0]),
qml.RY(beta, wires=wires[1]),
qml.CNOT(wires=[wires[0], wires[1]]),
qml.RY(alpha, wires=wires[1]),
qml.CNOT(wires=[wires[1], wires[0]]),
]

# We need the matrix representation of this interior part, V, in order to
# decompose U = (A \otimes B) V (C \otimes D)
#
# Looking at the decomposition above, V has determinant -1 (because there
# are 3 CNOTs, each with determinant -1). The relationship between U and V
# requires that both are in SU(4), so we add a SWAP after to V. We will see
# how this gets fixed later.
#
# -V- = -X--RZ(d)--C---------X--SWAP-|
# -V- = -C--RY(b)--X--RY(a)--C--SWAP-|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comments are so good it's almost like reading a demo 🤩


RZd = qml.RZ(delta, wires=0).matrix
RYb = qml.RY(beta, wires=0).matrix
RYa = qml.RY(alpha, wires=0).matrix

V = qml.math.linalg.multi_dot(
[
SWAP,
CNOT10,
qml.math.kron(qml.math.eye(2), RYa),
glassnotes marked this conversation as resolved.
Show resolved Hide resolved
CNOT01,
qml.math.kron(RZd, RYb),
CNOT10,
]
)

# Now we need to find the four SU(2) operations A, B, C, D
A, B, C, D = _extract_su2su2_prefactors(swap_U, V)

# At this point, we have the following:
# -U-SWAP- = --C--X-RZ(d)-C-------X-SWAP--A|
# -U-SWAP- = --D--C-RZ(b)-X-RY(a)-C-SWAP--B|
#
# Using the relationship that SWAP(A \otimes B) SWAP = B \otimes A,
# -U-SWAP- = --C--X-RZ(d)-C-------X--B--SWAP-|
# -U-SWAP- = --D--C-RZ(b)-X-RY(a)-C--A--SWAP-|
#
# Now the SWAPs cancel, giving us the desired decomposition
# (up to a global phase).
# -U- = --C--X-RZ(d)-C-------X--B--|
# -U- = --D--C-RZ(b)-X-RY(a)-C--A--|

A_ops = zyz_decomposition(A, wires[1])
B_ops = zyz_decomposition(B, wires[0])
C_ops = zyz_decomposition(C, wires[0])
D_ops = zyz_decomposition(D, wires[1])

# Return the full decomposition
return C_ops + D_ops + interior_decomp + A_ops + B_ops
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

Loading