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

Adds elementary gate decomposition for single-qubit QubitUnitary #1427

Merged
merged 45 commits into from
Jul 7, 2021

Conversation

glassnotes
Copy link
Contributor

@glassnotes glassnotes commented Jun 22, 2021

Context: Currently, there is no decomposition/synthesis for the arbitrary QubitUnitary operation.

Description of the Change: Adds the math needed to express arbitrary single-qubit matrices as a Rot gate (or an RZ gate, if they are diagonal).

Benefits: This should allow QubitUnitary to run on more devices. Furthermore, I think that by using the decomposition we can make QubitUnitary fully differentiable, at least for the single-qubit case (see example below).

Possible Drawbacks: None

Related GitHub Issues: None

Example:

import pennylane as qml
from pennylane import numpy as np

dev = qml.device('default.qubit', wires=1)

def qfunc(x):
    qml.Hadamard(wires=0)

    # Create a unitary matrix based on an input parameter
    Ux = np.array([[1, 0], [0, np.exp(1j * x)]])
    ops = qml.QubitUnitary.decomposition(Ux, wires=0)
    for op in ops:
        op.queue()

    return qml.expval(qml.PauliX(0))
>>> original_qnode = qml.QNode(qfunc, dev, diff_method="adjoint")
>>> print(qml.grad(original_qnode)(0.2))
-0.19866933079506127

@github-actions
Copy link
Contributor

Hello. You may have forgotten to update the changelog!
Please edit .github/CHANGELOG.md with:

  • A one-to-two sentence description of the change. You may include a small working example for new features.
  • A link back to this PR.
  • Your name (or GitHub username) in the contributors section.

Copy link
Member

@josh146 josh146 left a comment

Choose a reason for hiding this comment

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

@glassnotes this is awesome! A couple of things that crossed my mind:

  • This means that we can now support qml.Hermitian (arbitrary Hermitian matrix observables) directly on hardware, since it currently diagonalizes down to a QubitUnitary rotation 🙌

  • I love that it results in differentiability 🤯

  • In your code block in the PR comment, I don't think you need the op.queue()? Calling .decomposition should automatically queue the ops, so they might be being queued twice.

  • Part of me wonder if this feels like a compilation transform? Becuase there will be use cases where users want to force a unitary decomposition on a device that natively supports it. But I don't know? it would add overhead compared to simply calling QubitUnitary.decomposition() manually.

  • Don't forget to add interface tests! To ensure it works with different tensor input

  • Along the same lines, gradient tests should also be added 🙂

pennylane/ops/qubit.py Outdated Show resolved Hide resolved
pennylane/ops/qubit.py Outdated Show resolved Hide resolved
# if desired. If the top left element is 0, can only use the off-diagonal elements
if qml.math.isclose(U[0, 0], 0):
phi = 1j * qml.math.log(U[0, 1] / U[1, 0])
omega = -phi - 2 * qml.math.angle(U[1, 0])
Copy link
Member

Choose a reason for hiding this comment

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

You may need to be careful here:

  • phi is complex
  • 2 * angle(...) will be real (I think float64)

and TensorFlow (maybe also PyTorch?) will not automatically cast the latter to a complex number before doing the subtraction.

I think the following should work:

Suggested change
omega = -phi - 2 * qml.math.angle(U[1, 0])
omega = -phi - qml.math.cast_like(2 * qml.math.angle(U[1, 0]), phi)

Copy link
Member

@josh146 josh146 Jun 23, 2021

Choose a reason for hiding this comment

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

(Note: qml.math.cast(..., "complex128") would also work above, but cast_like is safer, in that it will support both complex64 and complex128 depending on whatever phi is)

Copy link
Contributor Author

@glassnotes glassnotes Jun 23, 2021

Choose a reason for hiding this comment

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

Oh thanks, yes this looks safer! I originally had the conversion to real in the line of phi rather than in the return value, do you think it'd be better to convert phi first so that the subtraction ends up real? Or is it better to just do the casting anyways?

Copy link
Member

Choose a reason for hiding this comment

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

I think you'd need the casting regardless, potentially you could have float64 subtract float32, which would also cause an error

