-
Notifications
You must be signed in to change notification settings - Fork 575
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 Carry and Sum Operators #1169
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1169 +/- ##
=======================================
Coverage 98.13% 98.13%
=======================================
Files 145 145
Lines 10984 11010 +26
=======================================
+ Hits 10779 10805 +26
Misses 205 205
Continue to review full report at Codecov.
|
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 @DSGuala, this PR is progressing nicely!
pennylane/ops/qubit.py
Outdated
* Gradient recipe: TODO | ||
|
||
Args: | ||
wires (Union[Wires or int]): the wires the operation acts on |
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.
There are two ways we could think about this:
- Either we just let the user pass in
wires
(must be 4 of them) - We have 4 separate arguments, e.g., input carry, output carry, input A, input B
I'm in favour of the first option, but as long as we make it clear which wire does what. This is similar to CNOT, where we have a single wires
argument.
However, in going for the first option, we'll have to be more careful when we make an adder between two registers, since this Carry
operation will work on a combination of those registers.
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 also prefer the first option. I think it should be okay as long as we are clear about which wires do what.
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Update default.mixed Add expand()
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Updated docstrings
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 @DSGuala! I have requested some changes.
doc/_static/ops/Carry.svg
Outdated
@@ -0,0 +1,451 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> |
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.
On second thoughts, maybe doc/_static/templates/carry/
and doc/_static/templates/sum
might be better spots for this content.
Although they are ops, they are more familiar conceptually as a template 👍
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.
Question: if carry
is thought more as a template than an op, should it be in the templates
module? In addition, would we add it to the templates quickstart gallery?
Going even further, where do we draw the line between an op and a template? Are ops what we think of when we imagine low-level device gate sets (X, Y, RX, CNOT, CSWAP), whereas a template is a larger operation designed with a specific purpose in mind?
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.
🤔 Thinking over my last comment:
-
Operations are general purpose unitaries, often relating to the underlying device architecture, that must be combined to build up problem-specific unitaries.
-
Templates are larger unitaries designed for specific algorithms or applications
Is this the distinction we want to draw?
I think this is important to consider, as it determines whether the source code lives in pennylane/templates
or pennylane/ops/qubit.py
(which is getting very crowded!)
@mariaschuld might also have some thoughts here.
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.
This is a very good question 🤔 and the line can indeed be blurred.
Could we define a template to be a unitary where only the expand()
method is known? At least for Carry
and Sum
, we know the matrix representation too.
But yeah, it's not fully clear where the line should be. The original motivation to put these as ops was:
- We'll be using them as primitives within something that is definitely a template: the
Adder
circuit - Similar precedents are already ops, e.g.,
CRot
has a matrix and decomposition, but is unlikely to be used directly on hardware (?) - Doesn't seem to fit what I expect from a template, e.g., is not "scalable" (i.e., can have variable depth/width) and doesn't have input parameters.
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.
Could we define a template to be a unitary where only the expand() method is known? At least for Carry and Sum, we know the matrix representation too.
I'm not sure this makes sense either - there is nothing stopping a device from declaring that it supports an Op directly, causing the expand
to be avoided. And, the matrix
property is just for convenience, most devices ignore it.
I think the main difference will essentially come down to a conceptual difference, and will likely be flexible.
doc/_static/ops/Carry.svg
Outdated
@@ -0,0 +1,451 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="531.153pt" height="66.041pt" viewBox="0 0 531.153 66.041" version="1.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.
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.
Ohhh so that's the carried value?
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.
The bottom right comes from this paper and is indeed the carried value. Perhaps there is a clearer way to write it. Just |C>
does not seem clear enough
doc/_static/ops/Carry.svg
Outdated
@@ -0,0 +1,451 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="531.153pt" height="66.041pt" viewBox="0 0 531.153 66.041" version="1.1"> | |||
<defs> |
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.
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.
Perfect, will change this.
tests/ops/test_qubit_ops.py
Outdated
([0, 1, 2, 3], "0000", "0000"), | ||
([0, 1, 2, 3], "0010", "0010"), | ||
([0, 1, 2, 3], "0100", "0110"), | ||
([0, 1, 2, 3], "0110", "0101"), | ||
([0, 1, 2, 3], "1010", "1011"), |
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 like these! Ideally it would be good to cover all 16 eventualities, and confirm by hand that the second bitstring is indeed the carry.
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.
Going over all 16 eventualities + changing to the tape mode with and without expansion should be a comprehensive enough set of tests (and same for Sum)
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 like these! Ideally it would be good to cover all 16 eventualities, and confirm by hand that the second bitstring is indeed the carry.
Also that the sum is the correct one, right?
tests/ops/test_qubit_ops.py
Outdated
([3, 1, 2, 0], "0110", "1100"), | ||
([3, 2, 0, 1], "1010", "0110"), |
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.
Nice to permute!
tests/ops/test_qubit_ops.py
Outdated
([3, 2, 0, 1], "1010", "0110"), | ||
], | ||
) | ||
def test_carry(self, wires, input_string, output_string): |
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.
It would be good to test the expand()
method. To do this, I suggest here having a parametrize
over expand = True/False
.
Then, below, instead of using
@qml.qnode(dev)
def circuit():
...
return qml.probs()
You can do
with qml.tape.QuantumTape() as tape:
...
qml.probs()
This is using the lower level tape functionality, which gives us control over expansion.
To execute, we'd then do
dev.execute(tape)
So, when it comes to expanding, before executing we'd do tape = tape.expand()
, which will then allow us to test if the expansion is correct.
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
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 so far!
My main comments are regarding docstrings, which I believe could be improved for further clarity
pennylane/ops/qubit.py
Outdated
|
||
class Carry(Operation): | ||
r"""Carry(wires) | ||
Apply the ``Carry`` operation to the input wires. |
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'm left confused after reading the docstring. Can we provide a short intuitive explanation of what this operation does? Maybe with a simple but explicit example?
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.
Is it correct to say that this operation performs addition of two bit values and stores the value that is carried over in a separate qubit? So for example it would map |0>|1>|1>|0>-->|1>|1>|0>|1>? I'm still confused about the role of the fourth qubit... 🤔
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.
Yes! I will be improving the docstrings. The carry operation would map |0>|1>|1>|0> --> |0>|1>|0>|1>. The fourth qubit takes the carried value. I think the confusion here comes from the fact that Carry
is a primitive operation for an Adder
template. The fourth qubit actually takes the carry value of the first three qubits.
pennylane/ops/qubit.py
Outdated
wires (Union[Wires or int]): the wires the operation acts on | ||
|
||
**Example** | ||
TODO |
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.
Should add extra text explaining that qubit 3 is the carry qubit, otherwise it may be confusing why we apply Sum
to four qubits only to sum three of them
pennylane/ops/qubit.py
Outdated
|
||
class Sum(Operation): | ||
r"""Sum() | ||
Apply a ``Sum`` operation on the input wires. |
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.
This is not really accurate, is it? The operation is only summing up two of the wires, not all of them, right?
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.
Hopefully this is clearer in the new version. For sum, it is indeed the modulo 2 sum of all input wires
pennylane/ops/qubit.py
Outdated
wires (Union[Wires or int]): the wires the operation acts on | ||
|
||
**Example** | ||
TODO |
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.
Actually, the operation only acts on three wires, right? So this should be wires=[0,1,2]
and explain that one of the wires is a carry qubit
pennylane/ops/qubit.py
Outdated
wires (Union[Wires or int]): the wires the operation acts on | ||
|
||
**Example** | ||
TODO |
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.
Finally, these docstrings could also benefit from the example actually performing a specific sum, e.g. 1+1 = 0. And it would be good to explain everything is done modulo 2
tests/ops/test_qubit_ops.py
Outdated
([0, 1, 2, 3], "0000", "0000"), | ||
([0, 1, 2, 3], "0010", "0010"), | ||
([0, 1, 2, 3], "0100", "0110"), | ||
([0, 1, 2, 3], "0110", "0101"), | ||
([0, 1, 2, 3], "1010", "1011"), |
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 like these! Ideally it would be good to cover all 16 eventualities, and confirm by hand that the second bitstring is indeed the carry.
Also that the sum is the correct one, right?
* Update imgs * Add to changelog * Add to docs * Update docs * Update expand * Add to docs * Update example * Typo * Add subtlety * Add * Update docstring * Update docs * Respond to suggestion
Co-authored-by: Chase Roberts <chase@xanadu.ai>
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 @DSGuala!
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 @DSGuala! Ready to merge - would be good to do one quick last check of the docs to see if everything is still rendering nicely.
@@ -401,7 +401,13 @@ def test_adjoint_unitaries(self, op, tol): | |||
np.testing.assert_allclose(res2, np.eye(2 ** len(op.wires)), atol=tol) | |||
assert op.wires == op_d.wires |
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.
Can you add QubitSum to this test aswell?
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 add the test I mentioned earlier
Context:
Carry and sum operators are used to build quantum adders. These can be useful in various applications e.g. Shor's algorithm.
Description of the Change:
Added the basic QubitCarry and QubitSum operators to
qubit.py
Benefits:
Will enable basic summation operations.
Possible Drawbacks:
The user still needs to organize the carry and sum operators to perform multi-bit additions.
There may be more optimal summation methods depending on the application (e.g. by using the quantum fourier transform)