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:

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(op[, context])

Apply an instantiated operator or measurement to a queuing context.

process_queue(queue)

Process the annotated queue, creating a list of quantum operations and measurement processes.

Classes

AnnotatedQueue

Lightweight class that maintains a basic queue of operations, in addition to metadata annotations.

QueuingError

Exception that is raised when there is a queuing error

QueuingManager()

Singleton global entry point for managing active recording contexts.

WrappedObj(obj)

Wraps an object to make its hash dependent on its identity