-
Notifications
You must be signed in to change notification settings - Fork 575
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
Make tape mode default #1040
Changes from 37 commits
4df4002
5951d93
973dc5b
016ea50
eea1885
573daa3
ff4aa03
2e1ae19
20c1b36
1d63f2c
bba5743
db93789
e470446
5ca97e4
1930dfa
a4f91cb
3416709
dc5f45d
4de4e28
a0c00c7
14149d4
883591b
52fdd9d
5372913
992bdbc
8c8c165
826aca1
8d367e0
05676ac
d06ad0e
3768dd7
77864ca
20e469a
027c2df
206fbe3
bc8dfbf
3580382
345be59
54fc9de
681d739
6c8bfc4
8156079
51a4120
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -230,3 +230,6 @@ def circuit(): | |
def version(): | ||
"""Returns the PennyLane version number.""" | ||
return __version__ | ||
|
||
|
||
enable_tape() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is placed at the bottom of the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this tuple created later down in non-tape mode? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed device differentiation from the |
||
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 | ||
|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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. | ||
|
@@ -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" | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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) |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
, andqml.CircuitGraph
all actually point to thepennylane/tape
versions 🙂There was a problem hiding this comment.
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 theqml
level