The latest release of PennyLane is now out and available for everyone to use. It comes with many new additions, including a new graphical circuit drawer, new quantum-aware optimizers, faster performance, smarter circuit decompositions, general hardware gradient support, and more.

Check out the table of contents below, or keep reading to find out more.

- Shiny new circuit drawer! π¨ποΈ
- New and improved quantum-aware optimizers π
- Characterize your quantum models with classical QNode reconstruction π¨βπ¬
- Faster performance with optimized quantum workflows π
- Hardware gradients of arbitrary operations π£
- Define device-specific custom decompositions πΌ
- New operations, templates, and transforms π€
- Improvements
- Breaking changes
- Contributors

## Shiny new circuit drawer! π¨ποΈ

The PennyLane circuit drawer has received a makeover π In addition to our existing text-based
circuit drawer, you can now draw your QNode in full graphical glory with `qml.draw_mpl()`

:

```
qml.drawer.use_style('black_white')
dev = qml.device("default.qubit", wires=4)
@qml.qnode(dev)
def circuit(x, z):
qml.QFT(wires=(0,1,2,3))
qml.Toffoli(wires=(0,1,2))
qml.CSWAP(wires=(0,2,3))
qml.RX(x, wires=0)
qml.CRZ(z, wires=(3,0))
return qml.expval(qml.PauliZ(0))
fig, ax = qml.draw_mpl(circuit)(1.2345, 1.2345)
fig.show()
```

For more details and examples, please refer to the `qml.draw_mpl`

documentation. You can
also view all available circuit styles in the `qml.drawer`

module.

## New and improved quantum-aware optimizers π

When it comes to building variational algorithms, designing your quantum circuit and embedding is only half the battle — care needs to also be taken to choose an optimization method that improves the speed of convergence while minimizing the number of quantum evaluations required!

Quantum-aware optimizers generally provide the best of both worlds here; taking into account the geometry of the quantum landscape to both improve convergence while reducing quantum resources.

With this release, PennyLane introduces a brand new quantum-aware optimizer,
`qml.LieAlgebraOptimizer`

, alongside other optimizer improvements.

### Perform gradient descent on the special unitary group

Riemannian gradient descent algorithms can be used to optimize a function directly on a Lie group as opposed to on a Euclidean parameter space.

Extending this to QNodes, `qml.LieAlgebraOptimizer`

is a new quantum-aware Lie Algebra optimizer
that allows one to perform gradient descent directly on the special unitary group:

```
dev = qml.device("default.qubit", wires=2)
H = -1.0 * qml.PauliX(0) - qml.PauliZ(1) - qml.PauliY(0) @ qml.PauliX(1)
@qml.qnode(dev)
def circuit():
qml.RX(0.1, wires=[0])
qml.RY(0.5, wires=[1])
qml.CNOT(wires=[0,1])
qml.RY(0.6, wires=[0])
return qml.expval(H)
opt = qml.LieAlgebraOptimizer(circuit=circuit, stepsize=0.1)
```

Note that, unlike other optimizers, the `LieAlgebraOptimizer`

accepts a QNode
with *no* parameters, and instead grows the circuit by appending operations
during the optimization:

```
>>> circuit()
tensor(-1.3351865, requires_grad=True)
>>> circuit1, cost = opt.step_and_cost()
>>> circuit1()
tensor(-1.99378872, requires_grad=True)
>>> qml.draw(circuit1, expansion_strategy='device')()
0: ββRX(0.1)βββCββRY(0.6)βββββββRZ(0.0634)βββRZ(5.55e-18)βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββRZ(0.142)ββββββββββββββββRZ(-0.0787)ββHββββββββββRZ(-0.0787)ββHββRX(1.57)ββRZ(0.164)ββRX(-1.57)ββRX(1.57)βββRZ(0.145)ββRX(-1.57)ββHβββRZ(-0.179)ββHββHββββββββββRZ(-0.0539)ββHββββββββββRX(1.57)βββRZ(0.0787)ββRX(-1.57)ββRX(1.57)βββRZ(0.205)ββRX(-1.57)ββββ€ β¨Hamiltonian(-1, -1, -1)β©
1: ββRY(0.5)βββ°XββRZ(2.78e-17)βββββββββββββββ°RZ(5.55e-18)ββHββRZ(-3.05e-17)ββHββRX(1.57)ββRZ(-0.0959)ββRX(-1.57)ββHβββ°RZ(0.142)ββHββRX(1.57)βββ°RZ(-0.0787)ββRX(-1.57)βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ°RZ(0.145)ββHββββββββββββββ°RZ(-0.179)ββHββRX(1.57)βββ°RZ(-0.0539)ββRX(-1.57)ββHββββββββββ°RZ(0.0787)ββHββββββββββRX(1.57)βββ°RZ(0.205)ββRX(-1.57)βββ°β€ β¨Hamiltonian(-1, -1, -1)β©
```

