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

Make tape mode default #1040

Merged
merged 43 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4df4002
fixes so far
josh146 Jan 26, 2021
5951d93
more
josh146 Jan 26, 2021
973dc5b
more
josh146 Jan 26, 2021
016ea50
Update
josh146 Jan 26, 2021
eea1885
openqasm
josh146 Jan 26, 2021
573daa3
inverse
josh146 Jan 26, 2021
ff4aa03
black
josh146 Jan 26, 2021
2e1ae19
black
josh146 Jan 26, 2021
20c1b36
fixes
josh146 Jan 26, 2021
1d63f2c
more tests passing
josh146 Jan 26, 2021
bba5743
more
josh146 Jan 26, 2021
db93789
Update pennylane/tape/__init__.py
josh146 Jan 26, 2021
e470446
more
josh146 Jan 26, 2021
5ca97e4
Merge branch 'make-tape-default' of github.com:PennyLaneAI/pennylane …
josh146 Jan 26, 2021
1930dfa
more
josh146 Jan 26, 2021
a4f91cb
remove enable_tape from docs
josh146 Jan 26, 2021
3416709
sampling fixes
josh146 Jan 26, 2021
dc5f45d
Merge branch 'master' into make-tape-default
josh146 Jan 26, 2021
4de4e28
more
josh146 Jan 26, 2021
a0c00c7
Merge branch 'make-tape-default' of github.com:PennyLaneAI/pennylane …
josh146 Jan 26, 2021
14149d4
add test
josh146 Jan 26, 2021
883591b
Merge branch 'master' into make-tape-default
antalszava Jan 26, 2021
52fdd9d
Apply suggestions from code review
josh146 Jan 27, 2021
5372913
Update tests/beta/test_default_tensor_tf.py
josh146 Jan 27, 2021
992bdbc
suggested changes
josh146 Jan 27, 2021
8c8c165
Merge branch 'make-tape-default' of github.com:PennyLaneAI/pennylane …
josh146 Jan 27, 2021
826aca1
revert max-depth to 2
josh146 Jan 27, 2021
8d367e0
Merge branch 'master' into make-tape-default
josh146 Jan 27, 2021
05676ac
remove import
josh146 Jan 27, 2021
d06ad0e
Merge branch 'make-tape-default' of github.com:PennyLaneAI/pennylane …
josh146 Jan 27, 2021
3768dd7
bug fix
josh146 Jan 28, 2021
77864ca
lint
josh146 Jan 28, 2021
20e469a
lint
josh146 Jan 28, 2021
027c2df
fix docstrings
josh146 Jan 28, 2021
206fbe3
fix templates
josh146 Jan 28, 2021
bc8dfbf
fix templates
josh146 Jan 28, 2021
3580382
Merge branch 'master' into make-tape-default
mariaschuld Jan 28, 2021
345be59
Tape hash (#1041)
antalszava Jan 28, 2021
54fc9de
no PL import in tape circuit graph
antalszava Jan 28, 2021
681d739
Update tests/beta/test_default_tensor_tf.py
josh146 Jan 28, 2021
6c8bfc4
Update pennylane/circuit_graph.py
josh146 Jan 28, 2021
8156079
Update pennylane/_qubit_device.py
josh146 Jan 28, 2021
51a4120
Merge branch 'master' into make-tape-default
josh146 Jan 28, 2021
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
2 changes: 1 addition & 1 deletion doc/code/qml_tape.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,4 @@ For more details and examples, please see the tape documentation.
.. automodapi:: pennylane.tape
:no-main-docstr:
:include-all-objects:
:skip: enable_tape, disable_tape
:skip: QNode, qnode, enable_tape, disable_tape
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 that we are skipping these two? There seems to be other new objects too like TapeCircuitGraph, would we like to skip to show info about them once we deprecate the old core?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of all the mocking done by qml.enable_tape(), qml.qnode, qml.QNode, and qml.CircuitGraph all actually point to the pennylane/tape versions 🙂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I am skipping them from showing at the tape module level, as they will instead be displayed at the qml level

3 changes: 3 additions & 0 deletions pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,6 @@ def circuit():
def version():
"""Returns the PennyLane version number."""
return __version__


enable_tape()
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 is placed at the bottom of the __init__ file, to allow the import logic to remain in non-tape mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that necessary so that non-tape mode can still be accessible through qml.disable_tape()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is because a lot of the logic that is executed above this isn't tape-mode compatible yet 😆

I initially added qml.enable_tape to the top of the __init__.py file, but it requires a lot of changes throughout the code base to get it to work. I figured it would be too hard to code-review, and might introduce bugs.

4 changes: 3 additions & 1 deletion pennylane/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import numpy as np

import pennylane as qml
from pennylane.operation import (
Operation,
Observable,
Expand Down Expand Up @@ -550,6 +551,8 @@ def check_validity(self, queue, observables):
)

for o in observables:
if isinstance(o, qml.tape.MeasurementProcess) and o.obs is not None:
o = o.obs

if isinstance(o, Tensor):
# TODO: update when all capabilities keys changed to "supports_tensor_observables"
Expand All @@ -569,7 +572,6 @@ def check_validity(self, queue, observables):
)
)
else:

