Release 0.17.0
New features since the last release
Circuit optimization
-
PennyLane can now perform quantum circuit optimization using the top-level transform
qml.compile
. Thecompile
transform allows you to chain together sequences of tape and quantum function transforms into custom circuit optimization pipelines. (#1475)For example, take the following decorated quantum function:
dev = qml.device('default.qubit', wires=[0, 1, 2]) @qml.qnode(dev) @qml.compile() def qfunc(x, y, z): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Hadamard(wires=2) qml.RZ(z, wires=2) qml.CNOT(wires=[2, 1]) qml.RX(z, wires=0) qml.CNOT(wires=[1, 0]) qml.RX(x, wires=0) qml.CNOT(wires=[1, 0]) qml.RZ(-z, wires=2) qml.RX(y, wires=2) qml.PauliY(wires=2) qml.CZ(wires=[1, 2]) return qml.expval(qml.PauliZ(wires=0))
The default behaviour of
qml.compile
is to apply a sequence of three transforms:commute_controlled
,cancel_inverses
, and thenmerge_rotations
.>>> print(qml.draw(qfunc)(0.2, 0.3, 0.4)) 0: ──H───RX(0.6)──────────────────┤ ⟨Z⟩ 1: ──H──╭X────────────────────╭C──┤ 2: ──H──╰C────────RX(0.3)──Y──╰Z──┤
The
qml.compile
transform is flexible and accepts a custom pipeline of tape and quantum function transforms (you can even write your own!). For example, if we wanted to only push single-qubit gates through controlled gates and cancel adjacent inverses, we could do:from pennylane.transforms import commute_controlled, cancel_inverses pipeline = [commute_controlled, cancel_inverses] @qml.qnode(dev) @qml.compile(pipeline=pipeline) def qfunc(x, y, z): qml.Hadamard(wires=0) qml.Hadamard(wires=1) qml.Hadamard(wires=2) qml.RZ(z, wires=2) qml.CNOT(wires=[2, 1]) qml.RX(z, wires=0) qml.CNOT(wires=[1, 0]) qml.RX(x, wires=0) qml.CNOT(wires=[1, 0]) qml.RZ(-z, wires=2) qml.RX(y, wires=2) qml.PauliY(wires=2) qml.CZ(wires=[1, 2]) return qml.expval(qml.PauliZ(wires=0))
>>> print(qml.draw(qfunc)(0.2, 0.3, 0.4)) 0: ──H───RX(0.4)──RX(0.2)────────────────────────────┤ ⟨Z⟩ 1: ──H──╭X───────────────────────────────────────╭C──┤ 2: ──H──╰C────────RZ(0.4)──RZ(-0.4)──RX(0.3)──Y──╰Z──┤
The following compilation transforms have been added and are also available to use, either independently, or within a
qml.compile
pipeline:-
commute_controlled
: push commuting single-qubit gates through controlled operations. (#1464) -
cancel_inverses
: removes adjacent pairs of operations that cancel out. (#1455) -
merge_rotations
: combines adjacent rotation gates of the same type into a single gate, including controlled rotations. (#1455) -
single_qubit_fusion
: acts on all sequences of single-qubit operations in a quantum function, and converts each sequence to a singleRot
gate. (#1458)
For more details on
qml.compile
and the available compilation transforms, see the compilation documentation. -
QNodes are even more powerful
- Computational basis samples directly from the underlying device can now be returned directly from QNodes via
qml.sample()
. (#1441)
dev = qml.device("default.qubit", wires=3, shots=5)
@qml.qnode(dev)
def circuit_1():
qml.Hadamard(wires=0)
qml.Hadamard(wires=1)
return qml.sample()
@qml.qnode(dev)
def circuit_2():
qml.Hadamard(wires=0)
qml.Hadamard(wires=1)
return qml.sample(wires=[0,2]) # no observable provided and wires specified
>>> print(circuit_1())
[[1, 0, 0],
[1, 1, 0],
[1, 0, 0],
[0, 0, 0],
[0, 1, 0]]
>>> print(circuit_2())
[[1, 0],
[1, 0],
[1, 0],
[0, 0],
[0, 0]]
>>> print(qml.draw(circuit_2)())
0: ──H──╭┤ Sample[basis]
1: ──H──│┤
2: ─────╰┤ Sample[basis]
-
The new
qml.apply
function can be used to add operations that might have already been instantiated elsewhere to the QNode and other queuing contexts: (#1433)op = qml.RX(0.4, wires=0) dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.RY(x, wires=0) qml.apply(op) return qml.expval(qml.PauliZ(0))
>>> print(qml.draw(circuit)(0.6)) 0: ──RY(0.6)──RX(0.4)──┤ ⟨Z⟩
Previously instantiated measurements can also be applied to QNodes.
Device Resource Tracker
-
The new Device Tracker capabilities allows for flexible and versatile tracking of executions, even inside parameter-shift gradients. This functionality will improve the ease of monitoring large batches and remote jobs. (#1355)
dev = qml.device('default.qubit', wires=1, shots=100) @qml.qnode(dev, diff_method="parameter-shift") def circuit(x): qml.RX(x, wires=0) return qml.expval(qml.PauliZ(0)) x = np.array(0.1) with qml.Tracker(circuit.device) as tracker: qml.grad(circuit)(x)
>>> tracker.totals {'executions': 3, 'shots': 300, 'batches': 1, 'batch_len': 2} >>> tracker.history {'executions': [1, 1, 1], 'shots': [100, 100, 100], 'batches': [1], 'batch_len': [2]} >>> tracker.latest {'batches': 1, 'batch_len': 2}
Users can also provide a custom function to the
callback
keyword that gets called each time the information is updated. This functionality allows users to monitor remote jobs or large parameter-shift batches.>>> def shots_info(totals, history, latest): ... print("Total shots: ", totals['shots']) >>> with qml.Tracker(circuit.device, callback=shots_info) as tracker: ... qml.grad(circuit)(0.1) Total shots: 100 Total shots: 200 Total shots: 300 Total shots: 300
Containerization support
-
Docker support for building PennyLane with support for all interfaces (TensorFlow, Torch, and Jax), as well as device plugins and QChem, for GPUs and CPUs, has been added. (#1391)
The build process using Docker and
make
requires that the repository source code is cloned or downloaded from GitHub. Visit the the detailed description for an extended list of options.
Improved Hamiltonian simulations
-
Added a sparse Hamiltonian observable and the functionality to support computing its expectation value with
default.qubit
. (#1398)For example, the following QNode returns the expectation value of a sparse Hamiltonian:
dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, diff_method="parameter-shift") def circuit(param, H): qml.PauliX(0) qml.SingleExcitation(param, wires=[0, 1]) return qml.expval(qml.SparseHamiltonian(H, [0, 1]))
We can execute this QNode, passing in a sparse identity matrix:
>>> print(circuit([0.5], scipy.sparse.eye(4).tocoo())) 0.9999999999999999
The expectation value of the sparse Hamiltonian is computed directly, which leads to executions that are faster by orders of magnitude. Note that "parameter-shift" is the only differentiation method that is currently supported when the observable is a sparse Hamiltonian.
-
VQE problems can now be intuitively set up by passing the Hamiltonian as an observable. (#1474)
dev = qml.device("default.qubit", wires=2) H = qml.Hamiltonian([1., 2., 3.], [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)]) w = qml.init.strong_ent_layers_uniform(1, 2, seed=1967) @qml.qnode(dev) def circuit(w): qml.templates.StronglyEntanglingLayers(w, wires=range(2)) return qml.expval(H)
>>> print(circuit(w)) -1.5133943637878295 >>> print(qml.grad(circuit)(w)) [[[-8.32667268e-17 1.39122955e+00 -9.12462052e-02] [ 1.02348685e-16 -7.77143238e-01 -1.74708049e-01]]]
Note that other measurement types like
var(H)
orsample(H)
, as well as multiple expectations likeexpval(H1), expval(H2)
are not supported. -
Added functionality to compute the sparse matrix representation of a
qml.Hamiltonian
object. (#1394)
New gradients module
-
A new gradients module
qml.gradients
has been added, which provides differentiable quantum gradient transforms. (#1476) (#1479) (#1486)Available quantum gradient transforms include:
qml.gradients.finite_diff
qml.gradients.param_shift
qml.gradients.param_shift_cv
For example,
>>> params = np.array([0.3,0.4,0.5], requires_grad=True) >>> with qml.tape.JacobianTape() as tape: ... qml.RX(params[0], wires=0) ... qml.RY(params[1], wires=0) ... qml.RX(params[2], wires=0) ... qml.expval(qml.PauliZ(0)) ... qml.var(qml.PauliZ(0)) >>> tape.trainable_params = {0, 1, 2} >>> gradient_tapes, fn = qml.gradients.finite_diff(tape) >>> res = dev.batch_execute(gradient_tapes) >>> fn(res) array([[-0.69688381, -0.32648317, -0.68120105], [ 0.8788057 , 0.41171179, 0.85902895]])
Even more new operations and templates
-
Grover Diffusion Operator template added. (#1442)
For example, if we have an oracle that marks the "all ones" state with a negative sign:
n_wires = 3 wires = list(range(n_wires)) def oracle(): qml.Hadamard(wires[-1]) qml.Toffoli(wires=wires) qml.Hadamard(wires[-1])
We can perform Grover's Search Algorithm:
dev = qml.device('default.qubit', wires=wires) @qml.qnode(dev) def GroverSearch(num_iterations=1): for wire in wires: qml.Hadamard(wire) for _ in range(num_iterations): oracle() qml.templates.GroverOperator(wires=wires) return qml.probs(wires)
We can see this circuit yields the marked state with high probability:
>>> GroverSearch(num_iterations=1) tensor([0.03125, 0.03125, 0.03125, 0.03125, 0.03125, 0.03125, 0.03125, 0.78125], requires_grad=True) >>> GroverSearch(num_iterations=2) tensor([0.0078125, 0.0078125, 0.0078125, 0.0078125, 0.0078125, 0.0078125, 0.0078125, 0.9453125], requires_grad=True)
-
A decomposition has been added to
QubitUnitary
that makes the single-qubit case fully differentiable in all interfaces. Furthermore, a quantum function transform,unitary_to_rot()
, has been added to decompose all single-qubit instances ofQubitUnitary
in a quantum circuit. (#1427)Instances of
QubitUnitary
may now be decomposed directly toRot
operations, orRZ
operations if the input matrix is diagonal. For example, let>>> U = np.array([ [-0.28829348-0.78829734j, 0.30364367+0.45085995j], [ 0.53396245-0.10177564j, 0.76279558-0.35024096j] ])
Then, we can compute the decomposition as:
>>> qml.QubitUnitary.decomposition(U, wires=0) [Rot(-0.24209530281458358, 1.1493817777199102, 1.733058145303424, wires=[0])]
We can also apply the transform directly to a quantum function, and compute the gradients of parameters used to construct the unitary matrices.
def qfunc_with_qubit_unitary(angles): z, x = angles[0], angles[1] Z_mat = np.array([[np.exp(-1j * z / 2), 0.0], [0.0, np.exp(1j * z / 2)]]) c = np.cos(x / 2) s = np.sin(x / 2) * 1j X_mat = np.array([[c, -s], [-s, c]]) qml.Hadamard(wires="a") qml.QubitUnitary(Z_mat, wires="a") qml.QubitUnitary(X_mat, wires="b") qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a"))
>>> dev = qml.device("default.qubit", wires=["a", "b"]) >>> transformed_qfunc = qml.transforms.unitary_to_rot(qfunc_with_qubit_unitary) >>> transformed_qnode = qml.QNode(transformed_qfunc, dev) >>> input = np.array([0.3, 0.4], requires_grad=True) >>> transformed_qnode(input) tensor(0.95533649, requires_grad=True) >>> qml.grad(transformed_qnode)(input) array([-0.29552021, 0. ])
-
Ising YY gate functionality added. (#1358)
Improvements
-
The
step
andstep_and_cost
methods ofQNGOptimizer
now accept a customgrad_fn
keyword argument to use for gradient computations. (#1487) -
The precision used by
default.qubit.jax
now matches the float precision indicated byfrom jax.config import config config.read('jax_enable_x64')
where
True
meansfloat64
/complex128
andFalse
meansfloat32
/complex64
. (#1485) -
The
./pennylane/ops/qubit.py
file is broken up into a folder of six separate files. (#1467) -
Changed to using commas as the separator of wires in the string representation of
qml.Hamiltonian
objects for multi-qubit terms. (#1465) -
Changed to using
np.object_
instead ofnp.object
as per the NumPy deprecations starting version 1.20. (#1466) -
Change the order of the covariance matrix and the vector of means internally in
default.gaussian
. (#1331) -
Added the
id
attribute to templates. (#1438) -
The
qml.math
module, for framework-agnostic tensor manipulation, has two new functions available: (#1490)-
qml.math.get_trainable_indices(sequence_of_tensors)
: returns the indices corresponding to trainable tensors in the input sequence. -
qml.math.unwrap(sequence_of_tensors)
: unwraps a sequence of tensor-like objects to NumPy arrays.
In addition, the behaviour of
qml.math.requires_grad
has been improved in order to correctly determine trainability during Autograd and JAX backwards passes. -
-
A new tape method,
tape.unwrap()
is added. This method is a context manager; inside the context, the tape's parameters are unwrapped to NumPy arrays and floats, and the trainable parameter indices are set. (#1491)These changes are temporary, and reverted on exiting the context.
>>> with tf.GradientTape(): ... with qml.tape.QuantumTape() as tape: ... qml.RX(tf.Variable(0.1), wires=0) ... qml.RY(tf.constant(0.2), wires=0) ... qml.RZ(tf.Variable(0.3), wires=0) ... with tape.unwrap(): ... print("Trainable params:", tape.trainable_params) ... print("Unwrapped params:", tape.get_parameters()) Trainable params: {0, 2} Unwrapped params: [0.1, 0.3] >>> print("Original parameters:", tape.get_parameters()) Original parameters: [<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.1>, <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.3>]
In addition,
qml.tape.Unwrap
is a context manager that unwraps multiple tapes:>>> with qml.tape.Unwrap(tape1, tape2):
Breaking changes
-
Removed the deprecated tape methods
get_resources
andget_depth
as they are superseded by thespecs
tape attribute. (#1522) -
Specifying
shots=None
withqml.sample
was previously deprecated. From this release onwards, settingshots=None
when sampling will raise an error. (#1522) -
The existing
pennylane.collections.apply
function is no longer accessible viaqml.apply
, and needs to be imported directly from thecollections
package. (#1358)
Bug fixes
-
Fixes a bug in
qml.adjoint
andqml.ctrl
where the adjoint of operations outside of aQNode
or aQuantumTape
could not be obtained. (#1532) -
Fixes a bug in
GradientDescentOptimizer
andNesterovMomentumOptimizer
where a cost function with one trainable parameter and non-trainable parameters raised an error. (#1495) -
Fixed an example in the documentation's introduction to numpy gradients, where the wires were a non-differentiable argument to the QNode. (#1499)
-
Fixed a bug where the adjoint of
qml.QFT
when using theqml.adjoint
function was not correctly computed. (#1451) -
Fixed the differentiability of the operation
IsingYY
for Autograd, Jax and Tensorflow. (#1425) -
Fixed a bug in the
torch
interface that prevented gradients from being computed on a GPU. (#1426) -
Quantum function transforms now preserve the format of the measurement results, so that a single measurement returns a single value rather than an array with a single element. (#1434)
-
Fixed a bug in the parameter-shift Hessian implementation, which resulted in the incorrect Hessian being returned for a cost function that performed post-processing on a vector-valued QNode. (#1436)
-
Fixed a bug in the initialization of
QubitUnitary
where the size of the matrix was not checked against the number of wires. (#1439)
Documentation
-
Improved Contribution Guide and Pull Requests Guide. (#1461)
-
Examples have been added to clarify use of the continuous-variable
FockStateVector
operation in the multi-mode case. (#1472)
Contributors
This release contains contributions from (in alphabetical order):
Juan Miguel Arrazola, Olivia Di Matteo, Anthony Hayes, Theodor Isacsson, Josh Izaac, Soran Jahangiri, Nathan Killoran, Arshpreet Singh Khangura, Leonhard Kunczik, Christina Lee, Romain Moyard, Lee James O'Riordan, Ashish Panigrahi, Nahum Sá, Maria Schuld, Jay Soni, Antal Száva, David Wierichs.