For more details, see the `qml.LieAlgebraOptimizer`

documentation.

### Improved quantum natural gradient support

The `qml.metric_tensor`

transform, used to perform quantum natural gradient optimization via
`qml.QNGOptimizer`

, can now be used to compute the full tensor on hardware, beyond the block
diagonal approximation.

This is performed using a combination of Hadamard tests and covariance matrix computations, minimizing the number of quantum executions required, while requiring an additional wire on the device.

```
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def circuit(weights):
qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.CNOT(wires=[0, 1])
qml.RZ(weights[2], wires=1)
return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
weights = np.array([0.2, 1.2, -0.9], requires_grad=True)
```

```
>>> qml.metric_tensor(circuit)(weights)
[[ 0.25 0. -0.23300977]
[ 0. 0.24013262 0.01763859]
[-0.23300977 0.01763859 0.21846983]]
>>> print(qml.draw(qml.metric_tensor(circuit))(weights))
0: ββHββββ€ Probs
1: ββββββ°β€ Probs
0: ββRX(0.2)ββZββSββHββββ€ Probs
1: βββββββββββββββββββββ°β€ Probs
0: ββRX(0.2)ββRY(1.2)βββCββββ€ Probs
1: βββββββββββββββββββββ°Xβββ°β€ Probs
0: ββββββXββRX(0.2)βββYβββ€
2: ββHβββ°Cββββββββββββ°Cβββ€ β¨Xβ©
0: ββββββXββRX(0.2)ββRY(1.2)βββCβββββββ€
1: ββββββββββββββββββββββββββββ°XβββZβββ€
2: ββHβββ°Cβββββββββββββββββββββββββ°Cβββ€ β¨Xβ©
0: ββRX(0.2)βββYββRY(1.2)βββCβββββββ€
1: βββββββββββββββββββββββββ°XβββZβββ€
2: ββHβββββββββ°Cββββββββββββββββ°Cβββ€ β¨Xβ©
```

Here, three 2-wire circuits have been used to compute the block-diagonal metric tensor elements, with two 3-wire circuits required to compute the off-block-diagonal elements.

As always, the full metric tensor remains fully differentiable:

```
>>> def cost(weights):
... mt = qml.metric_tensor(circuit)(weights)
... return np.linalg.norm(mt)
>>> cost(weights)
0.5264048745278436
>>> qml.grad(cost)(weights)
array([-0.03351384, 0.1444742 , 0. ])
```

## Characterize your quantum models with classical QNode reconstruction π¨βπ¬

Being able to characterize your variational circuits provides valuable insight when
building quantum models, and the `qml.fourier`

module provides a smorgasbord
of introspection.

New to this release is `qml.fourier.reconstruct`

, which returns a classical
function that exactly reconstructs a QNode along a specified
parameter dimension, by sampling the original QNode an optimum number of times.

```
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x, y):
qml.RX(x, wires=0)
qml.RY(y[0], wires=0)
qml.RY(y[1], wires=1)
qml.CNOT(wires=[0, 1])
qml.RY(y[1], wires=1)
return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
```

We can use `qml.fourier.qnode_spectrum`

to compute the frequency spectrum of this
QNode with respect to argument `x`

