Skip to content

Commit

Permalink
Fix inversion of templates (#1243)
Browse files Browse the repository at this point in the history
* fix inversion of templates

* fix

* linting

* more tests

* fix tests

* fix failing tests

* changelog

* suggested changes

* fix sphinx

* update

* update

* Apply suggestions from code review

* rever

* typo

* typo

* Update tests/tape/test_tape.py

Co-authored-by: antalszava <[email protected]>

* Apply suggestions from code review

Co-authored-by: antalszava <[email protected]>

Co-authored-by: Maria Schuld <[email protected]>
Co-authored-by: antalszava <[email protected]>
  • Loading branch information
3 people authored Apr 29, 2021
1 parent fd60d39 commit ef87a8c
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 65 deletions.
6 changes: 5 additions & 1 deletion .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@

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

* Updated the order of the parameters to the `GaussianState` operation to match
Expand All @@ -58,7 +62,7 @@

This release contains contributions from (in alphabetical order):

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

# 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()

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): # pylint: disable=arguments-differ
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)

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

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]

expected = np.array([1.0, -1.0j]) / np.sqrt(2)
assert np.allclose(dev.state, expected, atol=tol, rtol=0)
Expand Down
8 changes: 3 additions & 5 deletions tests/tape/test_tape.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,12 +604,10 @@ 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] == ["BasisState", "CNOT", "Rot", "RX"]

# 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 +634,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] == ["RX", "Rot", "CNOT"]


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
58 changes: 29 additions & 29 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,8 @@ def inverted_dummy_template_operations(wires):
ops = []

for wire in reversed(wires):
ops.append(qml.RY(-1, wires=[wire]).inv())
ops.append(qml.RX(1, wires=[wire]).inv())
ops.append(qml.RY(1, wires=[wire]))
ops.append(qml.RX(-1, wires=[wire]))

return ops

Expand All @@ -475,7 +475,7 @@ class TestInv:
def test_inversion_without_context(self):
"""Test that a sequence of operations is properly inverted."""
op_queue = [qml.PauliX(0), qml.PauliY(0).inv(), qml.PauliZ(0)]
inv_queue = [qml.PauliZ(0).inv(), qml.PauliY(0), qml.PauliX(0).inv()]
inv_queue = [qml.PauliZ(0), qml.PauliY(0), qml.PauliX(0)]

inv_ops = pu.inv(op_queue).operations

Expand Down Expand Up @@ -527,9 +527,9 @@ def test_inversion_with_context(self):
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RZ(3, wires=[0]).inv(),
qml.RY(2, wires=[0]).inv(),
qml.RX(1, wires=[0]).inv(),
qml.RZ(-3, wires=[0]),
qml.RY(-2, wires=[0]),
qml.RX(-1, wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand All @@ -554,9 +554,9 @@ def test_non_queued_inversion_with_context(self):
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RZ(3, wires=[0]).inv(),
qml.RY(2, wires=[0]).inv(),
qml.RX(1, wires=[0]).inv(),
qml.RZ(-3, wires=[0]),
qml.RY(-2, wires=[0]),
qml.RX(-1, wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand All @@ -582,10 +582,10 @@ def test_mixed_inversion_with_context(self):
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RY(2, wires=[0]).inv(),
qml.PauliZ(0).inv(),
qml.RX(1, wires=[0]).inv(),
qml.PauliX(0).inv(),
qml.RY(-2, wires=[0]),
qml.PauliZ(0),
qml.RX(-1, wires=[0]),
qml.PauliX(0),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand All @@ -612,10 +612,10 @@ def test_mixed_inversion_with_nested_context(self):
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RY(2, wires=[0]).inv(),
qml.PauliZ(0).inv(),
qml.RX(1, wires=[0]).inv(),
qml.PauliX(0).inv(),
qml.RY(-2, wires=[0]),
qml.PauliZ(0),
qml.RX(-1, wires=[0]),
qml.PauliX(0),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand Down Expand Up @@ -669,9 +669,9 @@ def qfunc():
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RZ(3, wires=[0]).inv(),
qml.RY(2, wires=[0]).inv(),
qml.RX(1, wires=[0]).inv(),
qml.RZ(-3, wires=[0]),
qml.RY(-2, wires=[0]),
qml.RX(-1, wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand Down Expand Up @@ -703,9 +703,9 @@ def qfunc():
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RZ(3, wires=[0]).inv(),
qml.RY(2, wires=[0]).inv(),
qml.RX(1, wires=[0]).inv(),
qml.RZ(-3, wires=[0]),
qml.RY(-2, wires=[0]),
qml.RX(-1, wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand Down Expand Up @@ -737,10 +737,10 @@ def qfunc():
inv_queue = [
qml.Hadamard(wires=[0]),
qml.CNOT(wires=[0, 1]),
qml.RY(2, wires=[0]).inv(),
qml.PauliZ(0).inv(),
qml.RX(1, wires=[0]).inv(),
qml.PauliX(0).inv(),
qml.RY(-2, wires=[0]),
qml.PauliZ(0),
qml.RX(-1, wires=[0]),
qml.PauliX(0),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[0]),
]
Expand Down Expand Up @@ -783,8 +783,8 @@ def qfunc():

def test_argument_wrapping(self):
"""Test that a single operation can be given to inv and is properly inverted."""
op = qml.PauliX(0)
exp_op = qml.PauliX(0).inv()
op = qml.RX(0.1, wires=0)
exp_op = qml.RX(-0.1, wires=0)

inv_ops = pu.inv(op).operations

Expand Down
Loading

0 comments on commit ef87a8c

Please sign in to comment.