observable_name = o.name

if issubclass(o.__class__, Operation) and o.inverse:
Expand Down
9 changes: 3 additions & 6 deletions pennylane/_qubit_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,10 @@ def execute(self, circuit, **kwargs):
# compute the required statistics
results = self.statistics(circuit.observables)

# Ensures that a combination with sample does not put
# expvals and vars in superfluous arrays
all_sampled = all(obs.return_type is Sample for obs in circuit.observables)
if circuit.is_sampled and not all_sampled:
results = self._asarray(results, dtype="object")
else:
if circuit.all_sampled or not circuit.is_sampled:
results = self._asarray(results)
else:
results = tuple([self._asarray(r) for r in results])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this tuple created later down in non-tape mode? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is a complete fix to how QNodes were returning samples.

Previously, if QNodes had a combination of samples and expvals, a ragged object array would be returned:

import pennylane as qml
import numpy as np

dev = qml.device('default.qubit', wires=3, shots=10)

@qml.qnode(dev, interface="tf")
def circuit(weights):
    qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1, 2])
    return qml.sample(qml.PauliZ(0)), qml.var(qml.PauliX(1))

However, this is deprecated behaviour in NumPy, and worse, this is inconsistent across interfaces --- tensorflow doesn't support ragged tensor outputs, for example.

A better approach is to simply return a tuple of tensors/arrays if we know the output shape will be ragged:

>>> weights = qml.init.strong_ent_layers_normal(n_wires=3, n_layers=2)
>>> print(circuit(weights))
(<tf.Tensor: shape=(10,), dtype=int64, numpy=array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])>,
 <tf.Tensor: shape=(), dtype=float64, numpy=0.9583473628726024>)

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 issue was uncovered due to failing sampling tests using the new backprop diff method)

josh146 marked this conversation as resolved.
Show resolved Hide resolved

if self._cache and circuit_hash not in self._cache_execute:
self._cache_execute[circuit_hash] = results
Expand Down
191 changes: 17 additions & 174 deletions pennylane/beta/devices/default_tensor_tf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@
raise ImportError("default.tensor.tf device requires TensorFlow>=2.0")

except ImportError as e:
raise ImportError("default.tensor.tf device requires TensorFlow>=2.0")
raise ImportError("default.tensor.tf device requires TensorFlow>=2.0") from e

from pennylane.variable import Variable
from pennylane.beta.devices.default_tensor import DefaultTensor
from pennylane.devices import tf_ops as ops

Expand All @@ -54,50 +53,25 @@ class DefaultTensorTF(DefaultTensor):

**Example**

The ``default.tensor.tf`` device supports various differentiation modes.
The ``default.tensor.tf`` device supports end-to-end classical backpropagation with the TensorFlow interface.

* *End-to-end classical backpropagation with the TensorFlow interface*.
Using this method, the created QNode is a 'white-box', and is
tightly integrated with your TensorFlow computation:
Using this method, the created QNode is a 'white-box', and is
tightly integrated with your TensorFlow computation:

>>> dev = qml.device("default.tensor.tf", wires=1)
>>> @qml.qnode(dev, interface="tf", diff_method="backprop")
>>> def circuit(x):
... qml.RX(x[1], wires=0)
... qml.Rot(x[0], x[1], x[2], wires=0)
... return qml.expval(qml.PauliZ(0))
>>> vars = tf.Variable([0.2, 0.5, 0.1])
>>> with tf.GradientTape() as tape:
... res = circuit(vars)
>>> tape.gradient(res, vars)
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.2526717e-01, -1.0086454e+00, 1.3877788e-17], dtype=float32)>
>>> dev = qml.device("default.tensor.tf", wires=1)
>>> @qml.qnode(dev, interface="tf", diff_method="backprop")
>>> def circuit(x):
... qml.RX(x[1], wires=0)
... qml.Rot(x[0], x[1], x[2], wires=0)
... return qml.expval(qml.PauliZ(0))
>>> vars = tf.Variable([0.2, 0.5, 0.1])
>>> with tf.GradientTape() as tape:
... res = circuit(vars)
>>> tape.gradient(res, vars)
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.2526717e-01, -1.0086454e+00, 1.3877788e-17], dtype=float32)>

In this mode, you must use the ``"tf"`` interface, as TensorFlow
is used as the device backend.

* *Device differentiation*. Using this method, the created QNode
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed device differentiation from the default.tensor.tf device, since it relies on the old Variable class (and it is superseded by adjoint diff)

is a 'black-box' to your classical computation. PennyLane will automatically
accept classical tensors from any supported interface, and query the
device directly for the quantum gradient when required.

>>> dev = qml.device("default.tensor.tf", wires=1)
>>> @qml.qnode(dev, interface="autograd", diff_method="device")
>>> def circuit(x):
... qml.RX(x[1], wires=0)
... qml.Rot(x[0], x[1], x[2], wires=0)
... return qml.expval(qml.PauliZ(0))
>>> grad_fn = qml.grad(circuit, argnum=[0])
>>> print(grad_fn([0.2, 0.5, 0.1]))
([array(-0.22526717), array(-1.00864546), array(6.9388939e-18)],)

In this mode, even though TensorFlow is used as the device backend, it
is independent of the chosen QNode interface. In the example above, we combine
``default.tensor.tf`` with the ``autograd`` interface.
It can also be used with the ``torch`` and the ``tf`` interface.

In addition to end-to-end classical backpropagation and device differentiation,
the ``default.tensor.tf`` device also supports ``parameter-shift`` and
``finite-diff`` differentiation methods.
In this mode, you must use the ``"tf"`` interface, as TensorFlow
is used as the device backend.

Args:
wires (int): number of subsystems in the quantum state represented by the device
Expand Down Expand Up @@ -144,141 +118,10 @@ class DefaultTensorTF(DefaultTensor):
C_DTYPE = ops.C_DTYPE
R_DTYPE = ops.R_DTYPE

def __init__(self, wires, shots=1000, representation="exact", contraction_method="auto"):
self.variables = []
"""List[tf.Variable]: Free parameters, cast to TensorFlow variables,
for this circuit."""

self.res = None
"""tf.tensor[tf.float64]: result from the last circuit execution"""

self.op_params = {}
"""dict[Operation, List[Any, tf.Variable]]: A mapping from each operation
in the queue, to the corresponding list of parameter values. These
values can be Python numeric types, NumPy arrays, or TensorFlow variables."""

self.tape = None
"""tf.GradientTape: the gradient tape under which all tensor network
modifications must be made"""

super().__init__(
wires,
shots=shots,
representation=representation,
contraction_method=contraction_method,
)

@classmethod
def capabilities(cls):
capabilities = super().capabilities().copy()
capabilities.update(
provides_jacobian=True,
passthru_interface="tf",
)
return capabilities

def reset(self):
self.res = None
self.variables = []
super().reset()

def execution_context(self):
self.tape = tf.GradientTape(persistent=True)
return self.tape

def pre_apply(self):
super().pre_apply()

self.op_params = {}

for operation in self.op_queue:
# Copy the operation parameters to the op_params dictionary.
# Note that these are the unwrapped parameters, so PennyLane
# free parameters will be represented as Variable instances.
self.op_params[operation] = operation.data[:]