, and use this to compute the classical reconstruction:

```
>>> x = 0.3
>>> y = np.array([0.1, -0.9])
>>> spectra = qml.fourier.qnode_spectrum(circuit, encoding_args={"x"})(x, y)
>>> univariate_x = qml.fourier.reconstruct(circuit, spectra=spectra)(x, y)["x"][()]
```

The returned reconstruction is exact and purely classical, and can be evaluated and differentiated without any quantum executions.

```
>>> circuit(x + 0.4, y) # will evaluate the quantum device
-0.08056443
>>> univariate_x(x + 0.4) # purely classical, no quantum executions
-0.0805644294814271
```

For more details on usage, reconstruction cost and differentiability support, please see the
`qml.fourier.reconstruct`

docstring.

## Faster performance with optimized quantum workflows π

The QNode has been re-written from the ground up, to support batch execution across the board, custom gradients, better decomposition strategies, and higher-order derivatives.

Note that the old QNode remains accessible at `@qml.qnode_old.qnode`

, however this will
be removed in the next release.

### Batch execute quantum circuits

Internally, if multiple circuits are generated for simultaneous execution, they will be packaged into a single job for execution on the device. This can lead to significant performance improvement when executing the QNode on remote quantum hardware or simulator devices with parallelization capabilities.

### \(n\)th order derivatives on hardware

Arbitrary \(n\)-th order derivatives are supported on hardware using gradient transforms such
as the parameter-shift rule. To specify that an \(n\)-th order derivative of a QNode will be
computed, the `max_diff`

argument should be set. By default, this is set to 1 (first-order
derivatives only).

Increasing this value allows for higher order derivatives to be extracted, at the cost of additional (classical) computational overhead during the backwards pass.

### Smarter circuit decomposition strategies

When decomposing the circuit, the default decomposition strategy `expansion_strategy="gradient"`

will prioritize decompositions that result in the smallest number of parametrized operations
required to satisfy the differentiation method.

While this may lead to a slight increase in classical processing, it significantly reduces the number of circuit evaluations needed to compute gradients of complicated unitaries.

To return to the old behaviour, `expansion_strategy="device"`

can be specified.

### Support for TensorFlow AutoGraph mode with quantum hardware

It is now possible to use TensorFlow’s AutoGraph
mode with QNodes on all devices and with arbitrary
differentiation methods. Previously, AutoGraph mode only supported `diff_method="backprop"`

. This
will result in significantly more performant model execution, at the cost of a more expensive
initial compilation.

Use AutoGraph to convert your QNodes or cost functions into TensorFlow
graphs by decorating them with `@tf.function`

:

```
dev = qml.device("lightning.qubit", wires=2)
@qml.qnode(dev, diff_method="adjoint", interface="tf", max_diff=1)
def circuit(x):
qml.RX(x[0], wires=0)
qml.RY(x[1], wires=1)
return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliZ(0))
@tf.function
def cost(x):
return tf.reduce_sum(circuit(x))
x = tf.Variable([0.5, 0.7], dtype=tf.float64)
with tf.GradientTape() as tape:
loss = cost(x)
grad = tape.gradient(loss, x)
```

The initial execution may take slightly longer than when executing the circuit in eager mode; this is because TensorFlow is tracing the function to create the graph. Subsequent executions will be much more performant.

Note that using AutoGraph with backprop-enabled devices, such as `default.qubit`

,
will yield the best performance.

For more details, please see the TensorFlow AutoGraph documentation.

## Hardware gradients of arbitrary operations π£

The parameter-shift rule is used in PennyLane to support analytic derivatives of QNodes on hardware. However, up to now, it has only supported a limited gate set, including single-qubit rotations and controlled rotations, necessitating lengthy decompositions when working with high-level templates and ansΓ€tze.

With this release, we introduce `qml.gradients.generate_shift_rule`

for computing parameter-shift
rules for arbitrary operations; this avoids the need for costly decompositions, and can
significantly reduce the number of quantum executions required.

