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

Fix inversion of templates #1243

Merged
merged 23 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,17 @@

<h3>Bug fixes</h3>

* A bug which resulted in `qml.adjoint()` and `qml.inv()` failing to work with
templates has been fixed.
[(#1243)](https://github.com/PennyLaneAI/pennylane/pull/1243)

<h3>Documentation</h3>

<h3>Contributors</h3>

This release contains contributions from:

Thomas Bromley, Diego Guala, Anthony Hayes, Antal Száva
Thomas Bromley, Diego Guala, Anthony Hayes, Josh Izaac, Antal Száva

# Release 0.15.0 (current release)

Expand Down
5 changes: 1 addition & 4 deletions pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,16 +589,13 @@ def inverse(self):
"""Boolean determining if the inverse of the operation was requested."""
return self._inverse

def adjoint(self, do_queue=False):
def adjoint(self):
"""Create an operation that is the adjoint of this one.

Adjointed operations are the conjugated and transposed version of the
original operation. Adjointed ops are equivalent to the inverted operation for unitary
gates.

Args:
do_queue: Whether to add the adjointed gate to the context queue.

Returns:
The adjointed operation.
"""
Expand Down
2 changes: 1 addition & 1 deletion pennylane/ops/qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,7 @@ def decomposition(state, wires):
return MottonenStatePreparation(state, wires)

def adjoint(self):
raise AdjointError("No adjoint exists for QubitStateVector operations.")
return qml.adjoint(MottonenStatePreparation)(self.parameters[0], wires=self.wires)


# =============================================================================
Expand Down
33 changes: 31 additions & 2 deletions pennylane/tape/tape.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,11 +648,40 @@ def inv(self):
self.trainable_params = {parameter_mapping[i] for i in self.trainable_params}
self._par_info = {parameter_mapping[k]: v for k, v in self._par_info.items()}

for op in self._ops:
op.inverse = not op.inverse
for idx, op in enumerate(self._ops):
try:
self._ops[idx] = op.adjoint()
except NotImplementedError:
op.inverse = not op.inverse

self._ops = list(reversed(self._ops))

def adjoint(self):
Copy link
Member Author

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.

"""Create a tape that is the adjoint of this one.

Adjointed tapes are the conjugated and transposed version of the
original tapes. Adjointed ops are equivalent to the inverted operation for unitary
gates.

Returns:
~.QuantumTape: the adjointed tape
"""
new_tape = self.copy(copy_operations=True)
qml.transforms.invisible(new_tape.inv)()
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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 :)

Copy link
Member Author

@josh146 josh146 Apr 29, 2021

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.

Copy link
Member Author

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).


# the current implementation of the adjoint
# transform requires that the returned inverted object
# is automatically queued.
QuantumTape._lock.acquire()
try:
QueuingContext.append(new_tape)
except Exception as _:
QuantumTape._lock.release()
raise
QuantumTape._lock.release()
mariaschuld marked this conversation as resolved.
Show resolved Hide resolved

return new_tape

# ========================================================
# Parameter handling
# ========================================================
Expand Down
5 changes: 5 additions & 0 deletions pennylane/templates/embeddings/amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ def __init__(self, features, wires, pad_with=None, normalize=False, pad=None, do
features = self._preprocess(features, wires, pad_with, normalize)
super().__init__(features, wires=wires, do_queue=do_queue)

def adjoint(self):
josh146 marked this conversation as resolved.
Show resolved Hide resolved
return qml.adjoint(qml.templates.MottonenStatePreparation)(
self.parameters[0], wires=self.wires
)

def expand(self):

with qml.tape.QuantumTape() as tape:
Expand Down
3 changes: 2 additions & 1 deletion pennylane/templates/layers/particle_conserving_u1.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,14 @@ 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)
Copy link
Member Author

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


for l in range(self.n_layers):
for i, wires_ in enumerate(nm_wires):
u1_ex_gate(
self.parameters[0][l, i, 0], self.parameters[0][l, i, 1], wires=wires_
)

return tape

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion pennylane/templates/layers/particle_conserving_u2.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ 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)

for l in range(self.n_layers):

Expand Down
50 changes: 34 additions & 16 deletions pennylane/transforms/adjoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member Author

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


This transform can be used to apply the adjoint of an arbitrary sequence of operations.

Args:
fn (function): Any python function that applies pennylane operations.
fn (function): A quantum function that applies quantum operations.

Returns:
function: A new function that will apply the same operations but adjointed and in reverse order.

**Example**

The adjoint transforms can be used within a QNode to apply the adjoint of
any quantum function. Consider the following quantum function, that applies two
operations:

.. code-block:: python3

def my_ops():
qml.RX(0.123, wires=0)
qml.RY(0.456, wires=0)
def my_ops(a, b, wire):
qml.RX(a, wires=wire)
qml.RY(b, wires=wire)

We can create a QNode that applies this quantum function,
followed by the adjoint of this function:

.. code-block:: python3

dev = qml.device('default.qubit', wires=1)