# Loop through the free parameter reference dictionary
for _, par_dep_list in self.parameters.items():
if not par_dep_list:
# parameter is not used within circuit
v = tf.Variable(0, dtype=self.R_DTYPE)
self.variables.append(v)
continue

# get the first parameter dependency for each free parameter
first = par_dep_list[0]

# For the above parameter dependency, get the corresponding
# operation parameter variable, and get the numeric value.
# Convert the resulting value to a TensorFlow tensor.
val = first.op.data[first.par_idx].val
mult = first.op.data[first.par_idx].mult
v = tf.Variable(val / mult, dtype=self.R_DTYPE)

# Mark the variable to be watched by the gradient tape,
# and append it to the variable list.
self.variables.append(v)

for p in par_dep_list:
# Replace the existing Variable free parameter in the op_params dictionary
# with the corresponding tf.Variable parameter.
# Note that the free parameter might be scaled by the
# variable.mult scaling factor.
mult = p.op.data[p.par_idx].mult
self.op_params[p.op][p.par_idx] = v * mult

# check that no Variables remain in the op_params dictionary
values = [item for sublist in self.op_params.values() for item in sublist]
assert not any(
isinstance(v, Variable) for v in values
), "A pennylane.Variable instance was not correctly converted to a tf.Variable"

# flatten the variables list in case of nesting
self.variables = tf.nest.flatten(self.variables)
self.tape.watch(self.variables)

for operation in self.op_queue:
# Apply each operation, but instead of passing operation.parameters
# (which contains the evaluated numeric parameter values),
# pass op_params[operation], which contains numeric values
# for fixed parameters, and tf.Variable objects for free parameters.
super().apply(operation.name, operation.wires, self.op_params[operation])

def apply(self, operation, wires, par):
# individual operations are already applied inside self.pre_apply()
pass

def execute(self, queue, observables, parameters=None, **kwargs):
# pylint: disable=bad-super-call
results = super(DefaultTensor, self).execute(queue, observables, parameters=parameters)

with self.tape:
# convert the results list into a single tensor
self.res = tf.stack(results)

if kwargs.get("return_native_type", False):
return self.res
# return the results as a NumPy array
return self.res.numpy()

def jacobian(self, queue, observables, parameters):
"""Calculates the Jacobian of the device circuit using TensorFlow
backpropagation.

Args:
queue (list[Operation]): operations to be applied to the device
observables (list[Observable]): observables to be measured
parameters (dict[int, ParameterDependency]): reference dictionary
mapping free parameter values to the operations that
depend on them

Returns:
array[float]: Jacobian matrix of size (``num_params``, ``num_wires``)
"""
self.reset()
self.execute(queue, observables, parameters=parameters)
jac = self.tape.jacobian(self.res, self.variables, experimental_use_pfor=False)
# TODO use unconnected_gradients=tf.UnconnectedGradients.ZERO instead of the following?
jac = [i if i is not None else tf.zeros(self.res.shape, dtype=tf.float64) for i in jac]
jac = tf.stack(jac)
return jac.numpy().T
3 changes: 3 additions & 0 deletions pennylane/circuit_drawer/representation_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ def operator_representation(self, op, wire):
Returns:
str: String representation of the Operator
"""
if isinstance(op, qml.tape.MeasurementProcess) and op.obs is not None:
op = op.obs

Comment on lines +319 to +321
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bug uncovered

if isinstance(op, qml.operation.Tensor):
constituent_representations = [
self.operator_representation(tensor_obs, wire) for tensor_obs in op.obs
Expand Down
51 changes: 32 additions & 19 deletions pennylane/circuit_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def hash(self):
"""
return hash(self.serialize())

def to_openqasm(self, rotations=True):
def to_openqasm(self, wires=None, rotations=True):
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 method now has the same issue re: device wires as the circuit drawer :)