Given an operator of the form \(U=e^{iHt}\), \(H = \sum_i a_i h_i\), where the eigenvalues of \(H\) are
known and all \(h_i\) commute, we can compute the *frequencies* (the unique positive differences of
any two eigenvalues) using `qml.gradients.eigvals_to_frequencies`

.

For example, consider the case where \(H\) has eigenspectrum `(-1, 0, 1)`

:

```
>>> frequencies = qml.gradients.eigvals_to_frequencies((-1, 0, 1))
>>> frequencies
(1, 2)
```

`qml.gradients.generate_shift_rule`

can then be used to compute parameter
shift rules to compute \(\partial^n f(t)\) using shifted cost function evaluations:

```
>>> coeffs, shifts = qml.gradients.generate_shift_rule(frequencies, order=1)
>>> coeffs
array([ 0.85355339, -0.85355339, -0.14644661, 0.14644661])
>>> shifts
array([ 0.78539816, -0.78539816, 2.35619449, -2.35619449])
```

This becomes cheaper than the standard application of the chain rule and two-term shift rule when the number of frequencies is less than the number of Pauli words in the generator.

For more details, including for generating \(n\)th order partial derivatives, see the documentation
for
`generate_shift_rule`

and
`generate_multi_shift_rule`

.

## Define device-specific custom decompositions πΌ

By passing the `custom_decomps`

keyword argument when loading a device, custom
operation decompositions can be registered with the device:

```
def custom_cnot(wires):
qml.Hadamard(wires=wires[1])
qml.CZ(wires=[wires[0], wires[1]])
qml.Hadamard(wires=wires[1])
def custom_hadamard(wires):
qml.RZ(np.pi, wires=wires)
qml.RY(np.pi / 2, wires=wires)
custom_decomps = {qml.CNOT : custom_cnot, "Hadamard" : custom_hadamard}
dev = qml.device("default.qubit", wires=3, custom_decomps=custom_decomps)
```

QNodes executed with this device will attempt to satisfy both the custom decomposition and the native device gate set, if possible:

```
>>> @qml.qnode(dev)
>>> def circuit(weights):
... qml.BasicEntanglerLayers(weights, wires=[0, 1, 2])
... return qml.expval(qml.PauliZ(0))
>>> weights = np.array([[0.4, 0.5, 0.6]])
>>> print(qml.draw(circuit, expansion_strategy="device")(weights))
0: ββRX(0.4)βββββββββββββββββββββββCββRZ(3.14)ββRY(1.57)βββββββββββββββββββββββββββZββRZ(3.14)ββRY(1.57)βββ€ β¨Zβ©
1: ββRX(0.5)ββRZ(3.14)ββRY(1.57)βββ°ZββRZ(3.14)ββRY(1.57)βββCβββββββββββββββββββββββββββββββββββββββββββββββ€
2: ββRX(0.6)ββRZ(3.14)ββRY(1.57)βββββββββββββββββββββββββββ°ZββRZ(3.14)ββRY(1.57)βββ°Cβββββββββββββββββββββββ€
```

A separate context manager, `qml.transforms.set_decomposition`

, is also available to enable
application of custom decompositions on devices that have already been created.

## New operations, templates, and transforms π€

Alongside the great new features above, we also have a ton of new operations, templates, and transforms to share. These include:

`qml.CommutingEvolution`

: A circuit template for time evolution under a commuting Hamiltonian. This template utilizes generalized parameter-shift rules to greatly minimize the number of shifted circuits that must be evaluated for gradient computation.

`qml.Barrier`

: Useful for separating blocks of operations during compilation, or for visualizing quantum circuits.

`qml.QubitDensityMatrix`

: Allows for initialization of mixed states when using mixed state simulators.

`qml.PauliError`

: Pauli operator error channel for an arbitrary number of qubits.

`qml.ThermalRelaxationError`

: A thermal relaxation error channel.

`qml.transforms.merge_amplitude_embedding`

: Automatically merge all amplitude embeddings in a QNode into a single embedding.

`qml.transforms.undo_swaps`

: Automatically remove all SWAP operations within a QNode by permuting the wires.

