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

Added a custom gate application for Toffoli in default.qubit #1249

Merged
merged 26 commits into from May 3, 2021
Merged

Added a custom gate application for Toffoli in default.qubit #1249

merged 26 commits into from May 3, 2021

Conversation

ryanhill1
Copy link
Contributor

Context:

Custom gate definition for the Toffoli gate

Description of the Change:

  • In the pennylane/devices/default_qubit.py file, extended the _apply_ops dictionary to contain the "Toffoli": self._apply_toffoli entry
  • Defined the _apply_toffoli method for DefaultQubit in the same file
  • Added test cases for applying the Toffoli gate in devices/test_default_qubit.py.

Benefits:

Custom gate definition for the Toffoli gate

Possible Drawbacks:

N/A

Related GitHub Issues:

#1215

@antalszava
Copy link
Contributor

Hi @ryanhill1, thanks so much for going for this! 😊 Let us know if anything questions come up in the process.

@ryanhill1 ryanhill1 mentioned this pull request Apr 22, 2021
@co9olguy
Copy link
Member

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?

@ryanhill1
Copy link
Contributor Author

@co9olguy yep! should be ready

Copy link
Contributor

@antalszava antalszava left a 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. 🙂

pennylane/devices/default_qubit.py Outdated Show resolved Hide resolved
.github/CHANGELOG.md Outdated Show resolved Hide resolved
.github/CHANGELOG.md Outdated Show resolved Hide resolved
Copy link
Contributor

@albi3ro albi3ro left a 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:

  1. run black
  2. get tests passing
  3. apply to more general cases
    a) any number of wires
    b) any ordering of control and target wires

pennylane/devices/default_qubit.py Outdated Show resolved Hide resolved
pennylane/devices/default_qubit.py Outdated Show resolved Hide resolved
@ryanhill1
Copy link
Contributor Author

ryanhill1 commented Apr 30, 2021

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
Copy link

codecov bot commented Apr 30, 2021

Codecov Report

Merging #1249 (0cd56a0) into master (fe5d1df) will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #1249   +/-   ##
=======================================
  Coverage   98.11%   98.12%           
=======================================
  Files         146      146           
  Lines       11056    11071   +15     
=======================================
+ Hits        10848    10863   +15     
  Misses        208      208           
Impacted Files Coverage Δ
pennylane/devices/default_qubit.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 fe5d1df...0cd56a0. Read the comment docs.

@co9olguy
Copy link
Member

Hi @ryanhill1, looks like all checks are passing 🎉

Let us know when this is ready for re-review 🙂

@ryanhill1
Copy link
Contributor Author

@co9olguy great! In that case it is ready for review

Copy link
Contributor

@albi3ro albi3ro left a 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 :)

image

Comment on lines +1966 to +1967
state_out = method(self.state, axes=[0, 2, 3])
op = op(wires=[0, 2, 3])
Copy link
Contributor

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.

Suggested change
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)

Copy link
Contributor Author

@ryanhill1 ryanhill1 May 3, 2021

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.

Copy link
Contributor

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 👍

Copy link
Contributor Author

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)

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)
Copy link
Contributor

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 👍

@albi3ro albi3ro self-requested a review May 3, 2021 17:32
Copy link
Contributor

@albi3ro albi3ro left a 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 👍

Copy link
Contributor

@antalszava antalszava left a 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.

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)
Copy link
Contributor

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

Copy link
Contributor Author

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):
Copy link
Contributor

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))
Copy link
Contributor

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?

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 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.

Copy link
Contributor

@antalszava antalszava left a 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 🎉

@antalszava antalszava merged commit 8fe31ad into PennyLaneAI:master May 3, 2021
@ryanhill1 ryanhill1 deleted the toffoli branch May 3, 2021 22:34
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.

None yet

5 participants