New features since last release
Better and more flexible shot control
-
Adds a new optimizer
qml.ShotAdaptiveOptimizer
, a gradient-descent optimizer where the shot rate is adaptively calculated using the variances of the parameter-shift gradient. (#1139)By keeping a running average of the parameter-shift gradient and the variance of the parameter-shift gradient, this optimizer frugally distributes a shot budget across the partial derivatives of each parameter.
In addition, if computing the expectation value of a Hamiltonian, weighted random sampling can be used to further distribute the shot budget across the local terms from which the Hamiltonian is constructed.
This optimizer is based on both the iCANS1 and Rosalin shot-adaptive optimizers.
Once constructed, the cost function can be passed directly to the optimizer's
step
method. The attributeopt.total_shots_used
can be used to track the number of shots per iteration.>>> coeffs = [2, 4, -1, 5, 2] >>> obs = [ ... qml.PauliX(1), ... qml.PauliZ(1), ... qml.PauliX(0) @ qml.PauliX(1), ... qml.PauliY(0) @ qml.PauliY(1), ... qml.PauliZ(0) @ qml.PauliZ(1) ... ] >>> H = qml.Hamiltonian(coeffs, obs) >>> dev = qml.device("default.qubit", wires=2, shots=100) >>> cost = qml.ExpvalCost(qml.templates.StronglyEntanglingLayers, H, dev) >>> params = qml.init.strong_ent_layers_uniform(n_layers=2, n_wires=2) >>> opt = qml.ShotAdaptiveOptimizer(min_shots=10) >>> for i in range(5): ... params = opt.step(cost, params) ... print(f"Step {i}: cost = {cost(params):.2f}, shots_used = {opt.total_shots_used}") Step 0: cost = -5.68, shots_used = 240 Step 1: cost = -2.98, shots_used = 336 Step 2: cost = -4.97, shots_used = 624 Step 3: cost = -5.53, shots_used = 1054 Step 4: cost = -6.50, shots_used = 1798
-
Batches of shots can now be specified as a list, allowing measurement statistics to be course-grained with a single QNode evaluation. (#1103)
>>> shots_list = [5, 10, 1000] >>> dev = qml.device("default.qubit", wires=2, shots=shots_list)
When QNodes are executed on this device, a single execution of 1015 shots will be submitted. However, three sets of measurement statistics will be returned; using the first 5 shots, second set of 10 shots, and final 1000 shots, separately.
For example, executing a circuit with two outputs will lead to a result of shape
(3, 2)
:>>> @qml.qnode(dev) ... def circuit(x): ... qml.RX(x, wires=0) ... qml.CNOT(wires=[0, 1]) ... return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)), qml.expval(qml.PauliZ(0)) >>> circuit(0.5) [[0.33333333 1. ] [0.2 1. ] [0.012 0.868 ]]
This output remains fully differentiable.
-
The number of shots can now be specified on a per-call basis when evaluating a QNode. (#1075).
For this, the qnode should be called with an additional
shots
keyword argument:>>> dev = qml.device('default.qubit', wires=1, shots=10) # default is 10 >>> @qml.qnode(dev) ... def circuit(a): ... qml.RX(a, wires=0) ... return qml.sample(qml.PauliZ(wires=0)) >>> circuit(0.8) [ 1 1 1 -1 -1 1 1 1 1 1] >>> circuit(0.8, shots=3) [ 1 1 1] >>> circuit(0.8) [ 1 1 1 -1 -1 1 1 1 1 1]
New differentiable quantum transforms
A new module is available, qml.transforms, which contains differentiable quantum transforms. These are functions that act on QNodes, quantum functions, devices, and tapes, transforming them while remaining fully differentiable.
-
A new adjoint transform has been added. (#1111) (#1135)
This new method allows users to apply the adjoint of an arbitrary sequence of operations.
def subroutine(wire): qml.RX(0.123, wires=wire) qml.RY(0.456, wires=wire) dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def circuit(): subroutine(0) qml.adjoint(subroutine)(0) return qml.expval(qml.PauliZ(0))
This creates the following circuit:
>>> print(qml.draw(circuit)()) 0: --RX(0.123)--RY(0.456)--RY(-0.456)--RX(-0.123)--| <Z>
Directly applying to a gate also works as expected.
qml.adjoint(qml.RX)(0.123, wires=0) # applies RX(-0.123)
-
A new transform
qml.ctrl
is now available that adds control wires to subroutines. (#1157)def my_ansatz(params): qml.RX(params[0], wires=0) qml.RZ(params[1], wires=1) # Create a new operation that applies `my_ansatz` # controlled by the "2" wire. my_ansatz2 = qml.ctrl(my_ansatz, control=2) @qml.qnode(dev) def circuit(params): my_ansatz2(params) return qml.state()
This is equivalent to:
@qml.qnode(...) def circuit(params): qml.CRX(params[0], wires=[2, 0]) qml.CRZ(params[1], wires=[2, 1]) return qml.state()
-
The
qml.transforms.classical_jacobian
transform has been added. (#1186)This transform returns a function to extract the Jacobian matrix of the classical part of a QNode, allowing the classical dependence between the QNode arguments and the quantum gate arguments to be extracted.
For example, given the following QNode:
>>> @qml.qnode(dev) ... def circuit(weights): ... qml.RX(weights[0], wires=0) ... qml.RY(weights[0], wires=1) ... qml.RZ(weights[2] ** 2, wires=1) ... return qml.expval(qml.PauliZ(0))
We can use this transform to extract the relationship :math:
f: \mathbb{R}^n \rightarrow\mathbb{R}^m
between the input QNode arguments :math:w
and the gate arguments :math:g
, for a given value of the QNode arguments:>>> cjac_fn = qml.transforms.classical_jacobian(circuit) >>> weights = np.array([1., 1., 1.], requires_grad=True) >>> cjac = cjac_fn(weights) >>> print(cjac) [[1. 0. 0.] [1. 0. 0.] [0. 0. 2.]]
The returned Jacobian has rows corresponding to gate arguments, and columns corresponding to QNode arguments; that is, :math:
J_{ij} = \frac{\partial}{\partial g_i} f(w_j)
.
More operations and templates
-
Added the
SingleExcitation
two-qubit operation, which is useful for quantum chemistry applications. (#1121)It can be used to perform an SO(2) rotation in the subspace spanned by the states :math:
|01\rangle
and :math:|10\rangle
. For example, the following circuit performs the transformation :math:|10\rangle \rightarrow \cos(\phi/2)|10\rangle - \sin(\phi/2)|01\rangle
:dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(phi): qml.PauliX(wires=0) qml.SingleExcitation(phi, wires=[0, 1])
The
SingleExcitation
operation supports analytic gradients on hardware using only four expectation value calculations, following results from Kottmann et al. -
Added the
DoubleExcitation
four-qubit operation, which is useful for quantum chemistry applications. (#1123)It can be used to perform an SO(2) rotation in the subspace spanned by the states :math:
|1100\rangle
and :math:|0011\rangle
. For example, the following circuit performs the transformation :math:|1100\rangle\rightarrow \cos(\phi/2)|1100\rangle - \sin(\phi/2)|0011\rangle
:dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(phi): qml.PauliX(wires=0) qml.PauliX(wires=1) qml.DoubleExcitation(phi, wires=[0, 1, 2, 3])
The
DoubleExcitation
operation supports analytic gradients on hardware using only four expectation value calculations, following results from Kottmann et al.. -
Added the
QuantumMonteCarlo
template for performing quantum Monte Carlo estimation of an expectation value on simulator. (#1130)The following example shows how the expectation value of sine squared over a standard normal distribution can be approximated:
from scipy.stats import norm m = 5 M = 2 ** m n = 10 N = 2 ** n target_wires = range(m + 1) estimation_wires = range(m + 1, n + m + 1) xmax = np.pi # bound to region [-pi, pi] xs = np.linspace(-xmax, xmax, M) probs = np.array([norm().pdf(x) for x in xs]) probs /= np.sum(probs) func = lambda i: np.sin(xs[i]) ** 2 dev = qml.device("default.qubit", wires=(n + m + 1)) @qml.qnode(dev) def circuit(): qml.templates.QuantumMonteCarlo( probs, func, target_wires=target_wires, estimation_wires=estimation_wires, ) return qml.probs(estimation_wires) phase_estimated = np.argmax(circuit()[:int(N / 2)]) / N expectation_estimated = (1 - np.cos(np.pi * phase_estimated)) / 2
-
Added the
QuantumPhaseEstimation
template for performing quantum phase estimation for an input unitary matrix. (#1095)Consider the matrix corresponding to a rotation from an
RX
gate:>>> phase = 5 >>> target_wires = [0] >>> unitary = qml.RX(phase, wires=0).matrix
The
phase
parameter can be estimated usingQuantumPhaseEstimation
. For example, using five phase-estimation qubits:n_estimation_wires = 5 estimation_wires = range(1, n_estimation_wires + 1) dev = qml.device("default.qubit", wires=n_estimation_wires + 1) @qml.qnode(dev) def circuit(): # Start in the |+> eigenstate of the unitary qml.Hadamard(wires=target_wires) QuantumPhaseEstimation( unitary, target_wires=target_wires, estimation_wires=estimation_wires, ) return qml.probs(estimation_wires) phase_estimated = np.argmax(circuit()) / 2 ** n_estimation_wires # Need to rescale phase due to convention of RX gate phase_estimated = 4 * np.pi * (1 - phase)
-
Added the
ControlledPhaseShift
gate as well as theQFT
operation for applying quantum Fourier transforms. (#1064)@qml.qnode(dev) def circuit_qft(basis_state): qml.BasisState(basis_state, wires=range(3)) qml.QFT(wires=range(3)) return qml.state()
-
Added the
ControlledQubitUnitary
operation. This enables implementation of multi-qubit gates with a variable number of control qubits. It is also possible to specify a different state for the control qubits using thecontrol_values
argument (also known as a mixed-polarity multi-controlled operation). (#1069) (#1104)For example, we can create a multi-controlled T gate using:
T = qml.T._matrix() qml.ControlledQubitUnitary(T, control_wires=[0, 1, 3], wires=2, control_values="110")
Here, the T gate will be applied to wire
2
if control wires0
and1
are in state1
, and control wire3
is in state0
. If no value is passed tocontrol_values
, the gate will be applied if all control wires are in the1
state. -
Added
MultiControlledX
for multi-controlledNOT
gates.
This is a special case ofControlledQubitUnitary
that applies a
Pauli X gate conditioned on the state of an arbitrary number of
control qubits.
(#1104)
Support for higher-order derivatives on hardware
-
Computing second derivatives and Hessians of QNodes is now supported with the parameter-shift differentiation method, on all machine learning interfaces. (#1130) (#1129) (#1110)
Hessians are computed using the parameter-shift rule, and can be evaluated on both hardware and simulator devices.
dev = qml.device('default.qubit', wires=1) @qml.qnode(dev, diff_method="parameter-shift") def circuit(p): qml.RY(p[0], wires=0) qml.RX(p[1], wires=0) return qml.expval(qml.PauliZ(0)) x = np.array([1.0, 2.0], requires_grad=True)
>>> hessian_fn = qml.jacobian(qml.grad(circuit)) >>> hessian_fn(x) [[0.2248451 0.7651474] [0.7651474 0.2248451]]
-
Added the function
finite_diff()
to compute finite-difference approximations to the gradient and the second-order derivatives of arbitrary callable functions. (#1090)This is useful to compute the derivative of parametrized
pennylane.Hamiltonian
observables with respect to their parameters.For example, in quantum chemistry simulations it can be used to evaluate the derivatives of the electronic Hamiltonian with respect to the nuclear coordinates:
>>> def H(x): ... return qml.qchem.molecular_hamiltonian(['H', 'H'], x)[0] >>> x = np.array([0., 0., -0.66140414, 0., 0., 0.66140414]) >>> grad_fn = qml.finite_diff(H, N=1) >>> grad = grad_fn(x) >>> deriv2_fn = qml.finite_diff(H, N=2, idx=[0, 1]) >>> deriv2_fn(x)
-
The JAX interface now supports all devices, including hardware devices, via the parameter-shift differentiation method. (#1076)
For example, using the JAX interface with Cirq:
dev = qml.device('cirq.simulator', wires=1) @qml.qnode(dev, interface="jax", diff_method="parameter-shift") 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]) print(circuit(weights))
Currently, when used with the parameter-shift differentiation method, only a single returned expectation value or variance is supported. Multiple expectations/variances, as well as probability and state returns, are not currently allowed.
Improvements
-
The
MottonenStatePreparation
template has improved performance on states with only real amplitudes by reducing the number of redundant CNOT gates at the end of a circuit.dev = qml.device("default.qubit", wires=2) inputstate = [np.sqrt(0.2), np.sqrt(0.3), np.sqrt(0.4), np.sqrt(0.1)] @qml.qnode(dev) def circuit(): mottonen.MottonenStatePreparation(inputstate,wires=[0, 1]) return qml.expval(qml.PauliZ(0))
Previously returned:
>>> print(qml.draw(circuit)()) 0: ──RY(1.57)──╭C─────────────╭C──╭C──╭C──┤ ⟨Z⟩ 1: ──RY(1.35)──╰X──RY(0.422)──╰X──╰X──╰X──┤
In this release, it now returns:
>>> print(qml.draw(circuit)()) 0: ──RY(1.57)──╭C─────────────╭C──┤ ⟨Z⟩ 1: ──RY(1.35)──╰X──RY(0.422)──╰X──┤
-
The templates are now classes inheriting from
Operation
, and define the ansatz in theirexpand()
method. This change does not affect the user interface. (#1138) (#1156) (#1163) (#1192)For convenience, some templates have a new method that returns the expected shape of the trainable parameter tensor, which can be used to create random tensors.
shape = qml.templates.BasicEntanglerLayers.shape(n_layers=2, n_wires=4) weights = np.random.random(shape) qml.templates.BasicEntanglerLayers(weights, wires=range(4))
-
QubitUnitary
now validates to ensure the input matrix is two dimensional. (#1128)
-
Most layers in Pytorch or Keras accept arbitrary dimension inputs, where each dimension barring the last (in the case where the actual weight function of the layer operates on one-dimensional vectors) is broadcast over. This is now also supported by KerasLayer and TorchLayer. (#1062).
Example use:
dev = qml.device("default.qubit", wires=4) x = tf.ones((5, 4, 4)) @qml.qnode(dev) def layer(weights, inputs): qml.templates.AngleEmbedding(inputs, wires=range(4)) qml.templates.StronglyEntanglingLayers(weights, wires=range(4)) return [qml.expval(qml.PauliZ(i)) for i in range(4)] qlayer = qml.qnn.KerasLayer(layer, {"weights": (4, 4, 3)}, output_dim=4) out = qlayer(x)
The output tensor has the following shape:
>>> out.shape (5, 4, 4)
-
If only one argument to the function
qml.grad
has therequires_grad
attribute set to True, then the returned gradient will be a NumPy array, rather than a tuple of length 1. (#1067) (#1081) -
An improvement has been made to how
QubitDevice
generates and post-processess samples, allowing QNode measurement statistics to work on devices with more than 32 qubits. (#1088) -
Due to the addition of
density_matrix()
as a return type from a QNode, tuples are now supported by theoutput_dim
parameter inqnn.KerasLayer
. (#1070) -
Two new utility methods are provided for working with quantum tapes. (#1175)
-
qml.tape.get_active_tape()
gets the currently recording tape. -
tape.stop_recording()
is a context manager that temporarily stops the currently recording tape from recording additional tapes or quantum operations.
For example:
>>> with qml.tape.QuantumTape(): ... qml.RX(0, wires=0) ... current_tape = qml.tape.get_active_tape() ... with current_tape.stop_recording(): ... qml.RY(1.0, wires=1) ... qml.RZ(2, wires=1) >>> current_tape.operations [RX(0, wires=[0]), RZ(2, wires=[1])]
-
-
When printing
qml.Hamiltonian
objects, the terms are sorted by number of wires followed by coefficients. (#981) -
Adds
qml.math.conj
to the PennyLane math module. (#1143)This new method will do elementwise conjugation to the given tensor-like object, correctly dispatching to the required tensor-manipulation framework to preserve differentiability.
>>> a = np.array([1.0 + 2.0j]) >>> qml.math.conj(a) array([1.0 - 2.0j])
-
The four-term parameter-shift rule, as used by the controlled rotation operations, has been updated to use coefficients that minimize the variance as per https://arxiv.org/abs/2104.05695. (#1206)
-
A new transform
qml.transforms.invisible
has been added, to make it easier to transform QNodes. (#1175)
Breaking changes
-
Devices do not have an
analytic
argument or attribute anymore. Instead,shots
is the source of truth for whether a simulator estimates return values from a finite number of shots, or whether it returns analytic results (shots=None
). (#1079) (#1196)dev_analytic = qml.device('default.qubit', wires=1, shots=None) dev_finite_shots = qml.device('default.qubit', wires=1, shots=1000) def circuit(): qml.Hadamard(wires=0) return qml.expval(qml.PauliZ(wires=0)) circuit_analytic = qml.QNode(circuit, dev_analytic) circuit_finite_shots = qml.QNode(circuit, dev_finite_shots)
Devices with
shots=None
return deterministic, exact results:>>> circuit_analytic() 0.0 >>> circuit_analytic() 0.0
Devices with
shots > 0
return stochastic results estimated from
samples in each run:>>> circuit_finite_shots() -0.062 >>> circuit_finite_shots() 0.034
The
qml.sample()
measurement can only be used on devices on which the number of shots is set explicitly. -
If creating a QNode from a quantum function with an argument named
shots
, aDeprecationWarning
is raised, warning the user that this is a reserved argument to change the number of shots on a per-call basis. (#1075) -
For devices inheriting from
QubitDevice
, the methodsexpval
,var
,sample
accept two new keyword arguments ---shot_range
andbin_size
. (#1103)These new arguments allow for the statistics to be performed on only a subset of device samples. This finer level of control is accessible from the main UI by instantiating a device with a batch of shots.
For example, consider the following device:
>>> dev = qml.device("my_device", shots=[5, (10, 3), 100])
This device will execute QNodes using 135 shots, however measurement statistics will be course grained across these 135 shots:
-
All measurement statistics will first be computed using the first 5 shots --- that is,
shots_range=[0, 5]
,bin_size=5
. -
Next, the tuple
(10, 3)
indicates 10 shots, repeated 3 times. This will useshot_range=[5, 35]
, performing the expectation value in bins of size 10 (bin_size=10
). -
Finally, we repeat the measurement statistics for the final 100 shots,
shot_range=[35, 135]
,bin_size=100
.
-
-
The old PennyLane core has been removed, including the following modules: (#1100)
pennylane.variables
pennylane.qnodes
As part of this change, the location of the new core within the Python
module has been moved:- Moves
pennylane.tape.interfaces
→pennylane.interfaces
- Merges
pennylane.CircuitGraph
andpennylane.TapeCircuitGraph
→pennylane.CircuitGraph
- Merges
pennylane.OperationRecorder
andpennylane.TapeOperationRecorder
→ pennylane.tape.operation_recorder
- Merges
pennylane.measure
andpennylane.tape.measure
→pennylane.measure
- Merges
pennylane.operation
andpennylane.tape.operation
→pennylane.operation
- Merges
pennylane._queuing
andpennylane.tape.queuing
→pennylane.queuing
This has no affect on import location.
In addition,
- All tape-mode functions have been removed (
qml.enable_tape()
,qml.tape_mode_active()
), - All tape fixtures have been deleted,
- Tests specifically for non-tape mode have been deleted.
-
The device test suite no longer accepts the
analytic
keyword. (#1216)
Bug fixes
-
Fixes a bug where using the circuit drawer with a
ControlledQubitUnitary
operation raised an error. (#1174) -
Fixes a bug and a test where the
QuantumTape.is_sampled
attribute was not being updated. (#1126) -
Fixes a bug where
BasisEmbedding
would not accept inputs whose bits are all ones or all zeros. (#1114) -
The
ExpvalCost
class raises an error if instantiated with non-expectation measurement statistics. (#1106) -
Fixes a bug where decompositions would reset the differentiation method of a QNode. (#1117)
-
Fixes a bug where the second-order CV parameter-shift rule would error if attempting to compute the gradient of a QNode with more than one second-order observable. (#1197)
-
Fixes a bug where repeated Torch interface applications after expansion caused an error. (#1223)
-
Sampling works correctly with batches of shots specified as a list. (#1232)
Documentation
-
Updated the diagram used in the Architectural overview page of the Development guide such that it doesn't mention Variables. (#1235)
-
Typos addressed in templates documentation. (#1094)
-
Upgraded the documentation to use Sphinx 3.5.3 and the new m2r2 package. (#1186)
-
Added
flaky
as dependency for running tests in the documentation. (#1113)
Contributors
This release contains contributions from (in alphabetical order):
Shahnawaz Ahmed, Juan Miguel Arrazola, Thomas Bromley, Olivia Di Matteo, Alain Delgado Gran, Kyle Godbey, Diego Guala, Theodor Isacsson, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Daniel Polatajko, Chase Roberts, Sankalp Sanand, Pritish Sehzpaul, Maria Schuld, Antal Száva, David Wierichs.