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
Added a custom gate application for Toffoli in default.qubit #1249
Conversation
Hi @ryanhill1, thanks so much for going for this! 😊 Let us know if anything questions come up in the process. |
Hi @ryanhill1, thanks for the contribution! 🎉 I see you've added code, docstrings, tests, and updated the changelog. Would you say it's ready for code review? |
@co9olguy yep! should be ready |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ryanhill1 thank you for picking this up! 🎊 Went for a skim through, it seems that the reshaping of the state will need to be adjusted after the computation.
Also, it looks that a file will need to be formatted with black
. This can be done by installing black
locally with pip install black
(the latest version is black-20.8b1
🙂) and then running the following from the directory where PennyLane resides:
black -l 100 pennylane/pennylane/devices/default_qubit.py
Feel free to leave a note for any questions or request another review and will have another look. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! This'll be a great addition.
Right now I think the next steps are:
- run black
- get tests passing
- apply to more general cases
a) any number of wires
b) any ordering of control and target wires
Thank you everyone for all of your feedback! I accidentally pushed some working versions the other day that were super messy, so if you can ignore those and just look at my most recent commit, I think it should be ready for code review! The toffoli function should now work for any number of wires, and any ordering of control and target wires. Plus it is a much simpler implementation than what I had before. I added three test cases that account for both controls being greater than the target, both smaller, and one smaller one greater. I also slightly modified the one and two-qubit test cases to allow us to use the same 4 qubit state across all of them, instead of defining different states for different numbers of wires, which got kind of messy. Let me know any thoughts and/or revisions, I'm happy to make edits as needed. |
Codecov Report
@@ Coverage Diff @@
## master #1249 +/- ##
=======================================
Coverage 98.11% 98.12%
=======================================
Files 146 146
Lines 11056 11071 +15
=======================================
+ Hits 10848 10863 +15
Misses 208 208
Continue to review full report at Codecov.
|
Hi @ryanhill1, looks like all checks are passing 🎉 Let us know when this is ready for re-review 🙂 |
@co9olguy great! In that case it is ready for review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vast improvement! Thanks for this contribution :)
I'm going to request that the wire order variable gets extracted to pytest parameterization. That way we only need one function. That may also involve using np.tensordot
instead of np.einsum
.
I did some timing to compare the different methods, dev._apply_unitary
versus dev._apply_toffoli
. Here's a graph of multiple repetitions of 5000 on 10 wires. Even around the noise on my computer, you can tell your implementation will add some speed and efficiency! Always very exciting to see :)
state_out = method(self.state, axes=[0, 2, 3]) | ||
op = op(wires=[0, 2, 3]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be less error-prone upon future modification and updates if it's explicit that these two variables are the same thing. Same for the other tests.
Also, we could pull out that variable and parameterize the test over wire order. It would make the code more concise and easier to maintain.
state_out = method(self.state, axes=[0, 2, 3]) | |
op = op(wires=[0, 2, 3]) | |
wires_order = (0,2,3) | |
state_out = method(self.state, axes=wires_order) | |
op = op(wires=wires_order) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've started pulling out the wires variables and parameterizing the tests over the wire order, but I'm having trouble elegantly replacing the einsum
functions with tensordot
functions. tensordot
does not provide a way of reordering the noncontracted dimensions, so I'm instead needing to slice along the control axis, apply tensordot
to each slice, and then reassemble the state. But this seems much too similar to the actual implementation to be a fair test. For example, in test_apply_single_qubit_op
, if wires_order = [0]
, then our test state can simply be constructed using state_out_tensordot = np.tensordot(matrix, self.state)
. But if wires_order = [1]
, we need to do
result0 = np.tensordot(matrix, self.state[0])
result1 = np.tensordot(matrix, self.state[1])
state_out_tensordot = np.stack([result0, result1])
which gets even more involved for higher dimensions, and therefore is hard to generalize without basically copying the _apply_toffoli
implementation. I've tried specifying different axes along which to apply tensordot
, but the problem again is the ordering of dimensions. tensordot
is fine for einsum
operations such as ab,bijk->aijk
, because the output is the default ordering. But it seems operations such as ab,ibjk->iajk
are not reproducible in tensordot
without further specification. And so the general case i.e. making one specification that works for any wires_order
, will require if/else
statements inside the testing function, which is not ideal. Do you have any ideas?
Edit: I might have just found a workound using np.kron
and np.eye
to expand the dimensional representation of matrix
according to wires_order
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry this didn't work out. The idea was to make the tests simpler, but this sounds like it just makes the tests more complicated.
It was more of a suggestion than a need, so I'm going ahead with changing my review to "approve". Thanks for trying it out anyway 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think that's right the call. The parameterized version of test_apply_single_qubit_op
would have needed to look something like this:
num_wires = 4
state = np.arange(2 ** num_wires)
...
single_qubit_wires_order = [
[1],
]
...
@pytest.mark.parametrize("op, method", single_qubit_ops)
@pytest.mark.parametrize("wires_order", single_qubit_wires_order)
def test_apply_single_qubit_op(self, op, method, wires_order, inverse):
"""Test if the application of single qubit operations is correct."""
state_out = method(self.state.reshape([2] * self.num_wires), axes=wires_order, inverse=inverse)
op = op(wires=wires_order)
matrix0 = op.inv().matrix if inverse else op.matrix
matrix1 = np.kron(np.eye(2 ** wires_order[0]), matrix0)
matrix2 = np.kron(matrix1, np.eye(2 ** (self.num_wires - wires_order[0] - 1)))
state_out_dot = np.dot(matrix2, self.state).reshape([2] * self.num_wires)
assert np.allclose(state_out, state_out_dot)
pennylane/devices/default_qubit.py
Outdated
cntrl_max = np.argmax(axes[:2]) | ||
sl_a0 = _get_slice(0, axes[cntrl_max], self.num_wires) | ||
sl_a1 = _get_slice(1, axes[cntrl_max], self.num_wires) | ||
sl_b0 = _get_slice(0, axes[cntrl_max ^ 1], self.num_wires - 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like the use of ^
! very clever way of doing this 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since my idea of parametrizing over wire order actually makes the tests more complicated, instead of less complicated, I'm going to retract that suggestion and change my review to approval 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ryanhill1 this is looking 🏆 💯 Had one question about the reason for increasing the number of qubits in the tests, but no major blockers. Will just check back for that, but nearing approval from my side.
pennylane/devices/default_qubit.py
Outdated
cntrl_max = np.argmax(axes[:2]) | ||
sl_a0 = _get_slice(0, axes[cntrl_max], self.num_wires) | ||
sl_a1 = _get_slice(1, axes[cntrl_max], self.num_wires) | ||
sl_b0 = _get_slice(0, axes[cntrl_max ^ 1], self.num_wires - 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe worth adding cntrl_min = cntrl_max ^ 1
after line cntrl_max
and using cntrl_min
for readability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just added that change, it does look better!
assert np.allclose(state_out, state_out_einsum) | ||
|
||
@pytest.mark.parametrize("op, method", three_qubit_ops) | ||
def test_apply_three_qubit_op_controls_split(self, op, method, inverse): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for adding this test case!
@@ -1909,8 +1909,8 @@ class TestApplyOps: | |||
"""Tests for special methods listed in _apply_ops that use array manipulation tricks to apply | |||
gates in DefaultQubit.""" | |||
|
|||
state = np.arange(2 ** 3).reshape((2, 2, 2)) | |||
dev = qml.device("default.qubit", wires=3) | |||
state = np.arange(2 ** 4).reshape((2, 2, 2, 2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come we had to go for a device with 4 qubits?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to a device with 4 qubits so we can test wire orderings in which the two controlled bits and the target bit are not all adjacent to one another. This helps ensure that _apply_toffoli
works for any number of wires, not just three.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! 💯 Amazing work @ryanhill1 🎉
Context:
Custom gate definition for the Toffoli gate
Description of the Change:
pennylane/devices/default_qubit.py
file, extended the_apply_ops
dictionary to contain the"Toffoli": self._apply_toffoli
entry_apply_toffoli
method forDefaultQubit
in the same filedevices/test_default_qubit.py
.Benefits:
Custom gate definition for the Toffoli gate
Possible Drawbacks:
N/A
Related GitHub Issues:
#1215