-
Notifications
You must be signed in to change notification settings - Fork 603
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
Fix inversion of templates #1243
Conversation
@@ -653,6 +653,23 @@ def inv(self): | |||
|
|||
self._ops = list(reversed(self._ops)) | |||
|
|||
def adjoint(self): |
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.
We need to add an adjoint()
method to the tape, so that the qml.adjoint()
function knows what to do when it recursively hits a tape.
adjoint(op.decomposition)(wires=op.wires) | ||
# calling adjoint on the expansion will automatically | ||
# queue the new operations. | ||
adjoint(op.expand)() |
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.
Much cleaner, because we do not need to know a signature of decomposition
here.
Codecov Report
@@ Coverage Diff @@
## master #1243 +/- ##
==========================================
- Coverage 98.14% 98.11% -0.03%
==========================================
Files 146 146
Lines 11035 11050 +15
==========================================
+ Hits 10830 10842 +12
- Misses 205 208 +3
Continue to review full report at Codecov.
|
def adjoint(self, do_queue=False): | ||
return qml.templates.MottonenStatePreparation(self.parameters[0], wires=self.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 was an idea by @mariaschuld: if inverting AmplitudeEmbedding, we should force it to go via the Mottonen route
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 was actually thinking that even without this explicit adjoint function this would be done, since the template calls QubitStateVector
which should not have an adjoint, and hence its decomposition is used? But great like that too!
@@ -266,13 +266,17 @@ def expand(self): | |||
|
|||
with qml.tape.QuantumTape() as tape: | |||
|
|||
qml.BasisState(self.init_state, wires=self.wires) | |||
qml.templates.BasisEmbedding(self.init_state, wires=self.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 ensures that this layer structure is invertible
res = circuit(weights) | ||
expected = np.zeros([2 ** 3]) | ||
expected[0] = 1.0 | ||
assert np.allclose(res, expected) |
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.
Any reason to change the assert statement from the previous tests?
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 think I decided it was safer to check for the ground state explicitly, since any other basis state would also satisfy assert len(np.nonzero(res)) == 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.
Great @josh146, you are a star. Shall I add the other tests?
Alternatively, we could just add a test that compares the tape returned by the new inv
method in the templates? Is there any way to check if a tape is the inverse of another? Because if that is checked, and the rest of the logic tested in the inverse/adjoint functions works, things should be solid, no?
Currently there is no way of doing this I believe 🤔 It might also be safer just to manually test every template? Even though it is a lot of extra work... But yeah I don't really have a good solution for parametrizing it, especially since a lot of the templates have different signatures. |
@@ -18,34 +18,51 @@ | |||
|
|||
|
|||
def adjoint(fn): | |||
"""Create a function that applies the adjoint of the provided operation or template. | |||
"""Create a function that applies the adjoint (inverse) of the provided operation or 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.
@Thenerdstation I updated this docstring after discussion with a user that was having trouble
@Thenerdstation @mariaschuld: I had an epiphany today! If we re-write
I'd like to take a second to thank codecov, as I was inspired to this approach due to all the missing lines covered due to the many |
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 @josh146! 🥇 (popped in for the review, hope that's okay :) )
Was wondering: is it worth using qml.state
in the tests?
There could be cases, where we get the expected probabilities with an unexpected state, e.g.,:
import pennylane as qml
dev = qml.device('default.qubit', wires=1)
@qml.qnode(dev)
def circuit_with_state():
qml.PauliX(0)
qml.S(wires=[0])
return qml.state()
print(circuit_with_state())
dev = qml.device('default.qubit', wires=1)
@qml.qnode(dev)
def circuit_with_probs():
qml.PauliX(0)
qml.S(wires=[0])
return qml.probs(wires=[0])
print(circuit_with_probs())
[0.+0.j 0.+1.j]
[0. 1.]
We have a complex amplitude in the state vector, and yet we'd sample the |1>
basis state with 100% theoretically. So checking [0, 1]
for the probabilities could give a false positive if we'd like to check that our state is in
~.QuantumTape: the adjointed tape | ||
""" | ||
new_tape = self.copy(copy_operations=True) | ||
qml.transforms.invisible(new_tape.inv)() |
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.
For my understanding: do we now have tape.inv
as the in-place operation and qml.adjoint
as the tape transform?
And qml.inv
is good for everything including tapes.
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.
Yep! qml.adjoint
and qml.inv
are currently interchangeable, but qml.inv
will be removed in the next release.
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.
Interesting, invisible makes sure it is not queued? so it is a transform transform :)
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.
Yep. Under the hood, it looks like this:
with tape.stop_recording():
# things you don't want queued go here
However, @Thenerdstation pointed out that with PL's functional UI, it also makes sense to have a functional 'entry point' to this functionality. Nathan is not wild about the name though, so I'm trying to brainstorm better names 😆
How about qml.transforms.ignore
? Although I feel that invisible
is a bit more clear in its intention --- the function is still executed, it is just invisible to the queue.
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.
Note that qml.transforms.invisible
is not meant to be user-facing; it is more for developers (and potentially advanced users).
@@ -2054,7 +2054,7 @@ def test_s_inverse(): | |||
test_s_inverse() | |||
operations = test_s_inverse.qtape.operations | |||
assert "S.inv" not in [i.name for i in operations] | |||
assert "PhaseShift.inv" in [i.name for i in operations] | |||
assert "PhaseShift" in [i.name for i in operations] |
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 this was changed? (and line 2056 stayed as is)
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.
With this change, qml.inv()
now uses qml.adjoint()
internally. As a result. gates that PL knows how to invert are automatically inverted; PhaseShift(x)
becomes PhaseShift(-x)
(rather than PhaseShift(x).inv
).
The only exception are gates where the inverse is not known.
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.
Oh! That makes sense. thank you! 🙂
Co-authored-by: antalszava <antalszava@gmail.com>
Co-authored-by: antalszava <antalszava@gmail.com>
@@ -463,8 +463,8 @@ def inverted_dummy_template_operations(wires): | |||
ops = [] | |||
|
|||
for wire in reversed(wires): | |||
ops.append(qml.RY(-1, wires=[wire]).inv()) | |||
ops.append(qml.RX(1, wires=[wire]).inv()) |
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 to understand, .inv() still works but will be deprecated?
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.
qml.inv()
, op.inv()
, and qml.adjoint()
should all work identically.
qml.inv()
is marked as deprecated (in my head, need to formalize this).op.inv()
is not marked for deprecation, we still need to work out what the future of this is.
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 sorting the inversion mess out!
Looks good to me. In the long run I wonder if there is a better option than testing every template? I guess when all the Operations follow the same design, adjoint() could be tested with only a few examples?
Yep, I think long-term (once templates are sorted out) this is something to be moved to the template tests. I.e., every template should have |
Context:
In v.0.15 of PennyLane, two conflicting changes were made:
expand
methodsqml.adjoint()
transform was introduced, which recursively inverts thedecomposition
methodAs a result,
qml.adjoint()
no longer works with templates.In addition, the template
expand()
methods did not take into account theself.inverse
flag, which is used by the deprecatedqml.inv()
function. As a result, theqml.inv()
function also stopped working with templates.Description of the Change:
The two main changes are:
qml.adjoint()
now uses theOperation.expand
method, rather thanOperation.decomposition
, for better compatibility and allowingqml.adjoint()
to again remain working with templates.qml.inv()
was modified to useqml.adjoint()
internally, allowing it to continue to work.Two additional bugs were fixed at the same time
tape.adjoint()
has been added, mirroringOperation.adjoint()
, so thatqml.adjoint()
will now work on nested tape structures.tape.inv()
now uses theop.adjoint()
method to invert operations, rather than the deprecatedOperation.inverse
flag.Benefits:
qml.inv()
andqml.adjoint()
now work again in all cases.Tests have been added to test
qml.inv()
andqml.adjoint()
on the templates, to ensure we catch this in future.Possible Drawbacks:
Not a drawback per se, but the following functionality is due to be removed in v0.16:
qml.inv()
tape.inv()
methodOperation.inverse
flagThis will help simplify the code base, all uses of these logical blocks can be deleted.
Related GitHub Issues: n/a