omega = -phi - 2 * qml.math.angle(U[1, 0])
else:
omega = 1j * qml.math.log(qml.math.tan(theta / 2) * U[0, 0] / U[1, 0])
phi = -omega - 2 * qml.math.angle(U[0, 0])
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
phi = -omega - 2 * qml.math.angle(U[0, 0])
phi = -omega - qml.math.cast_like(2 * qml.math.angle(U[0, 0]), omega)

tests/ops/test_qubit_ops.py Outdated Show resolved Hide resolved
@glassnotes
Copy link
Contributor Author

Part of me wonder if this feels like a compilation transform? Becuase there will be use cases where users want to force a unitary decomposition on a device that natively supports it. But I don't know? it would add overhead compared to simply calling QubitUnitary.decomposition() manually.

I am wondering this too; so, for additional context, I'd like to do this for the 2-qubit case as well. That's going to make the decomposition method extremely hairy. If we use transforms, we can make things more flexible (e.g., we can have different transforms for ZYZ versus ZXZ decompositions), and better separate the 1- and 2-qubit cases from each other. Then we can have something that passes through the circuit, finds QubitUnitarys, and depending on the number of qubits calls the appropriate transform.

@josh146
Copy link
Member

josh146 commented Jun 23, 2021

I am wondering this too; so, for additional context, I'd like to do this for the 2-qubit case as well.

Oh! Maybe the answer is... both?

  • We have transforms to make it flexible and keep the code organized.
  • The QubitUnitary.decomposition() method calls the transforms as needed, in order to support devices where QubitUnitary is not defined.

@glassnotes
Copy link
Contributor Author

* We have transforms to make it flexible and keep the code organized.

* The `QubitUnitary.decomposition()` method calls the transforms as needed, in order to support devices where `QubitUnitary` is not defined.

I like it! I'll give it a go and we can see what it looks like. After deciding on that I'll make the rest of the tests.

@glassnotes glassnotes requested a review from josh146 June 25, 2021 17:25
Copy link
Member

@josh146 josh146 left a comment

Choose a reason for hiding this comment

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

I really like the new organization! Especially the fact that the decompositions can be done three ways:

  • Automatically if the device does not support it
  • Manually by calling qml.QubitUnitary.decomposition(U)
  • Manually on an existing qfunc via a transform

Comment on lines 2160 to 2168
@staticmethod
def decomposition(U, wires):
# Decompose arbitrary single-qubit unitaries as the form RZ RY RZ
if qml.math.shape(U)[0] == 2:
wire = Wires(wires)[0]
decomp_ops = qml.transforms.decompositions._zyz_decomposition(U, wire)
return decomp_ops

return NotImplementedError("Decompositions only supported for single-qubit unitaries")
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 really nice! In particular, I like how the logic is in the transforms package in its own module. This will make it much easier to modify and test for new contributors, rather than looking through the already large qubit.py file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, thanks for the suggestion to separate it out! 😁 It will also make it much cleaner to add different decompositions, one for 2-qubit unitaries, etc. while keeping the qubit ops file tidy.

pennylane/transforms/__init__.py Show resolved Hide resolved
pennylane/transforms/decompositions.py Outdated Show resolved Hide resolved
@@ -0,0 +1,114 @@
# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
Copy link
Member

Choose a reason for hiding this comment

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

Suggest renaming this file to better reflect the contents? Unless you envision multiple decompositions living here.

In the latter case, we could even have transforms/decompositions/single_qubit_unitary.py?

