-
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
Add function to apply instantiated operations to queuing contexts #1433
Conversation
Hello. You may have forgotten to update the changelog!
|
Codecov Report
@@ Coverage Diff @@
## master #1433 +/- ##
=======================================
Coverage 98.24% 98.24%
=======================================
Files 160 160
Lines 11991 12016 +25
=======================================
+ Hits 11780 11805 +25
Misses 211 211
Continue to review full report at Codecov.
|
qml.PauliZ(0), | ||
qml.PauliZ(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.
There are two tests in this test function; one that tests Hamiltonian queuing for a Hamiltonian created outside the context, and one that tests Hamiltonian queuing for a Hamiltonian created inside the context.
This variable queue
is the expected result for the Hamiltonian outside the context. If you scroll down, you can see that the addition of these two elements makes it consistent with the expected result for the Hamiltonian inside the context.
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 took a quick look at the code for now, will provide more comments after using this for some local tests 👍
@glassnotes @mariaschuld I have renamed the function to |
qml.CNOT(wires=[0, 1]) | ||
|
||
>>> tape1.operations | ||
[Hadamard(wires=[1]), <QuantumTape: wires=[0], params=1>, PauliX(wires=[0]), CNOT(wires=[0, 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.
Why is the <QuantumTape: wires=[0], params=1>
part there? It looks like only a Hadamard
, PauliX
, and CNOT
should be applied to tape1
?
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 wait I think I just understood why from the tests later on; the contents of tape2
are also getting added to tape1
because it's within the same queuing context, but using qml.apply
allows us to apply something to tape1
without also applying it to tape2
?
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 exactly!
qml.Hadamard(wires=1) | ||
|
||
with qml.tape.QuantumTape() as tape2: | ||
op1 = qml.PauliX(wires=0) |
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 behaviour actually doesn't feel so intuitive; if I create something and assign it to a variable with the intention of queuing it in a different context, I wouldn't expect it to also be queued in the context that created it. Maybe the fact that operations are queued whenever they are instantiated could just be bolded or otherwise emphasized above at the start of the Example section, and be explicit that this includes cases like this one?
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 will emphasize it 👍 I think the example here is very much 'advanced' --- day-to-day, I think by the far the most common use-case of this functionality is to queue an operation that was instantiated outside of any queuing context.
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 suggestion @glassnotes; I've attempted to make it more clear here: 13aaea2
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.
That looks awesome! 💯
Co-authored-by: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com>
self.obs.append(o) | ||
else: | ||
raise ValueError("Can only perform tensor products between observables.") | ||
def queue(self, context=qml.QueuingContext, init=False): # pylint: disable=arguments-differ |
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 not sure I understand the init
argument?
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.
Ah - the queuing logic was previous occuring at the exact same time as the Tensor
datastructure was being computed.
That is, within __init__
, there was a single for loop that did two things:
- It queued each constituent observable
- It created the internal
obs
list of constituent observables.
I originally split it into two separate for loops in order to separate out the logic, but realized that it would result in looping through the observable list twice. Which is fine... but as the number of qubits increases, this could be a bottleneck.
So I combined it back into a single for loop, but use init=True
to ensure that building the datastructure only happens on instantiation, while queuing happens every time.
@@ -220,6 +221,9 @@ def _append(self, obj, **kwargs): | |||
def _remove(self, obj): | |||
self.queue.remove(obj) | |||
|
|||
append = _append | |||
remove = _remove |
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.
Trying to understand this one?
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.
Ah, this one is weird. QueuingContext.append
is a class method, so doesn't take self into account. Here, we are in a subclass, and simply overriding the inherited class methods.
"""Test that applying an operation without an active | ||
context raises an error""" | ||
with pytest.raises(RuntimeError, match="No queuing context"): | ||
qml.apply(qml.PauliZ(0)) |
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!
op1 = qml.PauliZ(0) | ||
op2 = qml.apply(op1) | ||
|
||
assert tape.operations == [op1, op2] |
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.
also cool, makes total sense!
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.
Very nice Josh! I have to admit I don't follow all changes at the beginning, but the main function, docstring and tests look awesome!
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.
@josh146 thanks for updating the documentation, helps make the examples much clearer! I've incorporated the method into my WIP transforms, and everything works seamlessly 💯
Context:
Currently, there is no consistent, user-facing approach for applying (already instantiated) operators to a queuing context. Instead, there is a combination of different approaches:
op.queue()
: the most common approach. Almost all operators provide their own queuing logic currently, barTensor
, which inherits a basic implementation from the base class and leads to inconsistent behaviour.qml.QueuingContext.append()
: appends the object directly to the currently active queuing context.context._append()
appends the object to a specific queuing context.This situation is not ideal, since
op.queue()
is not consistent, not documented, and not user-facing.Description of the Change:
Adds
Tensor.queue()
to make theTensor
class consistent with otherOperator
subclasses. This results in several accidental bug fixes that have plagued the Hamiltonian class (see Adds Hamiltonian Queueing and Transform to Compute Hamiltonian Expectation Value #1142)Modifies the existing
queue()
methods, so that they now take an optionalcontext
keyword argument. This allows operators to be queued to specific, provided, contexts, rather than just the currently active context.Adds a new function
qml.apply
, which will be the new user and developer facing method for queuing already instantiated operations.Benefits:
Possible Drawbacks:
qml.apply
function was an import ofqml.collections.apply
. This function was infrequently used or advertised, and now must be accessed by importing directly from the collections module.Related GitHub Issues: n/a