## Improvements

In addition to the new features listed above, the release contains a wide array of improvements and optimizations:

- Methods
`atomic_orbital`

and`molecular_orbital`

have been added to the`qml.hf.Molecule`

class for computing the values of atomic and molecular orbitals at a given position.

- The PennyLane
`qchem`

package is now lazily imported; it will only be imported the first time it is accessed.

- More templates now support the
`@qml.batch_params`

decorator, allowing them to be evaluated on hardware with parameters that include a batch dimension. These include`qml.AngleEmbedding`

,`qml.BasicEntanglerLayers`

, and`qml.MottonenStatePreparation`

.

- The text circuit drawer
`qml.draw()`

now supports a`max_length`

argument to help prevent text overflows when printing a circuit in a terminal.

`qml.Identity`

can now be used as a circuit operation, and is no longer restricted to measurements.

For the full list of improvements, please refer to the full release notes.

## Breaking changes

As new things are added, outdated features are removed. Here’s what will be disappearing in this release.

- The
`qml.template`

decorator has been removed. If the list of operations needs to be extracted from a quantum function, please use the`qml.tape.QuantumTape`

context manager instead.

- The deprecated
`default.tensor`

and`default.tensor.tf`

experimental devices have been removed.

- The
`qml.fourier.spectrum`

function has been removed (split into`qml.fourier.qnode_spectrum`

and`qml.fourier.circuit_spectrum`

).

- The
`diag_approx`

keyword argument of`qml.metric_tensor`

and`qml.QNGOptimizer`

has been removed. The`approx="block-diag"|"diag"|None`

keyword argument should be used instead.

- The
`qml.init`

module, which contains functions to generate random parameters for templates, has been removed. Instead, the templates provide a`shape()`

method.

- The
`par_domain`

attribute in the operator class was no longer used internally, and has been removed.

- The
`mutable`

keyword argument has been removed from the QNode, due to underlying buggy behaviour resulting in incorrect numerical results being returned during evaluation.

- The reversible QNode differentiation method has been temporarily removed.

- The
`DiagonalOperation`

subclass of`Operator`

has been removed. Instead, devices can check for the diagonal property using attributes;`op in qml.ops.qubit.attributes.diagonal_in_z_basis`

. Custom operations can be added to this attribute at runtime via`diagonal_in_z_basis.add("MyCustomOp")`

.

Other breaking changes include:

- When drawing QNodes, the default behaviour is to expand all operations to satisfy the gradient
method. The old behaviour — drawing QNodes using the native device gate set — can be
returned using
`qml.draw(circuit, expansion_strategy="device")`

.

- By default, QNodes only support first derivatives. Second (and higher) derivative support
can be activated by passing
`max_diff=2`

to the QNode decorator.

- The default behaviour of the
`qml.metric_tensor`

transform has been modified: By default, the full metric tensor is computed, leading to higher cost than the previous default of computing the block diagonal only. At the same time, the Hadamard tests for the full metric tensor require an additional wire on the device, so that`qml.metric_tensor(some_qnode)(weights)`

will revert back to the block diagonal restriction and raise a warning if the used device does not have an additional wire.

- The
`num_params`

attribute in the operator class is now dynamic. This makes it easier to define operator subclasses with a flexible number of parameters.

`QuantumTape.trainable_params`

now is a list instead of a set.

These highlights are just scratching the surface — check out the full release notes for more details.

## Contributors

As always, this release would not have been possible without the hard work of our development team and contributors:

Catalina Albornoz, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Samuel Banning, Benjamin Cordier, Alain Delgado, Olivia Di Matteo, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Jalani Kanem, Ankit Khandelwal, Nathan Killoran, Shumpei Kobayashi, Robert Lang, Christina Lee, Cedric Lin, Alejandro Montanez, Romain Moyard, Lee James O’Riordan, Chae-Yeun Park, Isidor Schoch, Maria Schuld, Jay Soni, Antal SzΓ‘va, Rodrigo Vargas, David Wierichs, Roeland Wiersema, Moritz Willmann.