New features since last release
Perform quantum machine learning with JAX
-
QNodes created with
default.qubit
now support a JAX interface, allowing JAX to be used to create, differentiate, and optimize hybrid quantum-classical models. (#947)This is supported internally via a new
default.qubit.jax
device. This device runs end to end in JAX, meaning that it supports all of the awesome JAX transformations (jax.vmap
,jax.jit
,jax.hessian
, etc).Here is an example of how to use the new JAX interface:
dev = qml.device("default.qubit", wires=1) @qml.qnode(dev, interface="jax", 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)) weights = jnp.array([0.2, 0.5, 0.1]) grad_fn = jax.grad(circuit) print(grad_fn(weights))
Currently, only
diff_method="backprop"
is supported, with plans to support more in the future.
New, faster, quantum gradient methods
-
A new differentiation method has been added for use with simulators. The
"adjoint"
method operates after a forward pass by iteratively applying inverse gates to scan backwards through the circuit. (#1032)This method is similar to the reversible method, but has a lower time overhead and a similar memory overhead. It follows the approach provided by Jones and Gacon. This method is only compatible with certain statevector-based devices such as
default.qubit
.Example use:
import pennylane as qml wires = 1 device = qml.device("default.qubit", wires=wires) @qml.qnode(device, diff_method="adjoint") def f(params): qml.RX(0.1, wires=0) qml.Rot(*params, wires=0) qml.RX(-0.3, wires=0) return qml.expval(qml.PauliZ(0)) params = [0.1, 0.2, 0.3] qml.grad(f)(params)
-
The default logic for choosing the 'best' differentiation method has been altered to improve performance. (#1008)
-
If the quantum device provides its own gradient, this is now the preferred differentiation method.
-
If the quantum device natively supports classical backpropagation, this is now preferred over the parameter-shift rule.
This will lead to marked speed improvement during optimization when using
default.qubit
, with a sight penalty on the forward-pass evaluation.
More details are available below in the 'Improvements' section for plugin developers.
-
-
PennyLane now supports analytical quantum gradients for noisy channels, in addition to its existing support for unitary operations. The noisy channels
BitFlip
,PhaseFlip
, andDepolarizingChannel
all support analytic gradients out of the box. (#968) -
A method has been added for calculating the Hessian of quantum circuits using the second-order parameter shift formula. (#961)
The following example shows the calculation of the Hessian:
n_wires = 5 weights = [2.73943676, 0.16289932, 3.4536312, 2.73521126, 2.6412488] dev = qml.device("default.qubit", wires=n_wires) with qml.tape.QubitParamShiftTape() as tape: for i in range(n_wires): qml.RX(weights[i], wires=i) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[2, 1]) qml.CNOT(wires=[3, 1]) qml.CNOT(wires=[4, 3]) qml.expval(qml.PauliZ(1)) print(tape.hessian(dev))
The Hessian is not yet supported via classical machine learning interfaces, but will be added in a future release.
More operations and templates
-
Two new error channels,
BitFlip
andPhaseFlip
have been added. (#954)They can be used in the same manner as existing error channels:
dev = qml.device("default.mixed", wires=2) @qml.qnode(dev) def circuit(): qml.RX(0.3, wires=0) qml.RY(0.5, wires=1) qml.BitFlip(0.01, wires=0) qml.PhaseFlip(0.01, wires=1) return qml.expval(qml.PauliZ(0))
-
Apply permutations to wires using the
Permute
subroutine. (#952)import pennylane as qml dev = qml.device('default.qubit', wires=5) @qml.qnode(dev) def apply_perm(): # Send contents of wire 4 to wire 0, of wire 2 to wire 1, etc. qml.templates.Permute([4, 2, 0, 1, 3], wires=dev.wires) return qml.expval(qml.PauliZ(0))
QNode transformations
-
The
qml.metric_tensor
function transforms a QNode to produce the Fubini-Study metric tensor with full autodifferentiation support---even on hardware. (#1014)Consider the following QNode:
dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, interface="autograd") def circuit(weights): # layer 1 qml.RX(weights[0, 0], wires=0) qml.RX(weights[0, 1], wires=1) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) # layer 2 qml.RZ(weights[1, 0], wires=0) qml.RZ(weights[1, 1], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(2))
We can use the
metric_tensor
function to generate a new function, that returns the metric tensor of this QNode:>>> met_fn = qml.metric_tensor(circuit) >>> weights = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], requires_grad=True) >>> met_fn(weights) tensor([[0.25 , 0. , 0. , 0. ], [0. , 0.25 , 0. , 0. ], [0. , 0. , 0.0025, 0.0024], [0. , 0. , 0.0024, 0.0123]], requires_grad=True)
The returned metric tensor is also fully differentiable, in all interfaces. For example, differentiating the
(3, 2)
element:>>> grad_fn = qml.grad(lambda x: met_fn(x)[3, 2]) >>> grad_fn(weights) array([[ 0.04867729, -0.00049502, 0. ], [ 0. , 0. , 0. ]])
Differentiation is also supported using Torch, Jax, and TensorFlow.
-
Adds the new function
qml.math.cov_matrix()
. This function accepts a list of commuting observables, and the probability distribution in the shared observable eigenbasis after the application of an ansatz. It uses these to construct the covariance matrix in a framework independent manner, such that the output covariance matrix is autodifferentiable. (#1012)For example, consider the following ansatz and observable list:
obs_list = [qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(2)] ansatz = qml.templates.StronglyEntanglingLayers
We can construct a QNode to output the probability distribution in the shared eigenbasis of the observables:
dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, interface="autograd") def circuit(weights): ansatz(weights, wires=[0, 1, 2]) # rotate into the basis of the observables for o in obs_list: o.diagonalizing_gates() return qml.probs(wires=[0, 1, 2])
We can now compute the covariance matrix:
>>> weights = qml.init.strong_ent_layers_normal(n_layers=2, n_wires=3) >>> cov = qml.math.cov_matrix(circuit(weights), obs_list) >>> cov array([[0.98707611, 0.03665537], [0.03665537, 0.99998377]])
Autodifferentiation is fully supported using all interfaces:
>>> cost_fn = lambda weights: qml.math.cov_matrix(circuit(weights), obs_list)[0, 1] >>> qml.grad(cost_fn)(weights)[0] array([[[ 4.94240914e-17, -2.33786398e-01, -1.54193959e-01], [-3.05414996e-17, 8.40072236e-04, 5.57884080e-04], [ 3.01859411e-17, 8.60411436e-03, 6.15745204e-04]], [[ 6.80309533e-04, -1.23162742e-03, 1.08729813e-03], [-1.53863193e-01, -1.38700657e-02, -1.36243323e-01], [-1.54665054e-01, -1.89018172e-02, -1.56415558e-01]]])
-
A new
qml.draw
function is available, allowing QNodes to be easily drawn without execution by providing example input. (#962)@qml.qnode(dev) def circuit(a, w): qml.Hadamard(0) qml.CRX(a, wires=[0, 1]) qml.Rot(*w, wires=[1]) qml.CRX(-a, wires=[0, 1]) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
The QNode circuit structure may depend on the input arguments; this is taken into account by passing example QNode arguments to the
qml.draw()
drawing function:>>> drawer = qml.draw(circuit) >>> result = drawer(a=2.3, w=[1.2, 3.2, 0.7]) >>> print(result) 0: ──H──╭C────────────────────────────╭C─────────╭┤ ⟨Z ⊗ Z⟩ 1: ─────╰RX(2.3)──Rot(1.2, 3.2, 0.7)──╰RX(-2.3)──╰┤ ⟨Z ⊗ Z⟩
A faster, leaner, and more flexible core
-
The new core of PennyLane, rewritten from the ground up and developed over the last few release cycles, has achieved feature parity and has been made the new default in PennyLane v0.14. The old core has been marked as deprecated, and will be removed in an upcoming release. (#1046) (#1040) (#1034) (#1035) (#1027) (#1026) (#1021) (#1054) (#1049)
While high-level PennyLane code and tutorials remain unchanged, the new core provides several advantages and improvements:
-
Faster and more optimized: The new core provides various performance optimizations, reducing pre- and post-processing overhead, and reduces the number of quantum evaluations in certain cases.
-
Support for in-QNode classical processing: this allows for differentiable classical processing within the QNode.
dev = qml.device("default.qubit", wires=1) @qml.qnode(dev, interface="tf") def circuit(p): qml.RX(tf.sin(p[0])**2 + p[1], wires=0) return qml.expval(qml.PauliZ(0))
The classical processing functions used within the QNode must match the QNode interface. Here, we use TensorFlow:
>>> params = tf.Variable([0.5, 0.1], dtype=tf.float64) >>> with tf.GradientTape() as tape: ... res = circuit(params) >>> grad = tape.gradient(res, params) >>> print(res) tf.Tensor(0.9460913127754935, shape=(), dtype=float64) >>> print(grad) tf.Tensor([-0.27255248 -0.32390003], shape=(2,), dtype=float64)
As a result of this change, quantum decompositions that require classical processing are fully supported and end-to-end differentiable in tape mode.
-
No more Variable wrapping: QNode arguments no longer become
Variable
objects within the QNode.dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(x): print("Parameter value:", x) qml.RX(x, wires=0) return qml.expval(qml.PauliZ(0))
Internal QNode parameters can be easily inspected, printed, and manipulated:
>>> circuit(0.5) Parameter value: 0.5 tensor(0.87758256, requires_grad=True)
-
Less restrictive QNode signatures: There is no longer any restriction on the QNode signature; the QNode can be defined and called following the same rules as standard Python functions.
For example, the following QNode uses positional, named, and variable keyword arguments:
x = torch.tensor(0.1, requires_grad=True) y = torch.tensor([0.2, 0.3], requires_grad=True) z = torch.tensor(0.4, requires_grad=True) @qml.qnode(dev, interface="torch") def circuit(p1, p2=y, **kwargs): qml.RX(p1, wires=0) qml.RY(p2[0] * p2[1], wires=0) qml.RX(kwargs["p3"], wires=0) return qml.var(qml.PauliZ(0))
When we call the QNode, we may pass the arguments by name even if defined positionally; any argument not provided will use the default value.
>>> res = circuit(p1=x, p3=z) >>> print(res) tensor(0.2327, dtype=torch.float64, grad_fn=<SelectBackward>) >>> res.backward() >>> print(x.grad, y.grad, z.grad) tensor(0.8396) tensor([0.0289, 0.0193]) tensor(0.8387)
This extends to the
qnn
module, whereKerasLayer
andTorchLayer
modules can be created from QNodes with unrestricted signatures. -
Smarter measurements: QNodes can now measure wires more than once, as long as all observables are commuting:
@qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) return [ qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) ]
Further, the
qml.ExpvalCost()
function allows for optimizing measurements to reduce the number of quantum evaluations required.
With the new PennyLane core, there are a few small breaking changes, detailed below in the 'Breaking Changes' section.
-
Improvements
-
The built-in PennyLane optimizers allow more flexible cost functions. The cost function passed to most optimizers may accept any combination of trainable arguments, non-trainable arguments, and keyword arguments. (#959) (#1053)
The full changes apply to:
AdagradOptimizer
AdamOptimizer
GradientDescentOptimizer
MomentumOptimizer
NesterovMomentumOptimizer
RMSPropOptimizer
RotosolveOptimizer
The
requires_grad=False
property must mark any non-trainable constant argument. TheRotoselectOptimizer
allows passing only keyword arguments.Example use:
def cost(x, y, data, scale=1.0): return scale * (x[0]-data)**2 + scale * (y-data)**2 x = np.array([1.], requires_grad=True) y = np.array([1.0]) data = np.array([2.], requires_grad=False) opt = qml.GradientDescentOptimizer() # the optimizer step and step_and_cost methods can # now update multiple parameters at once x_new, y_new, data = opt.step(cost, x, y, data, scale=0.5) (x_new, y_new, data), value = opt.step_and_cost(cost, x, y, data, scale=0.5) # list and tuple unpacking is also supported params = (x, y, data) params = opt.step(cost, *params)
-
The circuit drawer has been updated to support the inclusion of unused or inactive wires, by passing the
show_all_wires
argument. (#1033)dev = qml.device('default.qubit', wires=[-1, "a", "q2", 0]) @qml.qnode(dev) def circuit(): qml.Hadamard(wires=-1) qml.CNOT(wires=[-1, "q2"]) return qml.expval(qml.PauliX(wires="q2"))
>>> print(qml.draw(circuit, show_all_wires=True)()) >>> -1: ──H──╭C──┤ a: ─────│───┤ q2: ─────╰X──┤ ⟨X⟩ 0: ─────────┤
-
The logic for choosing the 'best' differentiation method has been altered to improve performance. (#1008)
-
If the device provides its own gradient, this is now the preferred differentiation method.
-
If a device provides additional interface-specific versions that natively support classical backpropagation, this is now preferred over the parameter-shift rule.
Devices define additional interface-specific devices via their
capabilities()
dictionary. For example,default.qubit
supports supplementary devices for TensorFlow, Autograd, and JAX:{ "passthru_devices": { "tf": "default.qubit.tf", "autograd": "default.qubit.autograd", "jax": "default.qubit.jax", }, }
As a result of this change, if the QNode
diff_method
is not explicitly provided, it is possible that the QNode will run on a supplementary device of the device that was specifically provided:dev = qml.device("default.qubit", wires=2) qml.QNode(dev) # will default to backprop on default.qubit.autograd qml.QNode(dev, interface="tf") # will default to backprop on default.qubit.tf qml.QNode(dev, interface="jax") # will default to backprop on default.qubit.jax
-
-
The
default.qubit
device has been updated so that internally it applies operations in a more functional style, i.e., by accepting an input state and returning an evolved state. (#1025) -
A new test series,
pennylane/devices/tests/test_compare_default_qubit.py
, has been added, allowing to test if a chosen device gives the same result asdefault.qubit
. (#897)Three tests are added:
test_hermitian_expectation
,test_pauliz_expectation_analytic
, andtest_random_circuit
.
-
Adds the following agnostic tensor manipulation functions to the
qml.math
module:abs
,angle
,arcsin
,concatenate
,dot
,squeeze
,sqrt
,sum
,take
,where
. These functions are required to fully support end-to-end differentiable Mottonen and Amplitude embedding. (#922) (#1011) -
The
qml.math
module now supports JAX. (#985) -
Several improvements have been made to the
Wires
class to reduce overhead and simplify the logic of how wire labels are interpreted: (#1019) (#1010) (#1005) (#983) (#967)-
If the input
wires
to a wires class instantiationWires(wires)
can be iterated over, its elements are interpreted as wire labels. Otherwise,wires
is interpreted as a single wire label. The only exception to this are strings, which are always interpreted as a single wire label, so users can address wires with labels such as"ancilla"
. -
Any type can now be a wire label as long as it is hashable. The hash is used to establish the uniqueness of two labels.
-
Indexing wires objects now returns a label, instead of a new
Wires
object. For example:>>> w = Wires([0, 1, 2]) >>> w[1] >>> 1
-
The check for uniqueness of wires moved from
Wires
instantiation to theqml.wires._process
function in order to reduce overhead from repeated creation ofWires
instances. -
Calls to the
Wires
class are substantially reduced, for example by avoiding to call Wires on Wires instances onOperation
instantiation, and by using labels instead ofWires
objects inside the default qubit device.
-
-
Adds the
PauliRot
generator to theqml.operation
module. This generator is required to construct the metric tensor. (#963) -
The templates are modified to make use of the new
qml.math
module, for framework-agnostic tensor manipulation. This allows the template library to be differentiable in backpropagation mode (diff_method="backprop"
). (#873) -
The circuit drawer now allows for the wire order to be (optionally) modified: (#992)
>>> dev = qml.device('default.qubit', wires=["a", -1, "q2"]) >>> @qml.qnode(dev) ... def circuit(): ... qml.Hadamard(wires=-1) ... qml.CNOT(wires=["a", "q2"]) ... qml.RX(0.2, wires="a") ... return qml.expval(qml.PauliX(wires="q2"))
Printing with default wire order of the device:
>>> print(circuit.draw()) a: ─────╭C──RX(0.2)──┤ -1: ──H──│────────────┤ q2: ─────╰X───────────┤ ⟨X⟩
Changing the wire order:
>>> print(circuit.draw(wire_order=["q2", "a", -1])) q2: ──╭X───────────┤ ⟨X⟩ a: ──╰C──RX(0.2)──┤ -1: ───H───────────┤
Breaking changes
-
QNodes using the new PennyLane core will no longer accept ragged arrays as inputs.
-
When using the new PennyLane core and the Autograd interface, non-differentiable data passed as a QNode argument or a gate must have the
requires_grad
property set toFalse
:@qml.qnode(dev) def circuit(weights, data): basis_state = np.array([1, 0, 1, 1], requires_grad=False) qml.BasisState(basis_state, wires=[0, 1, 2, 3]) qml.templates.AmplitudeEmbedding(data, wires=[0, 1, 2, 3]) qml.templates.BasicEntanglerLayers(weights, wires=[0, 1, 2, 3]) return qml.probs(wires=0) data = np.array(data, requires_grad=False) weights = np.array(weights, requires_grad=True) circuit(weights, data)
Bug fixes
-
Fixes an issue where if the constituent observables of a tensor product do not exist in the queue, an error is raised. With this fix, they are first queued before annotation occurs. (#1038)
-
Fixes an issue with tape expansions where information about sampling (specifically the
is_sampled
tape attribute) was not preserved. (#1027) -
Tape expansion was not properly taking into devices that supported inverse operations, causing inverse operations to be unnecessarily decomposed. The QNode tape expansion logic, as well as the
Operation.expand()
method, has been modified to fix this. (#956) -
Fixes an issue where the Autograd interface was not unwrapping non-differentiable PennyLane tensors, which can cause issues on some devices. (#941)
-
qml.vqe.Hamiltonian
prints any observable with any number of strings. (#987) -
Fixes a bug where parameter-shift differentiation would fail if the QNode contained a single probability output. (#1007)
-
Fixes an issue when using trainable parameters that are lists/arrays with
tape.vjp
. (#1042) -
The
TensorN
observable is updated to support being copied without any parameters or wires passed. (#1047) -
Fixed deprecation warning when importing
Sequence
fromcollections
instead ofcollections.abc
invqe/vqe.py
. (#1051)
Contributors
This release contains contributions from (in alphabetical order):
Juan Miguel Arrazola, Thomas Bromley, Olivia Di Matteo, Theodor Isacsson, Josh Izaac, Christina Lee, Alejandro Montanez, Steven Oud, Chase Roberts, Sankalp Sanand, Maria Schuld, Antal Száva, David Wierichs, Jiahao Yao.