Skip to content
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

Merged
merged 18 commits into from
Jun 30, 2021
5 changes: 3 additions & 2 deletions pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
PennyLane can be directly imported.
"""
from importlib import reload
import pkg_resources

import numpy as _np
import pkg_resources
from semantic_version import Spec, Version

from pennylane.queuing import apply_op, QueuingContext

import pennylane.init
import pennylane.fourier
import pennylane.kernels
Expand Down Expand Up @@ -60,7 +62,6 @@

# QueuingContext and collections needs to be imported after all other pennylane imports
from .collections import QNodeCollection, apply, dot, map, sum
from .queuing import QueuingContext
import pennylane.grouping # pylint:disable=wrong-import-order

# Look for an existing configuration file
Expand Down
14 changes: 8 additions & 6 deletions pennylane/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,18 +187,20 @@ def expand(self):

return tape

def queue(self):
def queue(self, context=qml.QueuingContext):
"""Append the measurement process to an annotated queue."""
if self.obs is not None:
try:
qml.QueuingContext.update_info(self.obs, owner=self)
context.update_info(self.obs, owner=self)
except qml.queuing.QueuingError:
self.obs.queue()
qml.QueuingContext.update_info(self.obs, owner=self)
self.obs.queue(context=context)
context.update_info(self.obs, owner=self)

qml.QueuingContext.append(self, owns=self.obs)
context.append(self, owns=self.obs)
else:
qml.QueuingContext.append(self)
context.append(self)

return self


def expval(op):
Expand Down
39 changes: 24 additions & 15 deletions pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,10 +464,9 @@ def parameters(self):
"""Current parameter values."""
return self.data.copy()

def queue(self):
def queue(self, context=qml.QueuingContext):
mariaschuld marked this conversation as resolved.
Show resolved Hide resolved
"""Append the operator to the Operator queue."""
qml.QueuingContext.append(self)

context.append(self)
return self # so pre-constructed Observable instances can be queued and returned in a single statement


Expand Down Expand Up @@ -1112,27 +1111,37 @@ class Tensor(Observable):
par_domain = None

def __init__(self, *args): # pylint: disable=super-init-not-called

self._eigvals_cache = None
self.obs = []
self._args = args
self.queue(init=True)

for o in args:
if isinstance(o, Tensor):
self.obs.extend(o.obs)
elif isinstance(o, Observable):
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
Copy link
Contributor

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?

Copy link
Member Author

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.

constituents = self.obs

if init:
constituents = self._args

for o in constituents:

if init:
if isinstance(o, Tensor):
self.obs.extend(o.obs)
elif isinstance(o, Observable):
self.obs.append(o)
else:
raise ValueError("Can only perform tensor products between observables.")

try:
qml.QueuingContext.update_info(o, owner=self)
context.update_info(o, owner=self)
except qml.queuing.QueuingError:
o.queue()
qml.QueuingContext.update_info(o, owner=self)
o.queue(context=context)
context.update_info(o, owner=self)
except NotImplementedError:
pass

qml.QueuingContext.append(self, owns=tuple(args))
context.append(self, owns=tuple(constituents))
return self

def __copy__(self):
cls = self.__class__
Expand Down
34 changes: 34 additions & 0 deletions pennylane/queuing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
This module contains the :class:`QueuingContext` abstract base class.
"""
import abc
import copy
from collections import OrderedDict, deque


Expand Down Expand Up @@ -220,6 +221,9 @@ def _append(self, obj, **kwargs):
def _remove(self, obj):
self.queue.remove(obj)

append = _append
josh146 marked this conversation as resolved.
Show resolved Hide resolved
remove = _remove
Copy link
Contributor

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?

Copy link
Member Author

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.



class AnnotatedQueue(QueuingContext):
"""Lightweight class that maintains a basic queue of operations, in addition
Expand All @@ -246,7 +250,37 @@ def _get_info(self, obj):

return self._queue[obj]

append = _append
remove = _remove
update_info = _update_info
get_info = _get_info
josh146 marked this conversation as resolved.
Show resolved Hide resolved

@property
def queue(self):
"""Returns a list of objects in the annotated queue"""
return list(self._queue.keys())


def apply_op(op, context=QueuingContext):
"""Apply an instantiated ``Operator`` to a queuing context.