"""
# Check dimensions
if qml.math.shape(U)[0] != 2 or qml.math.shape(U)[1] != 2:
raise ValueError("Cannot convert matrix with shape {qml.math.shape(U)} to SU(2).")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise ValueError("Cannot convert matrix with shape {qml.math.shape(U)} to SU(2).")
raise ValueError(f"Cannot convert matrix with shape {qml.math.shape(U)} to SU(2).")

Copy link
Member

Choose a reason for hiding this comment

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

Minor, but might be worth saving this value once to avoid it being recomputed 3 times:

shape = qml.math.shape(U)

if shape != (2, 2):
    raise ValueError(f"Cannot convert matrix with shape {shape} to SU(2).")

Comment on lines 72 to 73
omega = 2 * qml.math.angle(U[1, 1])
return [qml.RZ(omega, wires=wire)]
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

Comment on lines 76 to 77
cos2_theta_over_2 = qml.math.abs(U[0, 0] * U[1, 1])
theta = 2 * qml.math.arccos(qml.math.sqrt(cos2_theta_over_2))
Copy link
Member

Choose a reason for hiding this comment

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

very very very minor, but you could have at the top of the file

from pennylane import math

which would then allow you to have

cos2_theta_over_2 = math.abs(U[0, 0] * U[1, 1])
theta = 2 * math.arccos(math.sqrt(cos2_theta_over_2))

which might be slightly nicer for readability?

Comment on lines 104 to 107
dim_U = qml.math.shape(op.parameters[0])[0]

if dim_U != 2:
continue
Copy link
Member

Choose a reason for hiding this comment

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

Since the unitary size is already taken into account here, it would be nice to disable the then redundant check in _zyz_decomposition 🤔

Copy link
Contributor Author

@glassnotes glassnotes Jun 29, 2021

Choose a reason for hiding this comment

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

Actually I think the checks here are the ones to disable since this checked in _zyz_decomposition via _convert_to_su2. I think the check needs to stay in there because that's what gets called by the QubitUnitary.decomposition method (which itself doesn't do a full check for unitarity).

Never mind this check should not be disabled. Which check were you referring to?

original_grad = qml.grad(original_qnode)(input)
transformed_grad = qml.grad(transformed_qnode)(input)

assert qml.math.allclose(original_grad, transformed_grad)
Copy link
Member

Choose a reason for hiding this comment

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

💪


original_grad = jax.grad(original_qnode)(input)
transformed_grad = jax.grad(transformed_qnode)(input)
assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7)
Copy link
Member

Choose a reason for hiding this comment

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

amazing testing 💯

Copy link
Member

@josh146 josh146 left a comment

Choose a reason for hiding this comment

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

All my comments are minor, as soon as the failing tests are fixed, I'm happy to approve!

pennylane/transforms/decompose_single_qubit_unitaries.py Outdated Show resolved Hide resolved


@qfunc_transform
def decompose_single_qubit_unitaries(tape):
Copy link
Member

Choose a reason for hiding this comment

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

You could even have

decompose_single_qubit_unitaries(tape, decomp=zyz_decomposition)

which would allow users to swap out their own decompositions if they want

Copy link
Member

Choose a reason for hiding this comment

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

A separate thought: I don't really like the name, but am going to be really annoying when I say I don't have any better suggestions 🤔

  • I find it a bit too long
  • I keep remembering that the compile PR uses 'expand' instead of 'decompose'

You could have @decompose_unitaries, and then have it as an implementation detail that only single qubit unitaries are supported, but that also doesn't feel right.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

decompose_single_qubit_unitaries(tape, decomp=zyz_decomposition)

Oh I like this idea!

A separate thought: I don't really like the name, but am going to be really annoying when I say I don't have any better suggestions

I find it a bit too long
I keep remembering that the compile PR uses 'expand' instead of 'decompose'

Yes I feel the same (about this and some of the other compilation transforms). Some other ideas:

  • decompose_qubit_unitaries
  • unitaries_to_rot / unitary_to_rot
  • qubit_unitary_to_rot

Copy link
Member

Choose a reason for hiding this comment

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

Oh I like unitary_to_rot

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Me too 👍 It's the shortest and also captures that it's for single-qubit operations 🎉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

decompose_single_qubit_unitaries(tape, decomp=zyz_decomposition)

Actually, if we are going to have the transform convert things to Rot, it makes sense to keep as-is rather than adding the option, since Rot is intrinsically zyz. But later if we add more decompositions and start returning them as RZ, RY, RZ, etc. instead of Rot, this would make things more flexible.

pennylane/transforms/decompose_single_qubit_unitaries.py Outdated Show resolved Hide resolved
pennylane/transforms/decompose_single_qubit_unitaries.py Outdated Show resolved Hide resolved
pennylane/transforms/decompose_single_qubit_unitaries.py Outdated Show resolved Hide resolved
Comment on lines 59 to 72
>>> dev = qml.device('default.qubit', wires=1)
>>> qnode = qml.QNode(qfunc, dev)
>>> print(qml.draw(qnode)())
0: ──U0──┤ ⟨Z⟩
U0 =
[[-0.17111489+0.58564875j -0.69352236-0.38309524j]
[ 0.25053735+0.75164238j 0.60700543-0.06171855j]]

We can use the transform to decompose the gate:

>>> transformed_qfunc = decompose_single_qubit_unitaries(qfunc)
>>> transformed_qnode = qml.QNode(transformed_qfunc, dev)
>>> print(qml.draw(transformed_qnode)())
0: ──Rot(-1.35, 1.83, -0.606)──┤ ⟨Z⟩
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 great!

@@ -0,0 +1,86 @@
# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
Copy link
Member

Choose a reason for hiding this comment

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

Very minor: What do you think about this transform also living in the decompositions/ directory? I'm on the fence, but curious to get your thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm inclined to leave it where it is to keep the actual transform (which could be used in a compilation pipeline) separate from the different decompositions, which are not transforms and have multiple use cases.

Comment on lines +14 to +15
r"""This module contains decompositions for (numerically-specified) arbitrary
unitary operations into sequences of elementary operations.
Copy link
Member

