Release 0.13.0
New features since last release
Automatically optimize the number of measurements
-
QNodes in tape mode now support returning observables on the same wire whenever the observables are qubit-wise commuting Pauli words. Qubit-wise commuting observables can be evaluated with a single device run as they are diagonal in the same basis, via a shared set of single-qubit rotations. (#882)
The following example shows a single QNode returning the expectation values of the qubit-wise commuting Pauli words
XX
andXI
:qml.enable_tape() @qml.qnode(dev) def f(x): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.CRot(0.1, 0.2, 0.3, wires=[1, 0]) qml.RZ(x, wires=1) return qml.expval(qml.PauliX(0) @ qml.PauliX(1)), qml.expval(qml.PauliX(0))
>>> f(0.4) tensor([0.89431013, 0.9510565 ], requires_grad=True)
-
The
ExpvalCost
class (previouslyVQECost
) now provides observable optimization using theoptimize
argument, resulting in potentially fewer device executions. (#902)This is achieved by separating the observables composing the Hamiltonian into qubit-wise commuting groups and evaluating those groups on a single QNode using functionality from the
qml.grouping
module:qml.enable_tape() commuting_obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1)] H = qml.vqe.Hamiltonian([1, 1], commuting_obs) dev = qml.device("default.qubit", wires=2) ansatz = qml.templates.StronglyEntanglingLayers cost_opt = qml.ExpvalCost(ansatz, H, dev, optimize=True) cost_no_opt = qml.ExpvalCost(ansatz, H, dev, optimize=False) params = qml.init.strong_ent_layers_uniform(3, 2)
Grouping these commuting observables leads to fewer device executions:
>>> cost_opt(params) >>> ex_opt = dev.num_executions >>> cost_no_opt(params) >>> ex_no_opt = dev.num_executions - ex_opt >>> print("Number of executions:", ex_no_opt) Number of executions: 2 >>> print("Number of executions (optimized):", ex_opt) Number of executions (optimized): 1
New quantum gradient features
-
Compute the analytic gradient of quantum circuits in parallel on supported devices. (#840)
This release introduces support for batch execution of circuits, via a new device API method
Device.batch_execute()
. Devices that implement this new API support submitting a batch of circuits for parallel evaluation simultaneously, which can significantly reduce the computation time.Furthermore, if using tape mode and a compatible device, gradient computations will automatically make use of the new batch API---providing a speedup during optimization.
-
Gradient recipes are now much more powerful, allowing for operations to define their gradient via an arbitrary linear combination of circuit evaluations. (#909) (#915)
With this change, gradient recipes can now be of the form
\frac{\partial}{\partial\phi_k}f(\phi_k) = \sum_{i} c_i f(a_i \phi_k + s_i )
, and are no longer restricted to two-term shifts with identical (but opposite in sign) shift values.As a result, PennyLane now supports native analytic quantum gradients for the controlled rotation operations
CRX
,CRY
,CRZ
, andCRot
. This allows for parameter-shift analytic gradients on hardware, without decomposition.Note that this is a breaking change for developers; please see the Breaking Changes section for more details.
-
The
qnn.KerasLayer
class now supports differentiating the QNode through classical backpropagation in tape mode. (#869)qml.enable_tape() dev = qml.device("default.qubit.tf", wires=2) @qml.qnode(dev, interface="tf", diff_method="backprop") def f(inputs, weights): qml.templates.AngleEmbedding(inputs, wires=range(2)) qml.templates.StronglyEntanglingLayers(weights, wires=range(2)) return [qml.expval(qml.PauliZ(i)) for i in range(2)] weight_shapes = {"weights": (3, 2, 3)} qlayer = qml.qnn.KerasLayer(f, weight_shapes, output_dim=2) inputs = tf.constant(np.random.random((4, 2)), dtype=tf.float32) with tf.GradientTape() as tape: out = qlayer(inputs) tape.jacobian(out, qlayer.trainable_weights)
New operations, templates, and measurements
-
Adds the
qml.density_matrix
QNode return with partial trace capabilities. (#878)The density matrix over the provided wires is returned, with all other subsystems traced out.
qml.density_matrix
currently works for both thedefault.qubit
anddefault.mixed
devices.qml.enable_tape() dev = qml.device("default.qubit", wires=2) def circuit(x): qml.PauliY(wires=0) qml.Hadamard(wires=1) return qml.density_matrix(wires=[1]) # wire 0 is traced out
-
Adds the square-root X gate
SX
. (#871)dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(): qml.SX(wires=[0]) return qml.expval(qml.PauliZ(wires=[0]))
-
Two new hardware-efficient particle-conserving templates have been implemented to perform VQE-based quantum chemistry simulations. The new templates apply several layers of the particle-conserving entanglers proposed in Figs. 2a and 2b of Barkoutsos et al., arXiv:1805.04340 (#875) (#876)
Estimate and track resources
-
The
QuantumTape
class now contains basic resource estimation functionality. The methodtape.get_resources()
returns a dictionary with a list of the constituent operations and the number of times they appear in the circuit. Similarly,tape.get_depth()
computes the circuit depth. (#862)>>> with qml.tape.QuantumTape() as tape: ... qml.Hadamard(wires=0) ... qml.RZ(0.26, wires=1) ... qml.CNOT(wires=[1, 0]) ... qml.Rot(1.8, -2.7, 0.2, wires=0) ... qml.Hadamard(wires=1) ... qml.CNOT(wires=[0, 1]) ... qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) >>> tape.get_resources() {'Hadamard': 2, 'RZ': 1, 'CNOT': 2, 'Rot': 1} >>> tape.get_depth() 4
-
The number of device executions over a QNode's lifetime can now be returned using
num_executions
. (#853)>>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev) ... def circuit(x, y): ... qml.RX(x, wires=[0]) ... qml.RY(y, wires=[1]) ... qml.CNOT(wires=[0, 1]) ... return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) >>> for _ in range(10): ... circuit(0.432, 0.12) >>> print(dev.num_executions) 10
Improvements
-
Support for tape mode has improved across PennyLane. The following features now work in tape mode:
-
A new function,
qml.refresh_devices()
, has been added, allowing PennyLane to rescan installed PennyLane plugins and refresh the device list. In addition, theqml.device
loader will attempt to refresh devices if the required plugin device cannot be found. This will result in an improved experience if installing PennyLane and plugins within a running Python session (for example, on Google Colab), and avoid the need to restart the kernel/runtime. (#907) -
When using
grad_fn = qml.grad(cost)
to compute the gradient of a cost function with the Autograd interface, the value of the intermediate forward pass is now available via thegrad_fn.forward
property (#914):def cost_fn(x, y): return 2 * np.sin(x[0]) * np.exp(-x[1]) + x[0] ** 3 + np.cos(y) params = np.array([0.1, 0.5], requires_grad=True) data = np.array(0.65, requires_grad=False) grad_fn = qml.grad(cost_fn) grad_fn(params, data) # perform backprop and evaluate the gradient grad_fn.forward # the cost function value
-
Gradient-based optimizers now have a
step_and_cost
method that returns both the next step as well as the objective (cost) function output. (#916)>>> opt = qml.GradientDescentOptimizer() >>> params, cost = opt.step_and_cost(cost_fn, params)
-
PennyLane provides a new experimental module
qml.proc
which provides framework-agnostic processing functions for array and tensor manipulations. (#886)Given the input tensor-like object, the call is dispatched to the corresponding array manipulation framework, allowing for end-to-end differentiation to be preserved.
>>> x = torch.tensor([1., 2.]) >>> qml.proc.ones_like(x) tensor([1, 1]) >>> y = tf.Variable([[0], [5]]) >>> qml.proc.ones_like(y, dtype=np.complex128) <tf.Tensor: shape=(2, 1), dtype=complex128, numpy= array([[1.+0.j], [1.+0.j]])>
Note that these functions are experimental, and only a subset of common functionality is supported. Furthermore, the names and behaviour of these functions may differ from similar functions in common frameworks; please refer to the function docstrings for more details.
-
The gradient methods in tape mode now fully separate the quantum and classical processing. Rather than returning the evaluated gradients directly, they now return a tuple containing the required quantum and classical processing steps. (#840)
def gradient_method(idx, param, **options): # generate the quantum tapes that must be computed # to determine the quantum gradient tapes = quantum_gradient_tapes(self) def processing_fn(results): # perform classical processing on the evaluated tapes # returning the evaluated quantum gradient return classical_processing(results) return tapes, processing_fn
The
JacobianTape.jacobian()
method has been similarly modified to accumulate all gradient quantum tapes and classical processing functions, evaluate all quantum tapes simultaneously, and then apply the post-processing functions to the evaluated tape results. -
The MultiRZ gate now has a defined generator, allowing it to be used in quantum natural gradient optimization. (#912)
-
The CRot gate now has a
decomposition
method, which breaks the gate down into rotations and CNOT gates. This allowsCRot
to be used on devices that do not natively support it. (#908) -
The classical processing in the
MottonenStatePreparation
template has been largely rewritten to use dense matrices and tensor manipulations wherever possible. This is in preparation to support differentiation through the template in the future. (#864) -
Device-based caching has replaced QNode caching. Caching is now accessed by passing a
cache
argument to the device. (#851)The
cache
argument should be an integer specifying the size of the cache. For example, a cache of size 10 is created using:>>> dev = qml.device("default.qubit", wires=2, cache=10)
-
The
Operation
,Tensor
, andMeasurementProcess
classes now have the__copy__
special method defined. (#840)This allows us to ensure that, when a shallow copy is performed of an operation, the mutable list storing the operation parameters is also shallow copied. Both the old operation and the copied operation will continue to share the same parameter data,
>>> import copy >>> op = qml.RX(0.2, wires=0) >>> op2 = copy.copy(op) >>> op.data[0] is op2.data[0] True
however the list container is not a reference:
>>> op.data is op2.data False
This allows the parameters of the copied operation to be modified, without mutating the parameters of the original operation.
-
The
QuantumTape.copy
method has been tweaked so that (#840):-
Optionally, the tape's operations are shallow copied in addition to the tape by passing the
copy_operations=True
boolean flag. This allows the copied tape's parameters to be mutated without affecting the original tape's parameters. (Note: the two tapes will share parameter data until one of the tapes has their parameter list modified.) -
Copied tapes can be cast to another
QuantumTape
subclass by passing thetape_cls
keyword argument.
-
Breaking changes
-
Updated how parameter-shift gradient recipes are defined for operations, allowing for gradient recipes that are specified as an arbitrary number of terms. (#909)
Previously,
Operation.grad_recipe
was restricted to two-term parameter-shift formulas. With this change, the gradient recipe now contains elements of the form[c_i, a_i, s_i]
, resulting in a gradient recipe of\frac{\partial}{\partial\phi_k}f(\phi_k) = \sum_{i} c_i f(a_i \phi_k + s_i )
.As this is a breaking change, all custom operations with defined gradient recipes must be updated to continue working with PennyLane 0.13. Note though that if
grad_recipe = None
, the default gradient recipe remains unchanged, and corresponds to the two terms[c_0, a_0, s_0]=[1/2, 1, \pi/2]
and[c_1, a_1, s_1]=[-1/2, 1, -\pi/2]
for every parameter.
- The
VQECost
class has been renamed toExpvalCost
to reflect its general applicability beyond VQE. Use ofVQECost
is still possible but will result in a deprecation warning. (#913)
Bug fixes
-
The
default.qubit.tf
device is updated to handle TensorFlow objects (e.g.,tf.Variable
) as gate parameters correctly when using theMultiRZ
andCRot
operations. (#921) -
PennyLane tensor objects are now unwrapped in BaseQNode when passed as a keyword argument to the quantum function. (#903) (#893)
-
The new tape mode now prevents multiple observables from being evaluated on the same wire if the observables are not qubit-wise commuting Pauli words. (#882)
-
Fixes a bug in
default.qubit
whereby inverses of common gates were not being applied via efficient gate-specific methods, instead falling back to matrix-vector multiplication. The following gates were affected:PauliX
,PauliY
,PauliZ
,Hadamard
,SWAP
,S
,T
,CNOT
,CZ
. (#872) -
The
PauliRot
operation now gracefully handles single-qubit Paulis, and all-identity Paulis (#860). -
Fixes a bug whereby binary Python operators were not properly propagating the
requires_grad
attribute to the output tensor. (#889) -
Fixes a bug which prevents
TorchLayer
from doingbackward
when CUDA is enabled. (#899) -
Fixes a bug where multi-threaded execution of
QNodeCollection
sometimes fails because of simultaneous queuing. This is fixed by adding thread locking during queuing. (#910) -
Fixes a bug in
QuantumTape.set_parameters()
. The previous implementation assumed that theself.trainable_parms
set would always be iterated over in increasing integer order. However, this is not guaranteed behaviour, and can lead to the incorrect tape parameters being set if this is not the case. (#923) -
Fixes broken error message if a QNode is instantiated with an unknown exception. (#930)
Contributors
This release contains contributions from (in alphabetical order):
Juan Miguel Arrazola, Thomas Bromley, Christina Lee, Alain Delgado Gran, Olivia Di Matteo, Anthony Hayes, Theodor Isacsson, Josh Izaac, Soran Jahangiri, Nathan Killoran, Shumpei Kobayashi, Romain Moyard, Zeyue Niu, Maria Schuld, Antal Száva.