Args:
op (.Operator): the Operator to apply/queue
context (.QueuingContext): The queuing context to queue the operator to.
Note that if no context is specified, the operator is
applied to currently active queuing context.
josh146 marked this conversation as resolved.
Show resolved Hide resolved
"""
if not QueuingContext.recording():
raise RuntimeError("No queuing context available to append operation to.")

if op in getattr(context, "queue", QueuingContext.active_context().queue):
op = copy.copy(op)
josh146 marked this conversation as resolved.
Show resolved Hide resolved

if hasattr(op, "queue"):
# operator provides its own logic for queuing
op.queue(context=context)
else:
# append the operator directly to the relevant queuing context
context.append(op)

return op
10 changes: 5 additions & 5 deletions pennylane/vqe/vqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,18 +404,18 @@ def __isub__(self, H):
return self
raise ValueError(f"Cannot subtract {type(H)} from Hamiltonian")

def queue(self):
def queue(self, context=qml.QueuingContext):
"""Queues a qml.Hamiltonian instance"""
for o in self.ops:
try:
qml.QueuingContext.update_info(o, owner=self)
context.update_info(o, owner=self)
except QueuingError:
o.queue()
qml.QueuingContext.update_info(o, owner=self)
o.queue(context=context)
context.update_info(o, owner=self)
except NotImplementedError:
pass

qml.QueuingContext.append(self, owns=tuple(self.ops))
context.append(self, owns=tuple(self.ops))
return self


Expand Down
131 changes: 111 additions & 20 deletions tests/test_queuing.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,29 +319,10 @@ def test_update_error(self):
def test_append_annotating_object(self):
"""Test appending an object that writes annotations when queuing itself"""

class AnnotatingTensor(qml.operation.Tensor):
josh146 marked this conversation as resolved.
Show resolved Hide resolved
"""Dummy tensor class that queues itself on initialization
to an annotating queue."""

def __init__(self, *args):
super().__init__(*args)
self.queue()

def queue(self):
QueuingContext.append(self, owns=tuple(self.obs))

for o in self.obs:
try:
QueuingContext.update_info(o, owner=self)
except AttributeError:
pass

return self

with AnnotatedQueue() as q:
A = qml.PauliZ(0)
B = qml.PauliY(1)
tensor_op = AnnotatingTensor(A, B)
tensor_op = qml.operation.Tensor(A, B)

assert q.queue == [A, B, tensor_op]
assert q._get_info(A) == {"owner": tensor_op}
Expand Down Expand Up @@ -444,3 +425,113 @@ def template(x):
template(3)

assert str(recorder) == expected_output


test_observables = [
qml.PauliZ(0) @ qml.PauliZ(1),
qml.operation.Tensor(qml.PauliZ(0), qml.PauliX(1)),
qml.operation.Tensor(qml.PauliZ(0), qml.PauliX(1)) @ qml.Hadamard(2),
qml.Hamiltonian(
[0.1, 0.2, 0.3], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(1), qml.Identity(2)]
),
]


class TestApplyOp:
"""Tests for the apply_op function"""

def test_error(self):
"""Test that applying an operation without an active
context raises an error"""
with pytest.raises(RuntimeError, match="No queuing context"):
qml.apply_op(qml.PauliZ(0))

def test_default_queue_operation_inside(self):
"""Test applying an operation instantiated within the queuing
context to the existing active queue"""
with qml.tape.QuantumTape() as tape:
op1 = qml.PauliZ(0)
op2 = qml.apply_op(op1)

assert tape.operations == [op1, op2]
Copy link
Contributor

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!


def test_default_queue_operation_outside(self):
"""Test applying an operation instantiated outside a queuing context
to an existing active queue"""
op = qml.PauliZ(0)

with qml.tape.QuantumTape() as tape:
qml.apply_op(op)

assert tape.operations == [op]

@pytest.mark.parametrize("obs", test_observables)
def test_default_queue_measurements_outside(self, obs):
"""Test applying a measurement instantiated outside a queuing context
to an existing active queue"""
op = qml.expval(obs)

with qml.tape.QuantumTape() as tape:
qml.apply_op(op)

assert tape.measurements == [op]

@pytest.mark.parametrize("obs", test_observables)
def test_default_queue_measurements_outside(self, obs):
"""Test applying a measurement instantiated inside a queuing context
to an existing active queue"""

with qml.tape.QuantumTape() as tape:
op1 = qml.expval(obs)
op2 = qml.apply_op(op1)

assert tape.measurements == [op1, op2]

def test_different_queue_operation_inside(self):
"""Test applying an operation instantiated within the queuing
context to a specfied queuing context"""
with qml.tape.QuantumTape() as tape1:
with qml.tape.QuantumTape() as tape2:
op1 = qml.PauliZ(0)
op2 = qml.apply_op(op1, tape1)

assert tape1.operations == [tape2, op2]
assert tape2.operations == [op1]

def test_different_queue_operation_outside(self):
"""Test applying an operation instantiated outside a queuing context
to a specfied queuing context"""
op = qml.PauliZ(0)

with qml.tape.QuantumTape() as tape1:
with qml.tape.QuantumTape() as tape2:
qml.apply_op(op, tape1)

assert tape1.operations == [tape2, op]
assert tape2.operations == []

@pytest.mark.parametrize("obs", test_observables)
def test_different_queue_measurements_outside(self, obs):
"""Test applying a measurement instantiated outside a queuing context
to a specfied queuing context"""
op = qml.expval(obs)

with qml.tape.QuantumTape() as tape1:
with qml.tape.QuantumTape() as tape2:
qml.apply_op(op, tape1)

assert tape1.measurements == [op]
assert tape2.measurements == []

@pytest.mark.parametrize("obs", test_observables)
def test_different_queue_measurements_outside(self, obs):
"""Test applying a measurement instantiated inside a queuing context
to a specfied queuing context"""

with qml.tape.QuantumTape() as tape1:
with qml.tape.QuantumTape() as tape2:
op1 = qml.expval(obs)
op2 = qml.apply_op(op1, tape1)

assert tape1.measurements == [op2]
assert tape2.measurements == [op1]
2 changes: 2 additions & 0 deletions tests/test_vqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,8 @@ def test_hamiltonian_queue(self):
queue = [
qml.Hadamard(wires=1),
qml.PauliX(wires=0),
qml.PauliZ(0),
qml.PauliZ(2),
Comment on lines +813 to +814
Copy link
Member Author

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.

qml.PauliZ(0) @ qml.PauliZ(2),
qml.PauliX(1),
qml.PauliZ(1),
Expand Down