"""Serialize the circuit as an OpenQASM 2.0 program.

Only operations are serialized; all measurements
Expand All @@ -265,6 +265,7 @@ def to_openqasm(self, rotations=True):
in ``qelib1.inc`` are available.

Args:
wires (Wires or None): the wires to use when serializing the circuit
rotations (bool): in addition to serializing user-specified
operations, also include the gates that diagonalize the
measured wires such that they are in the eigenbasis of the circuit observables.
Expand All @@ -273,16 +274,7 @@ def to_openqasm(self, rotations=True):
str: OpenQASM serialization of the circuit
"""
# We import decompose_queue here to avoid a circular import
from pennylane.qnodes.base import decompose_queue # pylint: disable=import-outside-toplevel

class QASMSerializerDevice:
"""A mock device, to be used when performing the decomposition.
The short_name is used in error messages if the decomposition fails.
"""

# pylint: disable=too-few-public-methods
short_name = "QASM serializer"
supports_operation = staticmethod(lambda x: x in OPENQASM_GATES)
wires = wires or self.wires

# add the QASM headers
qasm_str = "OPENQASM 2.0;\n"
Expand All @@ -293,8 +285,8 @@ class QASMSerializerDevice:
return qasm_str

# create the quantum and classical registers
qasm_str += "qreg q[{}];\n".format(self.num_wires)
qasm_str += "creg c[{}];\n".format(self.num_wires)
qasm_str += "qreg q[{}];\n".format(len(wires))
qasm_str += "creg c[{}];\n".format(len(wires))

# get the user applied circuit operations
operations = self.operations
Expand All @@ -304,28 +296,41 @@ class QASMSerializerDevice:
# to circuit observables
operations += self.diagonalizing_gates

with qml.tape.QuantumTape() as tape:
for op in operations:
op.queue()

if op.inverse:
op.inv()

# decompose the queue
decomposed_ops = decompose_queue(operations, QASMSerializerDevice)
operations = tape.expand(stop_at=lambda obj: obj.name in OPENQASM_GATES).operations

# create the QASM code representing the operations
for op in decomposed_ops:
gate = OPENQASM_GATES[op.name]
wires = ",".join(["q[{}]".format(self.wires.index(w)) for w in op.wires.tolist()])
for op in operations:
try:
gate = OPENQASM_GATES[op.name]
except KeyError as e:
raise ValueError(f"Operation {op.name} not supported by the QASM serializer") from e

wire_labels = ",".join(["q[{}]".format(wires.index(w)) for w in op.wires.tolist()])
josh146 marked this conversation as resolved.
Show resolved Hide resolved
params = ""

if op.num_params > 0:
# If the operation takes parameters, construct a string
# with parameter values.
params = "(" + ",".join([str(p) for p in op.parameters]) + ")"

qasm_str += "{name}{params} {wires};\n".format(name=gate, params=params, wires=wires)
qasm_str += "{name}{params} {wires};\n".format(
name=gate, params=params, wires=wire_labels
)

# apply computational basis measurements to each quantum register
# NOTE: This is not strictly necessary, we could inspect self.observables,
# and then only measure wires which are requested by the user. However,
# some devices which consume QASM require all registers be measured, so
# measure all wires to be safe.
for wire in range(self.num_wires):
for wire in range(len(wires)):
qasm_str += "measure q[{wire}] -> c[{wire}];\n".format(wire=wire)

return qasm_str
Expand Down Expand Up @@ -701,4 +706,12 @@ def diagonalizing_gates(self):
def is_sampled(self):
"""Returns ``True`` if the circuit graph contains observables
which are sampled."""
# TODO: remove when tape is core
return any(obs.return_type == Sample for obs in self.observables_in_order)

@property
def all_sampled(self):
"""Returns ``True`` if the circuit graph contains observables
which are sampled."""
# TODO: remove when tape is core
return all(obs.return_type == Sample for obs in self.observables_in_order)
1 change: 0 additions & 1 deletion pennylane/devices/default_qubit_jax.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class DefaultQubitJax(DefaultQubit):
Using this method, the created QNode is a 'white-box', and is
tightly integrated with your JAX computation:

>>> qml.enable_tape()
>>> dev = qml.device("default.qubit.jax", wires=1)
>>> @qml.qnode(dev, interface="jax", diff_method="backprop")
... def circuit(x):
Expand Down
Loading