Choose a reason for hiding this comment

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

I can imagine this being really useful going forward! Both for devs, and external contributors that want to contribute new decompositions to PL without the overhead of having to learn the Operations class.

Co-authored-by: Josh Izaac <josh146@gmail.com>
@codecov
Copy link

codecov bot commented Jun 29, 2021

Codecov Report

Merging #1427 (070bc97) into master (8cb8d04) will decrease coverage by 0.00%.
The diff coverage is 96.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1427      +/-   ##
==========================================
- Coverage   98.24%   98.23%   -0.01%     
==========================================
  Files         160      163       +3     
  Lines       12027    12076      +49     
==========================================
+ Hits        11816    11863      +47     
- Misses        211      213       +2     
Impacted Files Coverage Δ
pennylane/ops/qubit.py 98.65% <85.71%> (-0.09%) ⬇️
pennylane/transforms/unitary_to_rot.py 91.66% <91.66%> (ø)
pennylane/transforms/__init__.py 100.00% <100.00%> (ø)
pennylane/transforms/decompositions/__init__.py 100.00% <100.00%> (ø)
.../transforms/decompositions/single_qubit_unitary.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8cb8d04...070bc97. Read the comment docs.

@glassnotes glassnotes requested a review from josh146 June 29, 2021 17:02
Copy link
Member

@josh146 josh146 left a comment

Choose a reason for hiding this comment

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

This functionality looks great @glassnotes, very happy to approve! In particular, I like how you separated the tests into tests that only check interface decomposition, and tests that only check interface gradients. It is very tempting when writing tests to combine them into a single test (which I have done a lot in the codebase).

Just two minor comments:

  • zyz_decomposition doesn't currently appear in the docs
  • I'm not sure how important this is, but it could be nice to parametrize the gradient tests with diff_method = ["parameter-shift", "backprop"], just to make sure everything works correctly with the non-default parameter-shift logic.

pennylane/transforms/unitary_to_rot.py Show resolved Hide resolved
phi = 1j * math.log(U[0, 1] / U[1, 0])
omega = -phi - math.cast_like(2 * math.angle(U[1, 0]), phi)
else:
el_division = U[0, 0] / U[1, 0]
Copy link
Member

Choose a reason for hiding this comment

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

El Division sounds like a math-parody mariachi band

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤣 🤣

pennylane/transforms/unitary_to_rot.py Show resolved Hide resolved
tests/circuit_graph/test_qasm.py Show resolved Hide resolved
tests/circuit_graph/test_qasm.py Show resolved Hide resolved
tests/transforms/test_unitary_to_rot.py Show resolved Hide resolved
decomp_ops = qml.transforms.decompositions.zyz_decomposition(U, wire)
return decomp_ops

raise NotImplementedError("Decompositions only supported for single-qubit unitaries")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure why this isn't getting caught; the corresponding test is on line 1593 of the test_qubit_ops file.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I think this is a well-known coverage bug. Fine to ignore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants