qml.queuing¶
Overview¶
Warning
Unless you are a PennyLane developer, you likely do not need to use these classes directly.
This module contains the classes for placing objects into queues.
Description¶
Users provide quantum functions which PennyLane needs to convert into a circuit representation capable of being executed by a device. A quantum function is any callable that:
accepts classical inputs
constructs any number of quantum
Operator
objectsreturns one or more
MeasurementProcess
objects.
For example:
def qfunc(x, scale_value=1):
qml.RX(x * scale_value, wires=0)
if (1 != 2):
qml.S(0)
return qml.expval(qml.Z(0)), qml.expval(qml.X(1))
To convert from a quantum function to a representation of a circuit, we use queuing.
A queuable object is anything that can be placed into a queue. These will be Operator
,
MeasurementProcess
, and QuantumTape
objects. Operator
and
MeasurementProcess
objects achieve queuing via a queue()
method called upon construction.
Note that even though QuantumTape
is a queuable object, it does not have a queue
method.
When an object is queued, it sends itself to the QueuingManager
. The QueuingManager
is a global singleton class that facilitates placing objects in the queue. All of QueuingManager
’s methods
and properties are class methods and properties, so all instances will access the same information.
The active_context()
is the queue where any new objects are placed.
The QueuingManager
is said to be recording if an active context exists.
Active contexts are AnnotatedQueue
instances. They are context managers where recording occurs
within a with
block.
Let’s take a look at an example. If we query the QueuingManager
outside of an
AnnotatedQueue
’s context, we can see that nothing is recording and no active context exists.
>>> print("Are we recording? ", qml.QueuingManager.recording())
Are we recording? False
>>> print("What's the active context? ", qml.QueuingManager.active_context())
What's the active context? None
Inside of a context, we can see the active recording context:
>>> with qml.queuing.AnnotatedQueue() as q:
... print("Are we recording? ", qml.QueuingManager.recording())
... print("Is q the active queue? ", q is qml.QueuingManager.active_context())
Are we recording? True
Is q the active queue? True
If we have nested AnnotatedQueue
contexts, only the innermost one will be recording.
Once the currently active queue exits, any outer queue will resume recording.
>>> with qml.queuing.AnnotatedQueue() as q1:
... print("Is q1 recording? ", q1 is qml.QueuingManager.active_context())
... with qml.queuing.AnnotatedQueue() as q2:
... print("Is q1 recording? ", q1 is qml.QueuingManager.active_context())
... print("Is q1 recording? ", q1 is qml.QueuingManager.active_context())
Is q1 recording? True
Is q1 recording? False
Is q1 recording? True
If we construct an operator inside the recording context, we can see it is added to the queue:
>>> with qml.queuing.AnnotatedQueue() as q:
... op = qml.X(0)
>>> q.queue
[X(0)]
If an operator is constructed outside of the context, we can manually add it to the queue by
calling the queue()
method. The queue()
method is automatically
called upon initialization, but it can also be manually called at a later time.
>>> op = qml.X(0)
>>> with qml.queuing.AnnotatedQueue() as q:
... op.queue()
>>> q.queue
[X(0)]
An object can only exist up to once in the queue, so calling queue multiple times will not do anything.
>>> op = qml.X(0)
>>> with qml.queuing.AnnotatedQueue() as q:
... op.queue()
... op.queue()
>>> q.queue
[X(0)]
The apply()
method allows a single object to be queued multiple times in a circuit.
The function queues a copy of the original object if it already in the queue.
>>> op = qml.X(0)
>>> with qml.queuing.AnnotatedQueue() as q:
... qml.apply(op)
... qml.apply(op)
>>> q.queue
[X(0), X(0)]
>>> q.queue[0] is q.queue[1]
False
In the case of operators composed of other operators, like with SymbolicOp
and
CompositeOp
, the new nested operation removes its constituents from the queue.
Only the operators that will end up in the circuit will remain.
>>> with qml.queuing.AnnotatedQueue() as q:
... base = qml.X(0)
... print(q.queue)
... pow_op = base ** 1.5
... print(q.queue)
[X(0)]
[X(0)**1.5]
Once the queue is constructed, the process_queue()
function converts it into the operations
and measurements in the final circuit. This step eliminates any object that has an owner.
>>> with qml.queuing.AnnotatedQueue() as q:
... qml.StatePrep(np.array([1.0, 0]), wires=0)
... base = qml.X(0)
... pow_op = base ** 1.5
... qml.expval(qml.Z(0) @ qml.X(1))
>>> ops, measurements = qml.queuing.process_queue(q)
>>> ops
[StatePrep(tensor([1., 0.], requires_grad=True), wires=[0]), X(0)**1.5]
>>> measurements
[expval(Z(0) @ X(1))]
These lists can be used to construct a QuantumScript
:
>>> qml.tape.QuantumScript(ops, measurements)
<QuantumScript: wires=[0, 1], params=1>
In order to construct new operators within a recording, but without queuing them
use the stop_recording()
context upon construction:
>>> with qml.queuing.AnnotatedQueue() as q:
... with qml.QueuingManager.stop_recording():
... qml.Y(1)
>>> q.queue
[]
Functions¶
|
Apply an instantiated operator or measurement to a queuing context. |
|
Process the annotated queue, creating a list of quantum operations and measurement processes. |
Classes¶
Lightweight class that maintains a basic queue of operations, in addition to metadata annotations. |
|
Exception that is raised when there is a queuing error |
|
Singleton global entry point for managing active recording contexts. |
|
|
Wraps an object to make its hash dependent on its identity |