@qml.qnode(dev)
def circuit(a, b):
my_ops(a, b, wire=0)
qml.adjoint(my_ops)(a, b, wire=0)
return qml.expval(qml.PauliZ(0))

Printing this out, we can see that the inverse quantum
function has indeed been applied:

with qml.tape.QuantumTape() as tape:
my_ops()
>>> print(qml.draw(circuit)(0.2, 0.5))
0: ──RX(0.2)──RY(0.5)──RY(-0.5)──RX(-0.2)──┤ ⟨Z⟩

with qml.tape.QuantumTape() as tape_adj:
qml.adjoint(my_ops)()
The adjoint function can also be applied directly to templates and operations:

>>> print(tape.operations)
[RX(0.123, wires=[0]), RY(0.456, wires=[0])]
>>> print(tape_adj.operatioins)
[RY(-0.456, wires=[0]), RX(-0.123, wires=[0])]
>>> qml.adjoint(qml.RX)(0.123, wires=0)
>>> qml.adjoint(qml.templates.StronglyEntanglingLayers)(weights, wires=[0, 1])
josh146 marked this conversation as resolved.
Show resolved Hide resolved

.. UsageDetails::

Expand Down Expand Up @@ -102,9 +119,10 @@ def wrapper(*args, **kwargs):
try:
op.adjoint()
except NotImplementedError:
# Decompose the operation and adjoint the result.
# Expand the operation and adjoint the result.
# We do not do anything with the output since
# decomposition will automatically queue the new operations.
adjoint(op.decomposition)(wires=op.wires)
# calling adjoint on the expansion will automatically
# queue the new operations.
adjoint(op.expand)()
josh146 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

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.


return wrapper
13 changes: 7 additions & 6 deletions pennylane/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,9 @@ def circuit2():
"Please use inv on the function including its arguments, as in inv(template(args))."
)
elif isinstance(operation_list, qml.tape.QuantumTape):
operation_list.inv()
return operation_list
new_tape = operation_list.adjoint()
return new_tape

elif not isinstance(operation_list, Iterable):
raise ValueError("The provided operation_list is not iterable.")

Expand Down Expand Up @@ -334,13 +335,13 @@ def circuit2():
# exist on the queuing context
pass

with qml.tape.QuantumTape() as tape:
def qfunc():
for o in operation_list:
o.queue()
if o.inverse:
o.inv()

tape.inv()
with qml.tape.QuantumTape() as tape:
qml.adjoint(qfunc)()

return tape


Expand Down
2 changes: 1 addition & 1 deletion tests/devices/test_default_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Contributor

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)

Copy link
Member Author

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.

Copy link
Contributor

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! 🙂


expected = np.array([1.0, -1.0j]) / np.sqrt(2)
assert np.allclose(dev.state, expected, atol=tol, rtol=0)
Expand Down
1 change: 0 additions & 1 deletion tests/ops/test_qubit_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,6 @@ def test_adjoint_with_decomposition(self, op_builder):
"op",
[
qml.BasisState(np.array([0, 1]), wires=0),
qml.QubitStateVector(np.array([1.0, 0.0]), wires=0),
],
)
def test_adjoint_error_exception(self, op, tol):
Expand Down
10 changes: 5 additions & 5 deletions tests/tape/test_tape.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,12 +604,12 @@ def test_inverse(self):
tape.inv()

# check that operation order is reversed
assert tape.operations == [prep] + ops[::-1]
assert [o.name for o in tape.operations] == [
o.name.replace(".inv", "") for o in [prep] + ops[::-1]
]
josh146 marked this conversation as resolved.
Show resolved Hide resolved

# check that operations are inverted
assert ops[0].inverse
assert not ops[1].inverse
assert ops[2].inverse
assert np.allclose(tape.operations[2].parameters, -np.array(p[-1:0:-1]))

# check that parameter order has reversed
assert tape.get_parameters() == [init_state, p[1], p[2], p[3], p[0]]
Expand All @@ -636,7 +636,7 @@ def test_parameter_transforms(self):
tape.inv()
assert tape.trainable_params == {1, 2}
assert tape.get_parameters() == [p[0], p[1]]
assert tape._ops == ops
assert [o.name for o in tape._ops] == [o.name.replace(".inv", "") for o in ops]
josh146 marked this conversation as resolved.
Show resolved Hide resolved


class TestExpand:
Expand Down
2 changes: 1 addition & 1 deletion tests/templates/test_layers/test_particle_conserving_u1.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_operations(self):
assert gate_count == len(queue)

# check initialization of the qubit register
assert isinstance(queue[0], qml.BasisState)
assert isinstance(queue[0], qml.templates.BasisEmbedding)

# check all quantum operations
idx_CRot = 8
Expand Down
2 changes: 1 addition & 1 deletion tests/templates/test_layers/test_particle_conserving_u2.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_operations(self, layers, qubits, init_state):
assert len(queue) == n_gates

# initialization
assert isinstance(queue[0], qml.BasisState)
assert isinstance(queue[0], qml.templates.BasisEmbedding)

# order of gates
for op1, op2 in zip(queue[1:], exp_gates):
Expand Down
Loading