Release notes¶
This page contains the release notes for PennyLane.
- orphan
Release 0.24.0 (current release)¶
New features since last release
All new quantum information quantities 📏
Functionality for computing quantum information quantities for QNodes has been added. (#2554) (#2569) (#2598) (#2617) (#2631) (#2640) (#2663) (#2684) (#2688) (#2695) (#2710) (#2712)
This includes two new QNode measurements:
The Von Neumann entropy via
qml.vn_entropy
:>>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev) ... def circuit_entropy(x): ... qml.IsingXX(x, wires=[0,1]) ... return qml.vn_entropy(wires=[0], log_base=2) >>> circuit_entropy(np.pi/2) 1.0
The mutual information via
qml.mutual_info
:>>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev) ... def circuit(x): ... qml.IsingXX(x, wires=[0,1]) ... return qml.mutual_info(wires0=[0], wires1=[1], log_base=2) >>> circuit(np.pi/2) 2.0
New differentiable transforms are also available in the
qml.qinfo
module:The classical and quantum Fisher information via
qml.qinfo.classical_fisher
,qml.qinfo.quantum_fisher
, respectively:dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circ(params): qml.RY(params[0], wires=1) qml.CNOT(wires=(1,0)) qml.RY(params[1], wires=1) qml.RZ(params[2], wires=1) return qml.expval(qml.PauliX(0) @ qml.PauliX(1) - 0.5 * qml.PauliZ(1)) params = np.array([0.5, 1., 0.2], requires_grad=True) cfim = qml.qinfo.classical_fisher(circ)(params) qfim = qml.qinfo.quantum_fisher(circ)(params)
These quantities are typically employed in variational optimization schemes to tilt the gradient in a more favourable direction — producing what is known as the natural gradient. For example:
>>> grad = qml.grad(circ)(params) >>> cfim @ grad # natural gradient [ 5.94225615e-01 -2.61509542e-02 -1.18674655e-18] >>> qfim @ grad # quantum natural gradient [ 0.59422561 -0.02615095 -0.03989212]
The fidelity between two arbitrary states via
qml.qinfo.fidelity
:dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def circuit_rx(x): qml.RX(x[0], wires=0) qml.RZ(x[1], wires=0) return qml.state() @qml.qnode(dev) def circuit_ry(y): qml.RY(y, wires=0) return qml.state()
>>> x = np.array([0.1, 0.3], requires_grad=True) >>> y = np.array(0.2, requires_grad=True) >>> fid_func = qml.qinfo.fidelity(circuit_rx, circuit_ry, wires0=[0], wires1=[0]) >>> fid_func(x, y) 0.9905158135644924 >>> df = qml.grad(fid_func) >>> df(x, y) (array([-0.04768725, -0.29183666]), array(-0.09489803))
Reduced density matrices of arbitrary states via
qml.qinfo.reduced_dm
:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.IsingXX(x, wires=[0,1]) return qml.state()
>>> qml.qinfo.reduced_dm(circuit, wires=[0])(np.pi/2) [[0.5+0.j 0.+0.j] [0.+0.j 0.5+0.j]]
Similar transforms,
qml.qinfo.vn_entropy
andqml.qinfo.mutual_info
exist for transforming QNodes.
Currently, all quantum information measurements and transforms are differentiable, but only support statevector devices, with hardware support to come in a future release (with the exception of
qml.qinfo.classical_fisher
andqml.qinfo.quantum_fisher
, which are both hardware compatible).For more information, check out the new qinfo module and measurements page.
In addition to the QNode transforms and measurements above, functions for computing and differentiating quantum information metrics with numerical statevectors and density matrices have been added to the
qml.math
module. This enables flexible custom post-processing.Added functions include:
qml.math.reduced_dm
qml.math.vn_entropy
qml.math.mutual_info
qml.math.fidelity
For example:
>>> x = torch.tensor([1.0, 0.0, 0.0, 1.0], requires_grad=True) >>> en = qml.math.vn_entropy(x / np.sqrt(2.), indices=[0]) >>> en tensor(0.6931, dtype=torch.float64, grad_fn=<DivBackward0>) >>> en.backward() >>> x.grad tensor([-0.3069, 0.0000, 0.0000, -0.3069])
Faster mixed-state training with backpropagation 📉
The
default.mixed
device now supports differentiation via backpropagation with the Autograd, TensorFlow, and PyTorch (CPU) interfaces, leading to significantly more performant optimization and training. (#2615) (#2670) (#2680)As a result, the default differentiation method for the device is now
"backprop"
. To continue using the old default"parameter-shift"
, explicitly specify this differentiation method in the QNode:dev = qml.device("default.mixed", wires=2) @qml.qnode(dev, interface="autograd", diff_method="backprop") def circuit(x): qml.RY(x, wires=0) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(wires=1))
>>> x = np.array(0.5, requires_grad=True) >>> circuit(x) array(0.87758256) >>> qml.grad(circuit)(x) -0.479425538604203
Support for quantum parameter broadcasting 📡
Quantum operators, functions, and tapes now support broadcasting across parameter dimensions, making it more convenient for developers to execute their PennyLane programs with multiple sets of parameters. (#2575) (#2609)
Parameter broadcasting refers to passing tensor parameters with additional leading dimensions to quantum operators; additional dimensions will flow through the computation, and produce additional dimensions at the output.
For example, instantiating a rotation gate with a one-dimensional array leads to a broadcasted
Operation
:>>> x = np.array([0.1, 0.2, 0.3], requires_grad=True) >>> op = qml.RX(x, 0) >>> op.batch_size 3
Its matrix correspondingly is augmented by a leading dimension of size
batch_size
:>>> np.round(qml.matrix(op), 4) tensor([[[0.9988+0.j , 0. -0.05j ], [0. -0.05j , 0.9988+0.j ]], [[0.995 +0.j , 0. -0.0998j], [0. -0.0998j, 0.995 +0.j ]], [[0.9888+0.j , 0. -0.1494j], [0. -0.1494j, 0.9888+0.j ]]], requires_grad=True) >>> qml.matrix(op).shape (3, 2, 2)
This can be extended to quantum functions, where we may mix-and-match operations with batched parameters and those without. However, the
batch_size
of each batchedOperator
within the quantum function must be the same:>>> dev = qml.device('default.qubit', wires=1) >>> @qml.qnode(dev) ... def circuit_rx(x, z): ... qml.RX(x, wires=0) ... qml.RZ(z, wires=0) ... qml.RY(0.3, wires=0) ... return qml.probs(wires=0) >>> circuit_rx([0.1, 0.2], [0.3, 0.4]) tensor([[0.97092256, 0.02907744], [0.95671515, 0.04328485]], requires_grad=True)
Parameter broadcasting is supported on all devices, hardware and simulator. Note that if not natively supported by the underlying device, parameter broadcasting may result in additional quantum device evaluations.
A new transform,
qml.transforms.broadcast_expand
, has been added, which automates the process of transforming quantum functions (and tapes) to multiple quantum evaluations with no parameter broadcasting. (#2590)>>> dev = qml.device('default.qubit', wires=1) >>> @qml.transforms.broadcast_expand() >>> @qml.qnode(dev) ... def circuit_rx(x, z): ... qml.RX(x, wires=0) ... qml.RZ(z, wires=0) ... qml.RY(0.3, wires=0) ... return qml.probs(wires=0) >>> print(qml.draw(circuit_rx)([0.1, 0.2], [0.3, 0.4])) 0: ──RX(0.10)──RZ(0.30)──RY(0.30)─┤ Probs \ 0: ──RX(0.20)──RZ(0.40)──RY(0.30)─┤ Probs
Under-the-hood, this transform is used for devices that don’t natively support parameter broadcasting.
To specify that a device natively supports broadcasted tapes, the new flag
Device.capabilities()["supports_broadcasting"]
should be set toTrue
.To support parameter broadcasting for new or custom operations, the following new
Operator
class attributes must be specified:Operator.ndim_params
specifies expected number of dimensions for each parameter
Once set,
Operator.batch_size
andQuantumTape.batch_size
will dynamically compute the parameter broadcasting axis dimension, if present.
Improved JAX JIT support 🏎
JAX just-in-time (JIT) compilation now supports vector-valued QNodes, enabling new types of workflows and significant performance boosts. (#2034)
Vector-valued QNodes include those with:
qml.probs
;qml.state
;qml.sample
ormultiple
qml.expval
/qml.var
measurements.
Consider a QNode that returns basis-state probabilities:
dev = qml.device('default.qubit', wires=2) x = jnp.array(0.543) y = jnp.array(-0.654) @jax.jit @qml.qnode(dev, diff_method="parameter-shift", interface="jax") def circuit(x, y): qml.RX(x, wires=[0]) qml.RY(y, wires=[1]) qml.CNOT(wires=[0, 1]) return qml.probs(wires=[1])
>>> circuit(x, y) DeviceArray([0.8397495 , 0.16025047], dtype=float32)
Note that computing the jacobian of vector-valued QNode is not supported with JAX JIT. The output of vector-valued QNodes can, however, be used in the definition of scalar-valued cost functions whose gradients can be computed.
For example, one can define a cost function that outputs the first element of the probability vector:
def cost(x, y): return circuit(x, y)[0]
>>> jax.grad(cost, argnums=[0])(x, y) (DeviceArray(-0.2050439, dtype=float32),)
More drawing styles 🎨
New
solarized_light
andsolarized_dark
styles are available for drawing circuit diagram graphics. (#2662)
New operations & transforms 🤖
The
qml.IsingXY
gate is now available (see 1912.04424). (#2649)The
qml.ECR
(echoed cross-resonance) operation is now available (see 2105.01063). This gate is a maximally-entangling gate and is equivalent to a CNOT gate up to single-qubit pre-rotations. (#2613)The adjoint transform
adjoint
can now accept either a single instantiated operator or a quantum function. It returns an entity of the same type / call signature as what it was given: (#2222) (#2672)>>> qml.adjoint(qml.PauliX(0)) Adjoint(PauliX)(wires=[0]) >>> qml.adjoint(qml.RX)(1.23, wires=0) Adjoint(RX)(1.23, wires=[0])
Now,
adjoint
wraps operators in a symbolic operator classqml.ops.op_math.Adjoint
. This class should not be constructed directly; theadjoint
constructor should always be used instead. The class behaves just like any otherOperator
:>>> op = qml.adjoint(qml.S(0)) >>> qml.matrix(op) array([[1.-0.j, 0.-0.j], [0.-0.j, 0.-1.j]]) >>> qml.eigvals(op) array([1.-0.j, 0.-1.j])
A new symbolic operator class
qml.ops.op_math.Pow
represents an operator raised to a power. Whendecomposition()
is called, a list of new operators equal to this one raised to the given power is given: (#2621)>>> op = qml.ops.op_math.Pow(qml.PauliX(0), 0.5) >>> op.decomposition() [SX(wires=[0])] >>> qml.matrix(op) array([[0.5+0.5j, 0.5-0.5j], [0.5-0.5j, 0.5+0.5j]])
A new transform
qml.batch_partial
is available which behaves similarly tofunctools.partial
, but supports batching in the unevaluated parameters. (#2585)This is useful for executing a circuit with a batch dimension in some of its parameters:
dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(x, y): qml.RX(x, wires=0) qml.RY(y, wires=0) return qml.expval(qml.PauliZ(wires=0))
>>> batched_partial_circuit = qml.batch_partial(circuit, x=np.array(np.pi / 4)) >>> y = np.array([0.2, 0.3, 0.4]) >>> batched_partial_circuit(y=y) tensor([0.69301172, 0.67552491, 0.65128847], requires_grad=True)
A new transform
qml.split_non_commuting
is available, which splits a quantum function or tape into multiple functions/tapes determined by groups of commuting observables: (#2587)dev = qml.device("default.qubit", wires=1) @qml.transforms.split_non_commuting @qml.qnode(dev) def circuit(x): qml.RX(x,wires=0) return [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(0))]
>>> print(qml.draw(circuit)(0.5)) 0: ──RX(0.50)─┤ <X> \ 0: ──RX(0.50)─┤ <Z>
Improvements
Expectation values of multiple non-commuting observables from within a single QNode are now supported: (#2587)
>>> dev = qml.device('default.qubit', wires=1) >>> @qml.qnode(dev) ... def circuit_rx(x, z): ... qml.RX(x, wires=0) ... qml.RZ(z, wires=0) ... return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)) >>> circuit_rx(0.1, 0.3) tensor([ 0.02950279, -0.09537451], requires_grad=True)
Selecting which parts of parameter-shift Hessians are computed is now possible. (#2538)
The
argnum
keyword argument forqml.gradients.param_shift_hessian
is now allowed to be a two-dimensional Booleanarray_like
. Only the indicated entries of the Hessian will then be computed.A particularly useful example is the computation of the diagonal of the Hessian:
dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=0) qml.RX(x[2], wires=0) return qml.expval(qml.PauliZ(0)) argnum = qml.math.eye(3, dtype=bool) x = np.array([0.2, -0.9, 1.1], requires_grad=True)
>>> qml.gradients.param_shift_hessian(circuit, argnum=argnum)(x) tensor([[-0.09928388, 0. , 0. ], [ 0. , -0.27633945, 0. ], [ 0. , 0. , -0.09928388]], requires_grad=True)
Commuting Pauli operators are now measured faster. (#2425)
The logic that checks for qubit-wise commuting (QWC) observables has been improved, resulting in a performance boost that is noticable when many commuting Pauli operators of the same type are measured.
It is now possible to add
Observable
objects to the integer0
, for exampleqml.PauliX(wires=[0]) + 0
. (#2603)Wires can now be passed as the final argument to an
Operator
, instead of requiring the wires to be explicitly specified with keywordwires
. This functionality already existed forObservable
s, but now extends to allOperator
s: (#2432)>>> qml.S(0) S(wires=[0]) >>> qml.CNOT((0,1)) CNOT(wires=[0, 1])
The
qml.taper
function can now be used to consistently taper any additional observables such as dipole moment, particle number, and spin operators using the symmetries obtained from the Hamiltonian. (#2510)Sparse Hamiltonians’ representation has changed from Coordinate (COO) to Compressed Sparse Row (CSR) format. (#2561)
The CSR representation is more performant for arithmetic operations and matrix-vector products. This change decreases the
expval()
calculation time forqml.SparseHamiltonian
, specially for large workflows. In addition, the CSR format consumes less memory forqml.SparseHamiltonian
storage.IPython now displays the
str
representation of aHamiltonian
, rather than therepr
. This displays more information about the object. (#2648)The
qml.qchem
tests have been restructured. (#2593) (#2545)OpenFermion-dependent tests are now localized and collected in
tests.qchem.of_tests
. The new moduletest_structure
is created to collect the tests of theqchem.structure
module in one place and remove their dependency to OpenFermion.Test classes have been created to group the integrals and matrices unit tests.
An
operations_only
argument is introduced to thetape.get_parameters
method. (#2543)The
gradients
module now uses faster subroutines and uniform formats of gradient rules. (#2452)Instead of checking types, objects are now processed in the
QuantumTape
based on a new_queue_category
property. This is a temporary fix that will disappear in the future. (#2408)The
QNode
class now contains a new methodbest_method_str
that returns the best differentiation method for a provided device and interface, in human-readable format. (#2533)Using
Operation.inv()
in a queuing environment no longer updates the queue’s metadata, but merely updates the operation in place. (#2596)A new method
safe_update_info
is added toqml.QueuingContext
. This method is substituted forqml.QueuingContext.update_info
in a variety of places. (#2612) (#2675)BasisEmbedding
can accept an int as argument instead of a list of bits. (#2601)For example,
qml.BasisEmbedding(4, wires = range(4))
is now equivalent toqml.BasisEmbedding([0,1,0,0], wires = range(4))
(as4==0b100
).Introduced a new
is_hermitian
property toOperator
s to determine if an operator can be used in a measurement process. (#2629)Added separate
requirements_dev.txt
for separation of concerns for code development and just using PennyLane. (#2635)The performance of building sparse Hamiltonians has been improved by accumulating the sparse representation of coefficient-operator pairs in a temporary storage and by eliminating unnecessary
kron
operations on identity matrices. (#2630)Control values are now displayed distinctly in text and matplotlib drawings of circuits. (#2668)
The
TorchLayer
init_method
argument now accepts either atorch.nn.init
function or a dictionary which should specify atorch.nn.init
/torch.Tensor
for each different weight. (#2678)The unused keyword argument
do_queue
forOperation.adjoint
is now fully removed. (#2583)Several non-decomposable
Adjoint
operators are added to the device test suite. (#2658)The developer-facing
pow
method has been added toOperator
with concrete implementations for many classes. (#2225)The
ctrl
transform andControlledOperation
have been moved to the newqml.ops.op_math
submodule. The developer-facingControlledOperation
class is no longer imported top-level. (#2656)
Deprecations
qml.ExpvalCost
has been deprecated, and usage will now raise a warning. (#2571)Instead, it is recommended to simply pass Hamiltonians to the
qml.expval
function inside QNodes:@qml.qnode(dev) def ansatz(params): some_qfunc(params) return qml.expval(Hamiltonian)
Breaking changes
When using
qml.TorchLayer
, weights with negative shapes will now raise an error, while weights withsize = 0
will result in creating empty Tensor objects. (#2678)PennyLane no longer supports TensorFlow
<=2.3
. (#2683)The
qml.queuing.Queue
class has been removed. (#2599)The
qml.utils.expand
function is now removed;qml.operation.expand_matrix
should be used instead. (#2654)The module
qml.gradients.param_shift_hessian
has been renamed toqml.gradients.parameter_shift_hessian
in order to distinguish it from the identically named function. Note that theparam_shift_hessian
function is unaffected by this change and can be invoked in the same manner as before via theqml.gradients
module. (#2528)The properties
eigval
andmatrix
from theOperator
class were replaced with the methodseigval()
andmatrix(wire_order=None)
. (#2498)Operator.decomposition()
is now an instance method, and no longer accepts parameters. (#2498)Adds tests, adds no-coverage directives, and removes inaccessible logic to improve code coverage. (#2537)
The base classes
QubitDevice
andDefaultQubit
now accept data-types for a statevector. This enables a derived class (device) in a plugin to choose correct data-types: (#2448)>>> dev = qml.device("default.qubit", wires=4, r_dtype=np.float32, c_dtype=np.complex64) >>> dev.R_DTYPE <class 'numpy.float32'> >>> dev.C_DTYPE <class 'numpy.complex64'>
Bug fixes
Fixed a bug where returning
qml.density_matrix
using the PyTorch interface would return a density matrix with wrong shape. (#2643)Fixed a bug to make
param_shift_hessian
work with QNodes in which gates marked as trainable do not have any impact on the QNode output. (#2584)QNodes can now interpret variations on the interface name, like
"tensorflow"
or"jax-jit"
, when requesting backpropagation. (#2591)Fixed a bug for
diff_method="adjoint"
where incorrect gradients were computed for QNodes with parametrized observables (e.g.,qml.Hermitian
). (#2543)Fixed a bug where
QNGOptimizer
did not work with operators whose generator was a Hamiltonian. (#2524)Fixed a bug with the decomposition of
qml.CommutingEvolution
. (#2542)Fixed a bug enabling PennyLane to work with the latest version of Autoray. (#2549)
Fixed a bug which caused different behaviour for
Hamiltonian @ Observable
andObservable @ Hamiltonian
. (#2570)Fixed a bug in
DiagonalQubitUnitary._controlled
where an invalid operation was queued instead of the controlled version of the diagonal unitary. (#2525)Updated the gradients fix to only apply to the
strawberryfields.gbs
device, since the original logic was breaking some devices. (#2485) (#2595)Fixed a bug in
qml.transforms.insert
where operations were not inserted after gates within a template. (#2704)Hamiltonian.wires
is now properly updated after in place operations. (#2738)
Documentation
The centralized Xanadu Sphinx Theme is now used to style the Sphinx documentation. (#2450)
Added a reference to
qml.utils.sparse_hamiltonian
inqml.SparseHamiltonian
to clarify how to construct sparse Hamiltonians in PennyLane. (2572)Added a new section in the Gradients and Training page that summarizes the supported device configurations and provides justification. In addition, code examples were added for some selected configurations. (#2540)
Added a note for the Depolarization Channel that specifies how the channel behaves for the different values of depolarization probability
p
. (#2669)The quickstart documentation has been improved. (#2530) (#2534) (#2564 (#2565 (#2566) (#2607) (#2608)
The quantum chemistry quickstart documentation has been improved. (#2500)
Testing documentation has been improved. (#2536)
Documentation for the
pre-commit
package has been added. (#2567)Documentation for draw control wires change has been updated. (#2682)
Contributors
This release contains contributions from (in alphabetical order):
Guillermo Alonso-Linaje, Mikhail Andrenkov, Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Avani Bhardwaj, Albert Mitjans Coma, Isaac De Vlugt, Amintor Dusko, Trent Fridey, Christian Gogolin, Qi Hu, Katharine Hyatt, Josh Izaac, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Ankit Khandelwal, Christina Lee, Chae-Yeun Park, Mason Moreland, Romain Moyard, Maria Schuld, Jay Soni, Antal Száva, tal66, David Wierichs, WingCode.
- orphan
Release 0.23.1¶
Bug fixes
Fixed a bug enabling PennyLane to work with the latest version of Autoray. (#2548)
Contributors
This release contains contributions from (in alphabetical order):
Josh Izaac
- orphan
Release 0.23.0¶
New features since last release
More powerful circuit cutting ✂️
Quantum circuit cutting (running
N
-wire circuits on devices with fewer thanN
wires) is now supported for QNodes of finite-shots using the new@qml.cut_circuit_mc
transform. (#2313) (#2321) (#2332) (#2358) (#2382) (#2399) (#2407) (#2444)With these new additions, samples from the original circuit can be simulated using a Monte Carlo method, using fewer qubits at the expense of more device executions. Additionally, this transform can take an optional classical processing function as an argument and return an expectation value.
The following
3
-qubit circuit contains aWireCut
operation and asample
measurement. When decorated with@qml.cut_circuit_mc
, we can cut the circuit into two2
-qubit fragments:dev = qml.device("default.qubit", wires=2, shots=1000) @qml.cut_circuit_mc @qml.qnode(dev) def circuit(x): qml.RX(0.89, wires=0) qml.RY(0.5, wires=1) qml.RX(1.3, wires=2) qml.CNOT(wires=[0, 1]) qml.WireCut(wires=1) qml.CNOT(wires=[1, 2]) qml.RX(x, wires=0) qml.RY(0.7, wires=1) qml.RX(2.3, wires=2) return qml.sample(wires=[0, 2])
we can then execute the circuit as usual by calling the QNode:
>>> x = 0.3 >>> circuit(x) tensor([[1, 1], [0, 1], [0, 1], ..., [0, 1], [0, 1], [0, 1]], requires_grad=True)
Furthermore, the number of shots can be temporarily altered when calling the QNode:
>>> results = circuit(x, shots=123) >>> results.shape (123, 2)
The
cut_circuit_mc
transform also supports returning sample-based expectation values of observables using theclassical_processing_fn
argument. Refer to theUsageDetails
section of the transform documentation for an example.The
cut_circuit
transform now supports automatic graph partitioning by specifyingauto_cutter=True
to cut arbitrary tape-converted graphs using the general purpose graph partitioning framework KaHyPar. (#2330) (#2428)Note that
KaHyPar
needs to be installed separately with theauto_cutter=True
option.For integration with the existing low-level manual cut pipeline, refer to the documentation of the function .
@qml.cut_circuit(auto_cutter=True) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) qml.RY(0.9, wires=1) qml.RX(0.3, wires=2) qml.CZ(wires=[0, 1]) qml.RY(-0.4, wires=0) qml.CZ(wires=[1, 2]) return qml.expval(qml.grouping.string_to_pauli_word("ZZZ"))
>>> x = np.array(0.531, requires_grad=True) >>> circuit(x) 0.47165198882111165 >>> qml.grad(circuit)(x) -0.276982865449393
Grand QChem unification ⚛️ 🏰
Quantum chemistry functionality — previously split between an external
pennylane-qchem
package and internalqml.hf
differentiable Hartree-Fock solver — is now unified into a single, included,qml.qchem
module. (#2164) (#2385) (#2352) (#2420) (#2454)
(#2199) (#2371) (#2272) (#2230) (#2415) (#2426) (#2465)The
qml.qchem
module provides a differentiable Hartree-Fock solver and the functionality to construct a fully-differentiable molecular Hamiltonian.For example, one can continue to generate molecular Hamiltonians using
qml.qchem.molecular_hamiltonian
:symbols = ["H", "H"] geometry = np.array([[0., 0., -0.66140414], [0., 0., 0.66140414]]) hamiltonian, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, method="dhf")
By default, this will use the differentiable Hartree-Fock solver; however, simply set
method="pyscf"
to continue to use PySCF for Hartree-Fock calculations.Functions are added for building a differentiable dipole moment observable. Functions for computing multipole moment molecular integrals, needed for building the dipole moment observable, are also added. (#2173) (#2166)
The dipole moment observable can be constructed using
qml.qchem.dipole_moment
:symbols = ['H', 'H'] geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) mol = qml.qchem.Molecule(symbols, geometry) args = [geometry] D = qml.qchem.dipole_moment(mol)(*args)
The efficiency of computing molecular integrals and Hamiltonian is improved. This has been done by adding optimized functions for building fermionic and qubit observables and optimizing the functions used for computing the electron repulsion integrals. (#2316)
The
6-31G
basis set is added to the qchem basis set repo. This addition allows performing differentiable Hartree-Fock calculations with basis sets beyond the minimalsto-3g
basis set for atoms with atomic number 1-10. (#2372)The
6-31G
basis set can be used to construct a Hamiltonian assymbols = ["H", "H"] geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) H, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, basis="6-31g")
External dependencies are replaced with local functions for spin and particle number observables. (#2197) (#2362)
Pattern matching optimization 🔎 💎
Added an optimization transform that matches pieces of user-provided identity templates in a circuit and replaces them with an equivalent component. (#2032)
For example, consider the following circuit where we want to replace sequence of two
pennylane.S
gates with apennylane.PauliZ
gate.def circuit(): qml.S(wires=0) qml.PauliZ(wires=0) qml.S(wires=1) qml.CZ(wires=[0, 1]) qml.S(wires=1) qml.S(wires=2) qml.CZ(wires=[1, 2]) qml.S(wires=2) return qml.expval(qml.PauliX(wires=0))
We specify use the following pattern that implements the identity:
with qml.tape.QuantumTape() as pattern: qml.S(wires=0) qml.S(wires=0) qml.PauliZ(wires=0)
To optimize the circuit with this identity pattern, we apply the
qml.transforms.pattern_matching
transform.>>> dev = qml.device('default.qubit', wires=5) >>> qnode = qml.QNode(circuit, dev) >>> optimized_qfunc = qml.transforms.pattern_matching_optimization(pattern_tapes=[pattern])(circuit) >>> optimized_qnode = qml.QNode(optimized_qfunc, dev) >>> print(qml.draw(qnode)()) 0: ──S──Z─╭C──────────┤ <X> 1: ──S────╰Z──S─╭C────┤ 2: ──S──────────╰Z──S─┤ >>> print(qml.draw(optimized_qnode)()) 0: ──S⁻¹─╭C────┤ <X> 1: ──Z───╰Z─╭C─┤ 2: ──Z──────╰Z─┤
For more details on using pattern matching optimization you can check the corresponding documentation and also the following paper.
Measure the distance between two unitaries📏
Added the
HilbertSchmidt
and theLocalHilbertSchmidt
templates to be used for computing distance measures between unitaries. (#2364)Given a unitary
U
,qml.HilberSchmidt
can be used to measure the distance between unitaries and to define a cost function (cost_hst
) used for learning a unitaryV
that is equivalent toU
up to a global phase:# Represents unitary U with qml.tape.QuantumTape(do_queue=False) as u_tape: qml.Hadamard(wires=0) # Represents unitary V def v_function(params): qml.RZ(params[0], wires=1) @qml.qnode(dev) def hilbert_test(v_params, v_function, v_wires, u_tape): qml.HilbertSchmidt(v_params, v_function=v_function, v_wires=v_wires, u_tape=u_tape) return qml.probs(u_tape.wires + v_wires) def cost_hst(parameters, v_function, v_wires, u_tape): return (1 - hilbert_test(v_params=parameters, v_function=v_function, v_wires=v_wires, u_tape=u_tape)[0])
>>> cost_hst(parameters=[0.1], v_function=v_function, v_wires=[1], u_tape=u_tape) tensor(0.999, requires_grad=True)
For more information refer to the documentation of qml.HilbertSchmidt.
More tensor network support 🕸️
Adds the
qml.MERA
template for implementing quantum circuits with the shape of a multi-scale entanglement renormalization ansatz (MERA). (#2418)MERA follows the style of previous tensor network templates and is similar to quantum convolutional neural networks.
def block(weights, wires): qml.CNOT(wires=[wires[0],wires[1]]) qml.RY(weights[0], wires=wires[0]) qml.RY(weights[1], wires=wires[1]) n_wires = 4 n_block_wires = 2 n_params_block = 2 n_blocks = qml.MERA.get_n_blocks(range(n_wires),n_block_wires) template_weights = [[0.1,-0.3]]*n_blocks dev= qml.device('default.qubit',wires=range(n_wires)) @qml.qnode(dev) def circuit(template_weights): qml.MERA(range(n_wires),n_block_wires,block, n_params_block, template_weights) return qml.expval(qml.PauliZ(wires=1))
It may be necessary to reorder the wires to see the MERA architecture clearly:
>>> print(qml.draw(circuit,expansion_strategy='device',wire_order=[2,0,1,3])(template_weights)) 2: ───────────────╭C──RY(0.10)──╭X──RY(-0.30)───────────────┤ 0: ─╭X──RY(-0.30)─│─────────────╰C──RY(0.10)──╭C──RY(0.10)──┤ 1: ─╰C──RY(0.10)──│─────────────╭X──RY(-0.30)─╰X──RY(-0.30)─┤ <Z> 3: ───────────────╰X──RY(-0.30)─╰C──RY(0.10)────────────────┤
New transform for transpilation ⚙️
Added a swap based transpiler transform. (#2118)
The transpile function takes a quantum function and a coupling map as inputs and compiles the circuit to ensure that it can be executed on corresponding hardware. The transform can be used as a decorator in the following way:
dev = qml.device('default.qubit', wires=4) @qml.qnode(dev) @qml.transforms.transpile(coupling_map=[(0, 1), (1, 2), (2, 3)]) def circuit(param): qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[0, 2]) qml.CNOT(wires=[0, 3]) qml.PhaseShift(param, wires=0) return qml.probs(wires=[0, 1, 2, 3])
>>> print(qml.draw(circuit)(0.3)) 0: ─╭C───────╭C──────────╭C──Rϕ(0.30)─┤ ╭Probs 1: ─╰X─╭SWAP─╰X────╭SWAP─╰X───────────┤ ├Probs 2: ────╰SWAP─╭SWAP─╰SWAP──────────────┤ ├Probs 3: ──────────╰SWAP────────────────────┤ ╰Probs
Improvements
QuantumTape
objects are now iterable, allowing iteration over the contained operations and measurements. (#2342)with qml.tape.QuantumTape() as tape: qml.RX(0.432, wires=0) qml.RY(0.543, wires=0) qml.CNOT(wires=[0, 'a']) qml.RX(0.133, wires='a') qml.expval(qml.PauliZ(wires=[0]))
Given a
QuantumTape
object the underlying quantum circuit can be iterated over using afor
loop:>>> for op in tape: ... print(op) RX(0.432, wires=[0]) RY(0.543, wires=[0]) CNOT(wires=[0, 'a']) RX(0.133, wires=['a']) expval(PauliZ(wires=[0]))
Indexing into the circuit is also allowed via
tape[i]
:>>> tape[0] RX(0.432, wires=[0])
A tape object can also be converted to a sequence (e.g., to a
list
) of operations and measurements:>>> list(tape) [RX(0.432, wires=[0]), RY(0.543, wires=[0]), CNOT(wires=[0, 'a']), RX(0.133, wires=['a']), expval(PauliZ(wires=[0]))]
Added the
QuantumTape.shape
method andQuantumTape.numeric_type
attribute to allow extracting information about the shape and numeric type of the output returned by a quantum tape after execution. (#2044)dev = qml.device("default.qubit", wires=2) a = np.array([0.1, 0.2, 0.3]) def func(a): qml.RY(a[0], wires=0) qml.RX(a[1], wires=0) qml.RY(a[2], wires=0) with qml.tape.QuantumTape() as tape: func(a) qml.state()
>>> tape.shape(dev) (1, 4) >>> tape.numeric_type complex
Defined a
MeasurementProcess.shape
method and aMeasurementProcess.numeric_type
attribute to allow extracting information about the shape and numeric type of results obtained when evaluating QNodes using the specific measurement process. (#2044)The parameter-shift Hessian can now be computed for arbitrary operations that support the general parameter-shift rule for gradients, using
qml.gradients.param_shift_hessian
(#2319)Multiple ways to obtain the gradient recipe are supported, in the following order of preference:
A custom
grad_recipe
. It is iterated to obtain the shift rule for the second-order derivatives in the diagonal entries of the Hessian.Custom
parameter_frequencies
. The second-order shift rule can directly be computed using them.An operation’s
generator
. Its eigenvalues will be used to obtainparameter_frequencies
, if they are not given explicitly for an operation.
The strategy for expanding a circuit can now be specified with the
qml.specs
transform, for example to calculate the specifications of the circuit that will actually be executed by the device (expansion_strategy="device"
). (#2395)The
default.qubit
anddefault.mixed
devices now skip over identity operators instead of performing matrix multiplication with the identity. (#2356) (#2365)The function
qml.eigvals
is modified to use the efficientscipy.sparse.linalg.eigsh
method for obtaining the eigenvalues of aSparseHamiltonian
. Thisscipy
method is called to compute \(k\) eigenvalues of a sparse \(N \times N\) matrix ifk
is smaller than \(N-1\). If a larger \(k\) is requested, the dense matrix representation of the Hamiltonian is constructed and the regularqml.math.linalg.eigvalsh
is applied. (#2333)The function
qml.ctrl
was given the optional argumentcontrol_values=None
. If overridden,control_values
takes an integer or a list of integers corresponding to the binary value that each control value should take. The same change is reflected inControlledOperation
. Control values of0
are implemented byqml.PauliX
applied before and after the controlled operation (#2288)Operators now have a
has_matrix
property denoting whether or not the operator defines a matrix. (#2331) (#2476)Circuit cutting now performs expansion to search for wire cuts in contained operations or tapes. (#2340)
The
qml.draw
andqml.draw_mpl
transforms are now located in thedrawer
module. They can still be accessed via the top-levelqml
namespace. (#2396)Raise a warning where caching produces identical shot noise on execution results with finite shots. (#2478)
Deprecations
The
ObservableReturnTypes
Sample
,Variance
,Expectation
,Probability
,State
, andMidMeasure
have been moved tomeasurements
fromoperation
. (#2329) (#2481)
Breaking changes
The caching ability of devices has been removed. Using the caching on the QNode level is the recommended alternative going forward. (#2443)
One way for replicating the removed
QubitDevice
caching behaviour is by creating acache
object (e.g., a dictionary) and passing it to theQNode
:n_wires = 4 wires = range(n_wires) dev = qml.device('default.qubit', wires=n_wires) cache = {} @qml.qnode(dev, diff_method='parameter-shift', cache=cache) def expval_circuit(params): qml.templates.BasicEntanglerLayers(params, wires=wires, rotation=qml.RX) return qml.expval(qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliZ(3)) shape = qml.templates.BasicEntanglerLayers.shape(5, n_wires) params = np.random.random(shape)
>>> expval_circuit(params) tensor(0.20598436, requires_grad=True) >>> dev.num_executions 1 >>> expval_circuit(params) tensor(0.20598436, requires_grad=True) >>> dev.num_executions 1
The
qml.finite_diff
function has been removed. Please useqml.gradients.finite_diff
to compute the gradient of tapes of QNodes. Otherwise, manual implementation is required. (#2464)The
get_unitary_matrix
transform has been removed, please useqml.matrix
instead. (#2457)The
update_stepsize
method has been removed fromGradientDescentOptimizer
and its child optimizers. Thestepsize
property can be interacted with directly instead. (#2370)Most optimizers no longer flatten and unflatten arguments during computation. Due to this change, user provided gradient functions must return the same shape as
qml.grad
. (#2381)The old circuit text drawing infrastructure has been removed. (#2310)
RepresentationResolver
was replaced by theOperator.label
method.qml.drawer.CircuitDrawer
was replaced byqml.drawer.tape_text
.qml.drawer.CHARSETS
was removed because unicode is assumed to be accessible.Grid
andqml.drawer.drawable_grid
were removed because the custom data class was replaced by list of sets of operators or measurements.qml.transforms.draw_old
was replaced byqml.draw
.qml.CircuitGraph.greedy_layers
was deleted, as it was no longer needed by the circuit drawer and did not seem to have uses outside of that situation.qml.CircuitGraph.draw
was deleted, as we draw tapes instead.The tape method
qml.tape.QuantumTape.draw
now simply callsqml.drawer.tape_text
.In the new pathway, the
charset
keyword was deleted, themax_length
keyword defaults to100
, and thedecimals
andshow_matrices
keywords were added.
The deprecated QNode, available via
qml.qnode_old.QNode
, has been removed. Please transition to using the standardqml.QNode
. (#2336) (#2337) (#2338)In addition, several other components which powered the deprecated QNode have been removed:
The deprecated, non-batch compatible interfaces, have been removed.
The deprecated tape subclasses
QubitParamShiftTape
,JacobianTape
,CVParamShiftTape
, andReversibleTape
have been removed.
The deprecated tape execution method
tape.execute(device)
has been removed. Please useqml.execute([tape], device)
instead. (#2339)
Bug fixes
Fixed a bug in the
qml.PauliRot
operation, where computing the generator was not taking into account the operation wires. (#2466)Fixed a bug where non-trainable arguments were shifted in the
NesterovMomentumOptimizer
if a trainable argument was after it in the argument list. (#2466)Fixed a bug with
@jax.jit
for grad whendiff_method="adjoint"
andmode="backward"
. (#2460)Fixed a bug where
qml.DiagonalQubitUnitary
did not support@jax.jit
and@tf.function
. (#2445)Fixed a bug in the
qml.PauliRot
operation, where computing the generator was not taking into account the operation wires. (#2442)Fixed a bug with the padding capability of
AmplitudeEmbedding
where the inputs are on the GPU. (#2431)Fixed a bug by adding a comprehensible error message for calling
qml.probs
without passing wires or an observable. (#2438)The behaviour of
qml.about()
was modified to avoid warnings being emitted due to legacy behaviour ofpip
. (#2422)Fixed a bug where observables were not considered when determining the use of the
jax-jit
interface. (#2427) (#2474)Fixed a bug where computing statistics for a relatively few number of shots (e.g.,
shots=10
), an error arose due to indexing into an array using aDeviceArray
. (#2427)PennyLane Lightning version in Docker container is pulled from latest wheel-builds. (#2416)
Optimizers only consider a variable trainable if they have
requires_grad = True
. (#2381)Fixed a bug with
qml.expval
,qml.var
,qml.state
andqml.probs
(whenqml.probs
is the only measurement) where thedtype
specified on the device did not match thedtype
of the QNode output. (#2367)Fixed a bug where the output shapes from batch transforms are inconsistent with the QNode output shape. (#2215)
Fixed a bug caused by the squeezing in
qml.gradients.param_shift_hessian
. (#2215)Fixed a bug in which the
expval
/var
of aTensor(Observable)
would depend on the order in which the observable is defined: (#2276)>>> @qml.qnode(dev) ... def circ(op): ... qml.RX(0.12, wires=0) ... qml.RX(1.34, wires=1) ... qml.RX(3.67, wires=2) ... return qml.expval(op) >>> op1 = qml.Identity(wires=0) @ qml.Identity(wires=1) @ qml.PauliZ(wires=2) >>> op2 = qml.PauliZ(wires=2) @ qml.Identity(wires=0) @ qml.Identity(wires=1) >>> print(circ(op1), circ(op2)) -0.8636111153905662 -0.8636111153905662
Fixed a bug where
qml.hf.transform_hf()
would fail due to missing wires in the qubit operator that is prepared for tapering the HF state. (#2441)Fixed a bug with custom device defined jacobians not being returned properly. (#2485)
Documentation
The sections on adding operator and observable support in the “How to add a plugin” section of the plugins page have been updated. (#2389)
The missing arXiv reference in the
LieAlgebra
optimizer has been fixed. (#2325)
Contributors
This release contains contributions from (in alphabetical order):
Karim Alaa El-Din, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Alain Delgado, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Angus Lowe, Romain Moyard, Zeyue Niu, Matthew Silverman, Lee James O’Riordan, Maria Schuld, Jay Soni, Antal Száva, Maurice Weber, David Wierichs.
- orphan
Release 0.22.2¶
Bug fixes
Most compilation transforms, and relevant subroutines, have been updated to support just-in-time compilation with jax.jit. This fix was intended to be included in
v0.22.0
, but due to a bug was incomplete. (#2397)
Documentation
The documentation run has been updated to require
jinja2==3.0.3
due to an issue that arises withjinja2
v3.1.0
andsphinx
v3.5.3
. (#2378)
Contributors
This release contains contributions from (in alphabetical order):
Olivia Di Matteo, Christina Lee, Romain Moyard, Antal Száva.
- orphan
Release 0.22.1¶
Bug fixes
Fixes cases with
qml.measure
where unexpected operations were added to the circuit. (#2328)
Contributors
This release contains contributions from (in alphabetical order):
Guillermo Alonso-Linaje, Antal Száva.
- orphan
Release 0.22.0¶
New features since last release
Quantum circuit cutting ✂️
You can now run
N
-wire circuits on devices with fewer thanN
wires, by strategically placingWireCut
operations that allow their circuit to be partitioned into smaller fragments, at a cost of needing to perform a greater number of device executions. Circuit cutting is enabled by decorating a QNode with the@qml.cut_circuit
transform. (#2107) (#2124) (#2153) (#2165) (#2158) (#2169) (#2192) (#2216) (#2168) (#2223) (#2231) (#2234) (#2244) (#2251) (#2265) (#2254) (#2260) (#2257) (#2279)The example below shows how a three-wire circuit can be run on a two-wire device:
dev = qml.device("default.qubit", wires=2) @qml.cut_circuit @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) qml.RY(0.9, wires=1) qml.RX(0.3, wires=2) qml.CZ(wires=[0, 1]) qml.RY(-0.4, wires=0) qml.WireCut(wires=1) qml.CZ(wires=[1, 2]) return qml.expval(qml.grouping.string_to_pauli_word("ZZZ"))
Instead of executing the circuit directly, it will be partitioned into smaller fragments according to the
WireCut
locations, and each fragment executed multiple times. Combining the results of the fragment executions will recover the expected output of the original uncut circuit.>>> x = np.array(0.531, requires_grad=True) >>> circuit(0.531) 0.47165198882111165
Circuit cutting support is also differentiable:
>>> qml.grad(circuit)(x) -0.276982865449393
For more details on circuit cutting, check out the qml.cut_circuit documentation page or Peng et. al.
Conditional operations: quantum teleportation unlocked 🔓🌀
Support for mid-circuit measurements and conditional operations has been added, to enable use cases like quantum teleportation, quantum error correction and quantum error mitigation. (#2211) (#2236) (#2275)
Two new functions have been added to support this capability:
qml.measure()
places mid-circuit measurements in the middle of a quantum function.qml.cond()
allows operations and quantum functions to be conditioned on the result of a previous measurement.
For example, the code below shows how to teleport a qubit from wire 0 to wire 2:
dev = qml.device("default.qubit", wires=3) input_state = np.array([1, -1], requires_grad=False) / np.sqrt(2) @qml.qnode(dev) def teleport(state): # Prepare input state qml.QubitStateVector(state, wires=0) # Prepare Bell state qml.Hadamard(wires=1) qml.CNOT(wires=[1, 2]) # Apply gates qml.CNOT(wires=[0, 1]) qml.Hadamard(wires=0) # Measure first two wires m1 = qml.measure(0) m2 = qml.measure(1) # Condition final wire on results qml.cond(m2 == 1, qml.PauliX)(wires=2) qml.cond(m1 == 1, qml.PauliZ)(wires=2) # Return state on final wire return qml.density_matrix(wires=2)
We can double-check that the qubit has been teleported by computing the overlap between the input state and the resulting state on wire 2:
>>> output_state = teleport(input_state) >>> output_state tensor([[ 0.5+0.j, -0.5+0.j], [-0.5+0.j, 0.5+0.j]], requires_grad=True) >>> input_state.conj() @ output_state @ input_state tensor(1.+0.j, requires_grad=True)
For a full description of new capabilities, refer to the Mid-circuit measurements and conditional operations section in the documentation.
Train mid-circuit measurements by deferring them, via the new
@qml.defer_measurements
transform. (#2211) (#2236) (#2275)If a device doesn’t natively support mid-circuit measurements, the
@qml.defer_measurements
transform can be applied to the QNode to transform the QNode into one with terminal measurements and controlled operations:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) @qml.defer_measurements def circuit(x): qml.Hadamard(wires=0) m = qml.measure(0) def op_if_true(): return qml.RX(x**2, wires=1) def op_if_false(): return qml.RY(x, wires=1) qml.cond(m==1, op_if_true, op_if_false)() return qml.expval(qml.PauliZ(1))
>>> x = np.array(0.7, requires_grad=True) >>> print(qml.draw(circuit, expansion_strategy="device")(x)) 0: ──H─╭C─────────X─╭C─────────X─┤ 1: ────╰RX(0.49)────╰RY(0.70)────┤ <Z> >>> circuit(x) tensor(0.82358752, requires_grad=True)
Deferring mid-circuit measurements also enables differentiation:
>>> qml.grad(circuit)(x) -0.651546965338656
Debug with mid-circuit quantum snapshots 📷
A new operation
qml.Snapshot
has been added to assist in debugging quantum functions. (#2233) (#2289) (#2291) (#2315)qml.Snapshot
saves the internal state of devices at arbitrary points of execution.Currently supported devices include:
default.qubit
: each snapshot saves the quantum state vectordefault.mixed
: each snapshot saves the density matrixdefault.gaussian
: each snapshot saves the covariance matrix and vector of means
During normal execution, the snapshots are ignored:
dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, interface=None) def circuit(): qml.Snapshot() qml.Hadamard(wires=0) qml.Snapshot("very_important_state") qml.CNOT(wires=[0, 1]) qml.Snapshot() return qml.expval(qml.PauliX(0))
However, when using the
qml.snapshots
transform, intermediate device states will be stored and returned alongside the results.>>> qml.snapshots(circuit)() {0: array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]), 'very_important_state': array([0.70710678+0.j, 0. +0.j, 0.70710678+0.j, 0. +0.j]), 2: array([0.70710678+0.j, 0. +0.j, 0. +0.j, 0.70710678+0.j]), 'execution_results': array(0.)}
Batch embedding and state preparation data 📦
Added the
@qml.batch_input
transform to enable batching non-trainable gate parameters. In addition, theqml.qnn.KerasLayer
class has been updated to natively support batched training data. (#2069)As with other transforms,
@qml.batch_input
can be used to decorate QNodes:dev = qml.device("default.qubit", wires=2, shots=None) @qml.batch_input(argnum=0) @qml.qnode(dev, diff_method="parameter-shift", interface="tf") def circuit(inputs, weights): # add a batch dimension to the embedding data qml.AngleEmbedding(inputs, wires=range(2), rotation="Y") qml.RY(weights[0], wires=0) qml.RY(weights[1], wires=1) return qml.expval(qml.PauliZ(1))
Batched input parameters can then be passed during QNode evaluation:
>>> x = tf.random.uniform((10, 2), 0, 1) >>> w = tf.random.uniform((2,), 0, 1) >>> circuit(x, w) <tf.Tensor: shape=(10,), dtype=float64, numpy= array([0.46230079, 0.73971315, 0.95666004, 0.5355225 , 0.66180948, 0.44519553, 0.93874261, 0.9483197 , 0.78737918, 0.90866411])>
Even more mighty quantum transforms 🐛➡🦋
New functions and transforms of operators have been added:
qml.matrix()
for computing the matrix representation of one or more unitary operators. (#2241)qml.eigvals()
for computing the eigenvalues of one or more operators. (#2248)qml.generator()
for computing the generator of a single-parameter unitary operation. (#2256)
All operator transforms can be used on instantiated operators,
>>> op = qml.RX(0.54, wires=0) >>> qml.matrix(op) [[0.9637709+0.j 0. -0.26673144j] [0. -0.26673144j 0.9637709+0.j ]]
Operator transforms can also be used in a functional form:
>>> x = torch.tensor(0.6, requires_grad=True) >>> matrix_fn = qml.matrix(qml.RX) >>> matrix_fn(x, wires=[0]) tensor([[0.9553+0.0000j, 0.0000-0.2955j], [0.0000-0.2955j, 0.9553+0.0000j]], grad_fn=<AddBackward0>)
In its functional form, it is fully differentiable with respect to gate arguments:
>>> loss = torch.real(torch.trace(matrix_fn(x, wires=0))) >>> loss.backward() >>> x.grad tensor(-0.2955)
Some operator transform can also act on multiple operations, by passing quantum functions or tapes:
>>> def circuit(theta): ... qml.RX(theta, wires=1) ... qml.PauliZ(wires=0) >>> qml.matrix(circuit)(np.pi / 4) array([[ 0.92387953+0.j, 0.+0.j , 0.-0.38268343j, 0.+0.j], [ 0.+0.j, -0.92387953+0.j, 0.+0.j, 0. +0.38268343j], [ 0. -0.38268343j, 0.+0.j, 0.92387953+0.j, 0.+0.j], [ 0.+0.j, 0.+0.38268343j, 0.+0.j, -0.92387953+0.j]])
A new transform has been added to construct the pairwise-commutation directed acyclic graph (DAG) representation of a quantum circuit. (#1712)
In the DAG, each node represents a quantum operation, and edges represent non-commutation between two operations.
This transform takes into account that not all operations can be moved next to each other by pairwise commutation:
>>> def circuit(x, y, z): ... qml.RX(x, wires=0) ... qml.RX(y, wires=0) ... qml.CNOT(wires=[1, 2]) ... qml.RY(y, wires=1) ... qml.Hadamard(wires=2) ... qml.CRZ(z, wires=[2, 0]) ... qml.RY(-y, wires=1) ... return qml.expval(qml.PauliZ(0)) >>> dag_fn = qml.commutation_dag(circuit) >>> dag = dag_fn(np.pi / 4, np.pi / 3, np.pi / 2)
Nodes in the commutation DAG can be accessed via the
get_nodes()
method, returning a list of the form(ID, CommutationDAGNode)
:>>> nodes = dag.get_nodes() >>> nodes NodeDataView({0: <pennylane.transforms.commutation_dag.CommutationDAGNode object at 0x7f461c4bb580>, ...}, data='node')
Specific nodes in the commutation DAG can be accessed via the
get_node()
method:>>> second_node = dag.get_node(2) >>> second_node <pennylane.transforms.commutation_dag.CommutationDAGNode object at 0x136f8c4c0> >>> second_node.op CNOT(wires=[1, 2]) >>> second_node.successors [3, 4, 5, 6] >>> second_node.predecessors []
Improvements
The text-based drawer accessed via
qml.draw()
has been optimized and improved. (#2128) (#2198)The new drawer has:
a
decimals
keyword for controlling parameter roundinga
show_matrices
keyword for controlling display of matricesa different algorithm for determining positions
deprecation of the
charset
keywordadditional minor cosmetic changes
@qml.qnode(qml.device('lightning.qubit', wires=2)) 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))
>>> print(qml.draw(circuit, decimals=2)(a=2.3, w=[1.2, 3.2, 0.7])) 0: ──H─╭C─────────────────────────────╭C─────────┤ ╭<Z@Z> 1: ────╰RX(2.30)──Rot(1.20,3.20,0.70)─╰RX(-2.30)─┤ ╰<Z@Z>
The frequencies of gate parameters are now accessible as an operation property and can be used for circuit analysis, optimization via the
RotosolveOptimizer
and differentiation with the parameter-shift rule (including the general shift rule). (#2180) (#2182) (#2227)>>> op = qml.CRot(0.4, 0.1, 0.3, wires=[0, 1]) >>> op.parameter_frequencies [(0.5, 1.0), (0.5, 1.0), (0.5, 1.0)]
When using
qml.gradients.param_shift
, either a customgrad_recipe
or the parameter frequencies are used to obtain the shift rule for the operation, in that order of preference.See Vidal and Theis (2018) and Wierichs et al. (2021) for theoretical background information on the general parameter-shift rule.
No two-term parameter-shift rule is assumed anymore by default. (#2227)
Previously, operations marked for analytic differentiation that did not provide a
generator
,parameter_frequencies
or a customgrad_recipe
were assumed to satisfy the two-term shift rule. This now has to be made explicit for custom operations by adding any of the above attributes.Most compilation transforms, and relevant subroutines, have been updated to support just-in-time compilation with
jax.jit
. (#1894)The
qml.draw_mpl
transform supports aexpansion_strategy
keyword argument. (#2271)The
qml.gradients
module has been streamlined and special-purpose functions moved closer to their use cases, while preserving existing behaviour. (#2200)Added a new
partition_pauli_group
function to thegrouping
module for efficiently measuring theN
-qubit Pauli group with3 ** N
qubit-wise commuting terms. (#2185)The Operator class has undergone a major refactor with the following changes:
Matrices: the static method
Operator.compute_matrices()
defines the matrix representation of the operator, and the functionqml.matrix(op)
computes this for a given instance. (#1996)Eigvals: the static method
Operator.compute_eigvals()
defines the matrix representation of the operator, and the functionqml.eigvals(op)
computes this for a given instance. (#2048)Decompositions: the static method
Operator.compute_decomposition()
defines the matrix representation of the operator, and the methodop.decomposition()
computes this for a given instance. (#2024) (#2053)Sparse matrices: the static method
Operator.compute_sparse_matrix()
defines the sparse matrix representation of the operator, and the methodop.sparse_matrix()
computes this for a given instance. (#2050)Linear combinations of operators: The static method
compute_terms()
, used for representing the linear combination of coefficients and operators representing the operator, has been added. The methodop.terms()
computes this for a given instance. Currently, only theHamiltonian
class overwritescompute_terms()
to store coefficients and operators. TheHamiltonian.terms
property hence becomes a proper method called byHamiltonian.terms()
. (#2036)Diagonalization: The
diagonalizing_gates()
representation has been moved to the highest-levelOperator
class and is therefore available to all subclasses. A conditionqml.operation.defines_diagonalizing_gates
has been added, which can be used in tape contexts without queueing. In addition, a staticcompute_diagonalizing_gates
method has been added, which is called by default indiagonalizing_gates()
. (#1985) (#1993)Error handling has been improved for Operator representations. Custom errors subclassing
OperatorPropertyUndefined
are raised if a representation has not been defined. This replaces theNotImplementedError
and allows finer control for developers. (#2064) (#2287)A
Operator.hyperparameters
attribute, used for storing operation parameters that are never trainable, has been added to the operator class. (#2017)The
string_for_inverse
attribute is removed. (#2021)The
expand()
method was moved from theOperation
class to the mainOperator
class. (#2053) (#2239)
Deprecations
There are several important changes when creating custom operations: (#2214) (#2227) (#2030) (#2061)
The
Operator.matrix
method has been deprecated andOperator.compute_matrix
should be defined instead. Operator matrices should be accessed usingqml.matrix(op)
. If you were previously defining the class methodOperator._matrix()
, this is a a breaking change — please update your operation to instead overwriteOperator.compute_matrix
.The
Operator.decomposition
method has been deprecated andOperator.compute_decomposition
should be defined instead. Operator decompositions should be accessed usingOperator.decomposition()
.The
Operator.eigvals
method has been deprecated andOperator.compute_eigvals
should be defined instead. Operator eigenvalues should be accessed usingqml.eigvals(op)
.The
Operator.generator
property is now a method, and should return an operator instance representing the generator. Note that unlike the other representations above, this is a breaking change. Operator generators should be accessed usingqml.generator(op)
.The
Operation.get_parameter_shift
method has been deprecated and will be removed in a future release.Instead, the functionalities for general parameter-shift rules in the
qml.gradients
module should be used, together with the operation attributesparameter_frequencies
orgrad_recipe
.
Executing tapes using
tape.execute(dev)
is deprecated. Please use theqml.execute([tape], dev)
function instead. (#2306)The subclasses of the quantum tape, including
JacobianTape
,QubitParamShiftTape
,CVParamShiftTape
, andReversibleTape
are deprecated. Instead of callingJacobianTape.jacobian()
andJacobianTape.hessian()
, please use a standardQuantumTape
, and apply gradient transforms using theqml.gradients
module. (#2306)qml.transforms.get_unitary_matrix()
has been deprecated and will be removed in a future release. For extracting matrices of operations and quantum functions, please useqml.matrix()
. (#2248)The
qml.finite_diff()
function has been deprecated and will be removed in an upcoming release. Instead,qml.gradients.finite_diff()
can be used to compute purely quantum gradients (that is, gradients of tapes or QNode). (#2212)The
MultiControlledX
operation now accepts a singlewires
keyword argument for bothcontrol_wires
andwires
. The singlewires
keyword should be all the control wires followed by a single target wire. (#2121) (#2278)
Breaking changes
The representation of an operator as a matrix has been overhauled. (#1996)
The “canonical matrix”, which is independent of wires, is now defined in the static method
compute_matrix()
instead of_matrix
. By default, this method is assumed to take all parameters and non-trainable hyperparameters that define the operation.>>> qml.RX.compute_matrix(0.5) [[0.96891242+0.j 0. -0.24740396j] [0. -0.24740396j 0.96891242+0.j ]]
If no canonical matrix is specified for a gate,
compute_matrix()
raises aMatrixUndefinedError
.The generator property has been updated to an instance method,
Operator.generator()
. It now returns an instantiated operation, representing the generator of the instantiated operator. (#2030) (#2061)Various operators have been updated to specify the generator as either an
Observable
,Tensor
,Hamiltonian
,SparseHamiltonian
, orHermitian
operator.In addition,
qml.generator(operation)
has been added to aid in retrieving generator representations of operators.The argument
wires
inheisenberg_obs
,heisenberg_expand
andheisenberg_tr
was renamed towire_order
to be consistent with other matrix representations. (#2051)The property
kraus_matrices
has been changed to a method, and_kraus_matrices
renamed tocompute_kraus_matrices
, which is now a static method. (#2055)The
pennylane.measure
module has been renamed topennylane.measurements
. (#2236)
Bug fixes
The
basis
property ofqml.SWAP
was set to"X"
, which is incorrect; it is now set toNone
. (#2287)The
qml.RandomLayers
template now decomposes when the weights are a list of lists. (#2266)The
qml.QubitUnitary
operation now supports just-in-time compilation using JAX. (#2249)Fixes a bug in the JAX interface where
DeviceArray
objects were not being converted to NumPy arrays before executing an external device. (#2255)The
qml.ctrl
transform now works correctly with gradient transforms such as the parameter-shift rule. (#2238)Fixes a bug in which passing required arguments into operations as keyword arguments would throw an error because the documented call signature didn’t match the function definition. (#1976)
The operation
OrbitalRotation
previously was wrongfully registered to satisfy the four-term parameter shift rule. The correct eight-term rule will now be used when using the parameter-shift rule. (#2180)Fixes a bug where
qml.gradients.param_shift_hessian
would produce an error whenever all elements of the Hessian are known in advance to be 0. (#2299)
Documentation
The developer guide on adding templates and the architecture overview were rewritten to reflect the past and planned changes of the operator refactor. (#2066)
Links to the Strawberry Fields documentation for information on the CV model. (#2259)
Fixes the documentation example for
qml.QFT
. (#2232)Fixes the documentation example for using
qml.sample
withjax.jit
. (#2196)The
qml.numpy
subpackage is now included in the PennyLane API documentation. (#2179)Improves the documentation of
RotosolveOptimizer
regarding the usage of the passedsubstep_optimizer
and its keyword arguments. (#2160)Ensures that signatures of
@qml.qfunc_transform
decorated functions display correctly in the docs. (#2286)Docstring examples now display using the updated text-based circuit drawer. (#2252)
Add docstring to
OrbitalRotation.grad_recipe
. (#2193)
Contributors
This release contains contributions from (in alphabetical order):
Catalina Albornoz, Jack Y. Araz, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Olivia Di Matteo, Christian Gogolin, Diego Guala, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Angus Lowe, Maria Fernanda Morris, Romain Moyard, Zeyue Niu, Lee James O’Riordan, Chae-Yeun Park, Maria Schuld, Jay Soni, Antal Száva, David Wierichs.
- orphan
Release 0.21.0¶
New features since last release
Reduce qubit requirements of simulating Hamiltonians ⚛️
Functions for tapering qubits based on molecular symmetries have been added, following results from Setia et al. (#1966) (#1974) (#2041) (#2042)
With this functionality, a molecular Hamiltonian and the corresponding Hartree-Fock (HF) state can be transformed to a new Hamiltonian and HF state that acts on a reduced number of qubits, respectively.
# molecular geometry symbols = ["He", "H"] geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]]) mol = qml.hf.Molecule(symbols, geometry, charge=1) # generate the qubit Hamiltonian H = qml.hf.generate_hamiltonian(mol)(geometry) # determine Hamiltonian symmetries generators, paulix_ops = qml.hf.generate_symmetries(H, len(H.wires)) opt_sector = qml.hf.optimal_sector(H, generators, mol.n_electrons) # taper the Hamiltonian H_tapered = qml.hf.transform_hamiltonian(H, generators, paulix_ops, opt_sector)
We can compare the number of qubits required by the original Hamiltonian and the tapered Hamiltonian:
>>> len(H.wires) 4 >>> len(H_tapered.wires) 2
For quantum chemistry algorithms, the Hartree-Fock state can also be tapered:
n_elec = mol.n_electrons n_qubits = mol.n_orbitals * 2 hf_tapered = qml.hf.transform_hf( generators, paulix_ops, opt_sector, n_elec, n_qubits )
>>> hf_tapered tensor([1, 1], requires_grad=True)
New tensor network templates 🪢
Quantum circuits with the shape of a matrix product state tensor network can now be easily implemented using the new
qml.MPS
template, based on the work arXiv:1803.11537. (#1871)def block(weights, wires): qml.CNOT(wires=[wires[0], wires[1]]) qml.RY(weights[0], wires=wires[0]) qml.RY(weights[1], wires=wires[1]) n_wires = 4 n_block_wires = 2 n_params_block = 2 template_weights = np.array([[0.1, -0.3], [0.4, 0.2], [-0.15, 0.5]], requires_grad=True) dev = qml.device("default.qubit", wires=range(n_wires)) @qml.qnode(dev) def circuit(weights): qml.MPS(range(n_wires), n_block_wires, block, n_params_block, weights) return qml.expval(qml.PauliZ(wires=n_wires - 1))
The resulting circuit is:
>>> print(qml.draw(circuit, expansion_strategy="device")(template_weights)) 0: ──╭C──RY(0.1)───────────────────────────────┤ 1: ──╰X──RY(-0.3)──╭C──RY(0.4)─────────────────┤ 2: ────────────────╰X──RY(0.2)──╭C──RY(-0.15)──┤ 3: ─────────────────────────────╰X──RY(0.5)────┤ ⟨Z⟩
Added a template for tree tensor networks,
qml.TTN
. (#2043)def block(weights, wires): qml.CNOT(wires=[wires[0], wires[1]]) qml.RY(weights[0], wires=wires[0]) qml.RY(weights[1], wires=wires[1]) n_wires = 4 n_block_wires = 2 n_params_block = 2 n_blocks = qml.MPS.get_n_blocks(range(n_wires), n_block_wires) template_weights = [[0.1, -0.3]] * n_blocks dev = qml.device("default.qubit", wires=range(n_wires)) @qml.qnode(dev) def circuit(template_weights): qml.TTN(range(n_wires), n_block_wires, block, n_params_block, template_weights) return qml.expval(qml.PauliZ(wires=n_wires - 1))
The resulting circuit is:
>>> print(qml.draw(circuit, expansion_strategy="device")(template_weights)) 0: ──╭C──RY(0.1)─────────────────┤ 1: ──╰X──RY(-0.3)──╭C──RY(0.1)───┤ 2: ──╭C──RY(0.1)───│─────────────┤ 3: ──╰X──RY(-0.3)──╰X──RY(-0.3)──┤ ⟨Z⟩
Generalized RotosolveOptmizer 📉
The
RotosolveOptimizer
has been generalized to arbitrary frequency spectra in the cost function. Also note the changes in behaviour listed under Breaking changes. (#2081)Previously, the RotosolveOptimizer only supported variational circuits using special gates such as single-qubit Pauli rotations. Now, circuits with arbitrary gates are supported natively without decomposition, as long as the frequencies of the gate parameters are known. This new generalization extends the Rotosolve optimization method to a larger class of circuits, and can reduce the cost of the optimization compared to decomposing all gates to single-qubit rotations.
Consider the QNode
dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def qnode(x, Y): qml.RX(2.5 * x, wires=0) qml.CNOT(wires=[0, 1]) qml.RZ(0.3 * Y[0], wires=0) qml.CRY(1.1 * Y[1], wires=[1, 0]) return qml.expval(qml.PauliX(0) @ qml.PauliZ(1)) x = np.array(0.8, requires_grad=True) Y = np.array([-0.2, 1.5], requires_grad=True)
Its frequency spectra can be easily obtained via
qml.fourier.qnode_spectrum
:>>> spectra = qml.fourier.qnode_spectrum(qnode)(x, Y) >>> spectra {'x': {(): [-2.5, 0.0, 2.5]}, 'Y': {(0,): [-0.3, 0.0, 0.3], (1,): [-1.1, -0.55, 0.0, 0.55, 1.1]}}
We may then initialize the
RotosolveOptimizer
and minimize the QNode cost function by providing this information about the frequency spectra. We also compare the cost at each step to the initial cost.>>> cost_init = qnode(x, Y) >>> opt = qml.RotosolveOptimizer() >>> for _ in range(2): ... x, Y = opt.step(qnode, x, Y, spectra=spectra) ... print(f"New cost: {np.round(qnode(x, Y), 3)}; Initial cost: {np.round(cost_init, 3)}") New cost: 0.0; Initial cost: 0.706 New cost: -1.0; Initial cost: 0.706
The optimization with
RotosolveOptimizer
is performed in substeps. The minimal cost of these substeps can be retrieved by settingfull_output=True
.>>> x = np.array(0.8, requires_grad=True) >>> Y = np.array([-0.2, 1.5], requires_grad=True) >>> opt = qml.RotosolveOptimizer() >>> for _ in range(2): ... (x, Y), history = opt.step(qnode, x, Y, spectra=spectra, full_output=True) ... print(f"New cost: {np.round(qnode(x, Y), 3)} reached via substeps {np.round(history, 3)}") New cost: 0.0 reached via substeps [-0. 0. 0.] New cost: -1.0 reached via substeps [-1. -1. -1.]
However, note that these intermediate minimal values are evaluations of the reconstructions that Rotosolve creates and uses internally for the optimization, and not of the original objective function. For noisy cost functions, these intermediate evaluations may differ significantly from evaluations of the original cost function.
Improved JAX support 💻
The JAX interface now supports evaluating vector-valued QNodes. (#2110)
Vector-valued QNodes include those with:
qml.probs
;qml.state
;qml.sample
ormultiple
qml.expval
/qml.var
measurements.
Consider a QNode that returns basis-state probabilities:
dev = qml.device('default.qubit', wires=2) x = jnp.array(0.543) y = jnp.array(-0.654) @qml.qnode(dev, diff_method="parameter-shift", interface="jax") def circuit(x, y): qml.RX(x, wires=[0]) qml.RY(y, wires=[1]) qml.CNOT(wires=[0, 1]) return qml.probs(wires=[1])
The QNode can be evaluated and its jacobian can be computed:
>>> circuit(x, y) DeviceArray([0.8397495 , 0.16025047], dtype=float32) >>> jax.jacobian(circuit, argnums=[0, 1])(x, y) (DeviceArray([-0.2050439, 0.2050439], dtype=float32, weak_type=True), DeviceArray([ 0.26043, -0.26043], dtype=float32, weak_type=True))
Note that
jax.jit
is not yet supported for vector-valued QNodes.
Speedier quantum natural gradient ⚡
A new function for computing the metric tensor on simulators,
qml.adjoint_metric_tensor
, has been added, that uses classically efficient methods to massively improve performance. (#1992)This method, detailed in Jones (2020), computes the metric tensor using four copies of the state vector and a number of operations that scales quadratically in the number of trainable parameters.
Note that as it makes use of state cloning, it is inherently classical and can only be used with statevector simulators and
shots=None
.It is particularly useful for larger circuits for which backpropagation requires inconvenient or even unfeasible amounts of storage, but is slower. Furthermore, the adjoint method is only available for analytic computation, not for measurements simulation with
shots!=None
.dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circuit(x, y): qml.Rot(*x[0], wires=0) qml.Rot(*x[1], wires=1) qml.Rot(*x[2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) qml.RY(y[0], wires=0) qml.RY(y[1], wires=1) qml.RY(y[0], wires=2) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(1)) x = np.array([[0.2, 0.4, -0.1], [-2.1, 0.5, -0.2], [0.1, 0.7, -0.6]], requires_grad=False) y = np.array([1.3, 0.2], requires_grad=True)
>>> qml.adjoint_metric_tensor(circuit)(x, y) tensor([[ 0.25495723, -0.07086695], [-0.07086695, 0.24945606]], requires_grad=True)
Computational cost
The adjoint method uses \(2P^2+4P+1\) gates and state cloning operations if the circuit is composed only of trainable gates, where \(P\) is the number of trainable operations. If non-trainable gates are included, each of them is applied about \(n^2-n\) times, where \(n\) is the number of trainable operations that follow after the respective non-trainable operation in the circuit. This means that non-trainable gates later in the circuit are executed less often, making the adjoint method a bit cheaper if such gates appear later. The adjoint method requires memory for 4 independent state vectors, which corresponds roughly to storing a state vector of a system with 2 additional qubits.
Compute the Hessian on hardware ⬆️
A new gradient transform
qml.gradients.param_shift_hessian
has been added to directly compute the Hessian (2nd order partial derivative matrix) of QNodes on hardware. (#1884)The function generates parameter-shifted tapes which allow the Hessian to be computed analytically on hardware and software devices. Compared to using an auto-differentiation framework to compute the Hessian via parameter shifts, this function will use fewer device invocations and can be used to inspect the parameter-shifted “Hessian tapes” directly. The function remains fully differentiable on all supported PennyLane interfaces.
Additionally, the parameter-shift Hessian comes with a new batch transform decorator
@qml.gradients.hessian_transform
, which can be used to create custom Hessian functions.The following code demonstrates how to use the parameter-shift Hessian:
dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=0) return qml.expval(qml.PauliZ(0)) x = np.array([0.1, 0.2], requires_grad=True) hessian = qml.gradients.param_shift_hessian(circuit)(x)
>>> hessian tensor([[-0.97517033, 0.01983384], [ 0.01983384, -0.97517033]], requires_grad=True)
Improvements
The
qml.transforms.insert
transform now supports adding operation after or before certain specific gates. (#1980)Added a modified version of the
simplify
function to thehf
module. (#2103)This function combines redundant terms in a Hamiltonian and eliminates terms with a coefficient smaller than a cutoff value. The new function makes construction of molecular Hamiltonians more efficient. For LiH, as an example, the time to construct the Hamiltonian is reduced roughly by a factor of 20.
The QAOA module now accepts both NetworkX and RetworkX graphs as function inputs. (#1791)
The
CircuitGraph
, used to represent circuits via directed acyclic graphs, now uses RetworkX for its internal representation. This results in significant speedup for algorithms that rely on a directed acyclic graph representation. (#1791)For subclasses of
Operator
where the number of parameters is known before instantiation, thenum_params
is reverted back to being a static property. This allows to programmatically know the number of parameters before an operator is instantiated without changing the user interface. A test was added to ensure that different ways of definingnum_params
work as expected. (#2101) (#2135)A
WireCut
operator has been added for manual wire cut placement when constructing a QNode. (#2093)The new function
qml.drawer.tape_text
produces a string drawing of a tape. This function differs in implementation and minor stylistic details from the old string circuit drawing infrastructure. (#1885)The
RotosolveOptimizer
now raises an error if no trainable arguments are detected, instead of silently skipping update steps for all arguments. (#2109)The function
qml.math.safe_squeeze
is introduced andgradient_transform
allows for QNode argument axes of size1
. (#2080)qml.math.safe_squeeze
wrapsqml.math.squeeze
, with slight modifications:When provided the
axis
keyword argument, axes that do not have size1
will be ignored, instead of raising an error.The keyword argument
exclude_axis
allows to explicitly exclude axes from the squeezing.
The
adjoint
transform now raises and error whenever the object it is applied to is not callable. (#2060)An example is a list of operations to which one might apply
qml.adjoint
:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit_wrong(params): # Note the difference: v v qml.adjoint(qml.templates.AngleEmbedding(params, wires=dev.wires)) return qml.state() @qml.qnode(dev) def circuit_correct(params): # Note the difference: v v qml.adjoint(qml.templates.AngleEmbedding)(params, wires=dev.wires) return qml.state() params = list(range(1, 3))
Evaluating
circuit_wrong(params)
now raises aValueError
and if we applyqml.adjoint
correctly, we get>>> circuit_correct(params) [ 0.47415988+0.j 0. 0.73846026j 0. 0.25903472j -0.40342268+0.j ]
A precision argument has been added to the tape’s
to_openqasm
function to control the precision of parameters. (#2071)Interferometer now has a
shape
method. (#1946)The Barrier and Identity operations now support the
adjoint
method. (#2062) (#2063)qml.BasisStatePreparation
now supports thebatch_params
decorator. (#2091)Added a new
multi_dispatch
decorator that helps ease the definition of new functions inside PennyLane. The decorator is used throughout the math module, demonstrating use cases. (#2082) (#2096)We can decorate a function, indicating the arguments that are tensors handled by the interface:
>>> @qml.math.multi_dispatch(argnum=[0, 1]) ... def some_function(tensor1, tensor2, option, like): ... # the interface string is stored in ``like``. ... ...
Previously, this was done using the private utility function
_multi_dispatch
.>>> def some_function(tensor1, tensor2, option): ... interface = qml.math._multi_dispatch([tensor1, tensor2]) ... ...
The
IsingZZ
gate was added to thediagonal_in_z_basis
attribute. For this an explicit_eigvals
method was added. (#2113)The
IsingXX
,IsingYY
andIsingZZ
gates were added to thecomposable_rotations
attribute. (#2113)
Breaking changes
QNode arguments will no longer be considered trainable by default when using the Autograd interface. In order to obtain derivatives with respect to a parameter, it should be instantiated via PennyLane’s NumPy wrapper using the
requires_grad=True
attribute. The previous behaviour was deprecated in version v0.19.0 of PennyLane. (#2116) (#2125) (#2139) (#2148) (#2156)from pennylane import numpy as np @qml.qnode(qml.device("default.qubit", wires=2)) def circuit(x): ... x = np.array([0.1, 0.2], requires_grad=True) qml.grad(circuit)(x)
For the
qml.grad
andqml.jacobian
functions, trainability can alternatively be indicated via theargnum
keyword:import numpy as np @qml.qnode(qml.device("default.qubit", wires=2)) def circuit(hyperparam, param): ... x = np.array([0.1, 0.2]) qml.grad(circuit, argnum=1)(0.5, x)
qml.jacobian
now follows a different convention regarding its output shape. (#2059)Previously,
qml.jacobian
would attempt to stack the Jacobian for multiple QNode arguments, which succeeded whenever the arguments have the same shape. In this case, the stacked Jacobian would also be transposed, leading to the output shape(*reverse_QNode_args_shape, *reverse_output_shape, num_QNode_args)
If no stacking and transposing occurs, the output shape instead is a
tuple
where each entry corresponds to one QNode argument and has the shape(*output_shape, *QNode_arg_shape)
.This breaking change alters the behaviour in the first case and removes the attempt to stack and transpose, so that the output always has the shape of the second type.
Note that the behaviour is unchanged — that is, the Jacobian tuple is unpacked into a single Jacobian — if
argnum=None
and there is only one QNode argument with respect to which the differentiation takes place, or if an integer is provided asargnum
.A workaround that allowed
qml.jacobian
to differentiate multiple QNode arguments will no longer support higher-order derivatives. In such cases, combining multiple arguments into a single array is recommended.qml.metric_tensor
,qml.adjoint_metric_tensor
andqml.transforms.classical_jacobian
now follow a different convention regarding their output shape when being used with the Autograd interface (#2059)See the previous entry for details. This breaking change immediately follows from the change in
qml.jacobian
wheneverhybrid=True
is used in the above methods.The behaviour of
RotosolveOptimizer
has been changed regarding its keyword arguments. (#2081)The keyword arguments
optimizer
andoptimizer_kwargs
for theRotosolveOptimizer
have been renamed tosubstep_optimizer
andsubstep_kwargs
, respectively. Furthermore they have been moved fromstep
andstep_and_cost
to the initialization__init__
.The keyword argument
num_freqs
has been renamed tonums_frequency
and is expected to take a different shape now: Previously, it was expected to be anint
or a list of entries, with each entry in turn being either anint
or alist
ofint
entries. Now the expected structure is a nested dictionary, matching the formatting expected by qml.fourier.reconstruct This also matches the expected formatting of the new keyword argumentsspectra
andshifts
.For more details, see the RotosolveOptimizer documentation.
Deprecations
Deprecates the caching ability provided by
QubitDevice
. (#2154)Going forward, the preferred way is to use the caching abilities of the QNode:
dev = qml.device("default.qubit", wires=2) cache = {} @qml.qnode(dev, diff_method='parameter-shift', cache=cache) def circuit(): qml.RY(0.345, wires=0) return qml.expval(qml.PauliZ(0))
>>> for _ in range(10): ... circuit() >>> dev.num_executions 1
Bug fixes
Fixes a bug where an incorrect number of executions are recorded by a QNode using a custom cache with
diff_method="backprop"
. (#2171)Fixes a bug where the
default.qubit.jax
device can’t be used withdiff_method=None
and jitting. (#2136)Fixes a bug where the Torch interface was not properly unwrapping Torch tensors to NumPy arrays before executing gradient tapes on devices. (#2117)
Fixes a bug for the TensorFlow interface where the dtype of input tensors was not cast. (#2120)
Fixes a bug where batch transformed QNodes would fail to apply batch transforms provided by the underlying device. (#2111)
An error is now raised during QNode creation if backpropagation is requested on a device with finite shots specified. (#2114)
Pytest now ignores any
DeprecationWarning
raised within autograd’snumpy_wrapper
module. Other assorted minor test warnings are fixed. (#2007)Fixes a bug where the QNode was not correctly diagonalizing qubit-wise commuting observables. (#2097)
Fixes a bug in
gradient_transform
where the hybrid differentiation of circuits with a single parametrized gate failed and QNode argument axes of size1
where removed from the output gradient. (#2080)The available
diff_method
options for QNodes has been corrected in both the error messages and the documentation. (#2078)Fixes a bug in
DefaultQubit
where the second derivative of QNodes at positions corresponding to vanishing state vector amplitudes is wrong. (#2057)Fixes a bug where PennyLane didn’t require v0.20.0 of PennyLane-Lightning, but raised an error with versions of Lightning earlier than v0.20.0 due to the new batch execution pipeline. (#2033)
Fixes a bug in
classical_jacobian
when used with Torch, where the Jacobian of the preprocessing was also computed for non-trainable parameters. (#2020)Fixes a bug in queueing of the
two_qubit_decomposition
method that originally led to circuits with >3 two-qubit unitaries failing when passed through theunitary_to_rot
optimization transform. (#2015)Fixes a bug which allows using
jax.jit
to be compatible with circuits which returnqml.probs
when thedefault.qubit.jax
is provided with a custom shot vector. (#2028)Updated the
adjoint()
method for non-parametric qubit operations to solve a bug where repeatedadjoint()
calls don’t return the correct operator. (#2133)Fixed a bug in
insert()
which prevented operations that inherited from multiple classes to be inserted. (#2172)
Documentation
Fixes an error in the signs of equations in the
DoubleExcitation
page. (#2072)Extends the interfaces description page to explicitly mention device compatibility. (#2031)
Contributors
This release contains contributions from (in alphabetical order):
Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Esther Cruz, Olivia Di Matteo, Christian Gogolin, Diego Guala, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Nathan Killoran, Korbinian Kottmann, Christina Lee, Romain Moyard, Lee James O’Riordan, Maria Schuld, Jay Soni, Antal Száva, David Wierichs, Shaoming Zhang.
- orphan
Release 0.20.0¶
New features since last release
Shiny new circuit drawer!🎨🖌️
PennyLane now supports drawing a QNode with matplotlib! (#1803) (#1811) (#1931) (#1954)
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()
New and improved quantum-aware optimizers
Added
qml.LieAlgebraOptimizer
, a new quantum-aware Lie Algebra optimizer that allows one to perform gradient descent on the special unitary group. (#1911)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)
For more details, see the LieAlgebraOptimizer documentation.
The
qml.metric_tensor
transform can now be used to compute the full tensor, beyond the block diagonal approximation. (#1725)This is performed using Hadamard tests, and requires an additional wire on the device to execute the circuits produced by the transform, as compared to the number of wires required by the original circuit. The transform defaults to computing the full tensor, which can be controlled by the
approx
keyword argument.As an example, consider the QNode
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)
Then we can compute the (block) diagonal metric tensor as before, now using the
approx="block-diag"
keyword:>>> qml.metric_tensor(circuit, approx="block-diag")(weights) [[0.25 0. 0. ] [0. 0.24013262 0. ] [0. 0. 0.21846983]]
Instead, we now can also compute the full metric tensor, using Hadamard tests on the additional wire of the device:
>>> qml.metric_tensor(circuit)(weights) [[ 0.25 0. -0.23300977] [ 0. 0.24013262 0.01763859] [-0.23300977 0.01763859 0.21846983]]
See the metric tensor documentation. for more information and usage details.
Faster performance with optimized quantum workflows
The QNode has been re-written to support batch execution across the board, custom gradients, better decomposition strategies, and higher-order derivatives. (#1807) (#1969)
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.
Custom gradient transforms can be specified as the differentiation method:
@qml.gradients.gradient_transform def my_gradient_transform(tape): ... return tapes, processing_fn @qml.qnode(dev, diff_method=my_gradient_transform) def circuit():
For breaking changes related to the use of the new QNode, refer to the Breaking Changes section.
Note that the old QNode remains accessible at
@qml.qnode_old.qnode
, however this will be removed in the next release.Custom decompositions can now be applied to operations at the device level. (#1900)
For example, suppose we would like to implement the following QNode:
def circuit(weights): qml.BasicEntanglerLayers(weights, wires=[0, 1, 2]) return qml.expval(qml.PauliZ(0)) original_dev = qml.device("default.qubit", wires=3) original_qnode = qml.QNode(circuit, original_dev)
>>> weights = np.array([[0.4, 0.5, 0.6]]) >>> print(qml.draw(original_qnode, expansion_strategy="device")(weights)) 0: ──RX(0.4)──╭C──────╭X──┤ ⟨Z⟩ 1: ──RX(0.5)──╰X──╭C──│───┤ 2: ──RX(0.6)──────╰X──╰C──┤
Now, let’s swap out the decomposition of the
CNOT
gate intoCZ
andHadamard
, and furthermore the decomposition ofHadamard
intoRZ
andRY
rather than the decomposition already available in PennyLane. We define the two decompositions like so, and pass them to a device:def custom_cnot(wires): return [ qml.Hadamard(wires=wires[1]), qml.CZ(wires=[wires[0], wires[1]]), qml.Hadamard(wires=wires[1]) ] def custom_hadamard(wires): return [ qml.RZ(np.pi, wires=wires), qml.RY(np.pi / 2, wires=wires) ] # Can pass the operation itself, or a string custom_decomps = {qml.CNOT : custom_cnot, "Hadamard" : custom_hadamard} decomp_dev = qml.device("default.qubit", wires=3, custom_decomps=custom_decomps) decomp_qnode = qml.QNode(circuit, decomp_dev)
Now when we draw or run a QNode on this device, the gates will be expanded according to our specifications:
>>> print(qml.draw(decomp_qnode, 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,
set_decomposition
, has also been implemented to enable application of custom decompositions on devices that have already been created.>>> with qml.transforms.set_decomposition(custom_decomps, original_dev): ... print(qml.draw(original_qnode, 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──────────────────────┤
Given an operator of the form \(U=e^{iHt}\), where \(H\) has commuting terms and known eigenvalues,
qml.gradients.generate_shift_rule
computes the generalized parameter shift rules for determining the gradient of the expectation value \(f(t) = \langle 0|U(t)^\dagger \hat{O} U(t)|0\rangle\) on hardware. (#1788) (#1932)Given
\[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
.qml.gradients.generate_shift_rule
can then be used to compute the parameter shift rules to compute \(f'(t)\) using2R
shifted cost function evaluations. This becomes cheaper than the standard application of the chain rule and two-term shift rule whenR
is less than the number of Pauli words in the generator.For example, consider the case where \(H\) has eigenspectrum
(-1, 0, 1)
:>>> frequencies = qml.gradients.eigvals_to_frequencies((-1, 0, 1)) >>> frequencies (1, 2) >>> coeffs, shifts = qml.gradients.generate_shift_rule(frequencies) >>> coeffs array([ 0.85355339, -0.85355339, -0.14644661, 0.14644661]) >>> shifts array([ 0.78539816, -0.78539816, 2.35619449, -2.35619449])
As we can see,
generate_shift_rule
returns four coefficients \(c_i\) and shifts \(s_i\) corresponding to a four term parameter shift rule. The gradient can then be reconstructed via:\[\frac{\partial}{\partial\phi}f = \sum_{i} c_i f(\phi + s_i),\]where \(f(\phi) = \langle 0|U(\phi)^\dagger \hat{O} U(\phi)|0\rangle\) for some observable \(\hat{O}\) and the unitary \(U(\phi)=e^{iH\phi}\).
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 support
diff_method="backprop"
. This will result in significantly more performant model execution, at the cost of a more expensive initial compilation. (#1866)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.
Characterize your quantum models with classical QNode reconstruction
The
qml.fourier.reconstruct
function is added. It can be used to reconstruct QNodes outputting expectation values along a specified parameter dimension, with a minimal number of calls to the original QNode. The returned reconstruction is exact and purely classical, and can be evaluated without any quantum executions. (#1864)The reconstruction technique differs for functions with equidistant frequencies that are reconstructed using the function value at equidistant sampling points, and for functions with arbitrary frequencies reconstructed using arbitrary sampling points.
As an example, consider the following QNode:
dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x, Y, f=1.0): qml.RX(f * x, wires=0) qml.RY(Y[0], wires=0) qml.RY(Y[1], wires=1) qml.CNOT(wires=[0, 1]) qml.RY(3 * Y[1], wires=1) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
It has three variational parameters overall: A scalar input
x
and an array-valued inputY
with two entries. Additionally, we can tune the dependence onx
with the frequencyf
. We then can reconstruct the QNode output function with respect tox
via>>> x = 0.3 >>> Y = np.array([0.1, -0.9]) >>> rec = qml.fourier.reconstruct(circuit, ids="x", nums_frequency={"x": {0: 1}})(x, Y) >>> rec {'x': {0: <function pennylane.fourier.reconstruct._reconstruct_equ.<locals>._reconstruction(x)>}}
As we can see, we get a nested dictionary in the format of the input
nums_frequency
with functions as values. These functions are simple float-to-float callables:>>> univariate = rec["x"][0] >>> univariate(x) -0.880208251507
For more details on usage, reconstruction cost and differentiability support, please see the fourier.reconstruct docstring.
State-of-the-art operations and templates
A circuit template for time evolution under a commuting Hamiltonian utilizing generalized parameter shift rules for cost function gradients is available as
qml.CommutingEvolution
. (#1788)If the template is handed a frequency spectrum during its instantiation, then
generate_shift_rule
is internally called to obtain the general parameter shift rules with respect toCommutingEvolution
‘s \(t\) parameter, otherwise the shift rule for a decomposition ofCommutingEvolution
will be used.The template can be initialized within QNode as:
import pennylane as qml n_wires = 2 dev = qml.device('default.qubit', wires=n_wires) coeffs = [1, -1] obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)] hamiltonian = qml.Hamiltonian(coeffs, obs) frequencies = (2,4) @qml.qnode(dev) def circuit(time): qml.PauliX(0) qml.CommutingEvolution(hamiltonian, time, frequencies) return qml.expval(qml.PauliZ(0))
Note that there is no internal validation that 1) the input
qml.Hamiltonian
is fully commuting and 2) the eigenvalue frequency spectrum is correct, since these checks become prohibitively expensive for large Hamiltonians.The
qml.Barrier()
operator has been added. With it we can separate blocks in compilation or use it as a visual tool. (#1844)Added the identity observable to be an operator. Now we can explicitly call the identity operation on our quantum circuits for both qubit and CV devices. (#1829)
Added the
qml.QubitDensityMatrix
initialization gate for mixed state simulation. (#1850)A thermal relaxation channel is added to the Noisy channels. The channel description can be found on the supplementary information of Quantum classifier with tailored quantum kernels. (#1766)
Added a new
qml.PauliError
channel that allows the application of an arbitrary number of Pauli operators on an arbitrary number of wires. (#1781)
Manipulate QNodes to your ❤️s content with new transforms
The
merge_amplitude_embedding
transformation has been created to automatically merge all gates of this type into one. (#1933)from pennylane.transforms import merge_amplitude_embedding dev = qml.device("default.qubit", wires = 3) @qml.qnode(dev) @merge_amplitude_embedding def qfunc(): qml.AmplitudeEmbedding([0,1,0,0], wires = [0,1]) qml.AmplitudeEmbedding([0,1], wires = 2) return qml.expval(qml.PauliZ(wires = 0))
>>> print(qml.draw(qnode)()) 0: ──╭AmplitudeEmbedding(M0)──┤ ⟨Z⟩ 1: ──├AmplitudeEmbedding(M0)──┤ 2: ──╰AmplitudeEmbedding(M0)──┤ M0 = [0.+0.j 0.+0.j 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
The
undo_swaps
transformation has been created to automatically remove all swaps of a circuit. (#1960)dev = qml.device('default.qubit', wires=3) @qml.qnode(dev) @qml.transforms.undo_swaps def qfunc(): qml.Hadamard(wires=0) qml.PauliX(wires=1) qml.SWAP(wires=[0,1]) qml.SWAP(wires=[0,2]) qml.PauliY(wires=0) return qml.expval(qml.PauliZ(0))
>>> print(qml.draw(qfunc)()) 0: ──Y──┤ ⟨Z⟩ 1: ──H──┤ 2: ──X──┤
Improvements
Added functions for computing the values of atomic and molecular orbitals at a given position. (#1867)
The functions
atomic_orbital
andmolecular_orbital
can be used, as shown in the following codeblock, to evaluate the orbitals. By generating values of the orbitals at different positions, one can plot the spatial shape of a desired orbital.symbols = ['H', 'H'] geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], requires_grad = False) mol = hf.Molecule(symbols, geometry) hf.generate_scf(mol)() ao = mol.atomic_orbital(0) mo = mol.molecular_orbital(1)
>>> print(ao(0.0, 0.0, 0.0)) >>> print(mo(0.0, 0.0, 0.0)) 0.6282468778183719 0.018251285973461928
Added support for Python 3.10. (#1964)
The execution of QNodes that have
multiple return types;
a return type other than Variance and Expectation
now raises a descriptive error message when using the JAX interface. (#2011)
The PennyLane
qchem
package is now lazily imported; it will only be imported the first time it is accessed. (#1962)qml.math.scatter_element_add
now supports adding multiple values at multiple indices with a single function call, in all interfaces (#1864)For example, we may set five values of a three-dimensional tensor in the following way:
>>> X = tf.zeros((3, 2, 9), dtype=tf.float64) >>> indices = [(0, 0, 1, 2, 2), (0, 0, 0, 0, 1), (1, 3, 8, 6, 7)] >>> values = [1 * i for i in range(1,6)] >>> qml.math.scatter_element_add(X, indices, values) <tf.Tensor: shape=(3, 2, 9), dtype=float64, numpy= array([[[0., 1., 0., 2., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0.]], [[0., 0., 0., 0., 0., 0., 0., 0., 3.], [0., 0., 0., 0., 0., 0., 0., 0., 0.]], [[0., 0., 0., 0., 0., 0., 4., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 5., 0.]]])>
All instances of
str.format
have been replace with f-strings. (#1970)Tests do not loop over automatically imported and instantiated operations any more, which was opaque and created unnecessarily many tests. (#1895)
A
decompose()
method has been added to theOperator
class such that we can obtain (and queue) decompositions directly from instances of operations. (#1873)>>> op = qml.PhaseShift(0.3, wires=0) >>> op.decompose() [RZ(0.3, wires=[0])]
qml.circuit_drawer.tape_mpl
produces a matplotlib figure and axes given a tape. (#1787)The
AngleEmbedding
,BasicEntanglerLayers
andMottonenStatePreparation
templates now support parameters with batch dimension when using the@qml.batch_params
decorator. (#1812) (#1883) (#1893)qml.draw
now supports amax_length
argument to help prevent text overflows when printing circuits. (#1892)Identity
operation is now part of both theops.qubit
andops.cv
modules. (#1956)
Breaking changes
The QNode has been re-written to support batch execution across the board, custom gradients, better decomposition strategies, and higher-order derivatives. (#1807) (#1969)
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.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.
Note that the old QNode remains accessible at
@qml.qnode_old.qnode
, however this will be removed in the next release.Certain features deprecated in
v0.19.0
have been removed: (#1981) (#1963)The
qml.template
decorator (use a ` QuantumTape <https://pennylane.readthedocs.io/en/stable/code/api/pennylane.tape.QuantumTape.html>`_ as a context manager to record operations and itsoperations
attribute to return them, see the linked page for examples);The
default.tensor
anddefault.tensor.tf
experimental devices;The
qml.fourier.spectrum
function (use theqml.fourier.circuit_spectrum
orqml.fourier.qnode_spectrum
functions instead);The
diag_approx
keyword argument ofqml.metric_tensor
andqml.QNGOptimizer
(passapprox='diag'
instead).
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. (#1725)
The
circuit_drawer
module has been renameddrawer
. (#1949)The
par_domain
attribute in the operator class has been removed. (#1907)The
mutable
keyword argument has been removed from the QNode, due to underlying bugs that result in incorrect results being returned from immutable QNodes. This functionality will return in an upcoming release. (#1807)The reversible QNode differentiation method has been removed; the adjoint differentiation method is preferred instead (
diff_method='adjoint'
). (#1807)QuantumTape.trainable_params
now is a list instead of a set. This means thattape.trainable_params
will return a list unlike before, but setting thetrainable_params
with a set works exactly as before. (#1904)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. (#1898) (#1909)The static method
decomposition()
, formerly in theOperation
class, has been moved to the baseOperator
class. (#1873)DiagonalOperation
is not a separate subclass any more. (#1889)Instead, devices can check for the diagonal property using attributes:
from pennylane.ops.qubit.attributes import diagonal_in_z_basis if op in diagonal_in_z_basis: # do something
Custom operations can be added to this attribute at runtime via
diagonal_in_z_basis.add("MyCustomOp")
.
Bug fixes
Fixes a bug with
qml.probs
when usingdefault.qubit.jax
. (#1998)Fixes a bug where output tensors of a QNode would always be put on the default GPU with
default.qubit.torch
. (#1982)Device test suite doesn’t use empty circuits so that it can also test the IonQ plugin, and it checks if operations are supported in more places. (#1979)
Fixes a bug where the metric tensor was computed incorrectly when using gates with
gate.inverse=True
. (#1987)Corrects the documentation of
qml.transforms.classical_jacobian
for the Autograd interface (and improves test coverage). (#1978)Fixes a bug where differentiating a QNode with
qml.state
using the JAX interface raised an error. (#1906)Fixes a bug with the adjoint of
qml.QFT
. (#1955)Fixes a bug where the
ApproxTimeEvolution
template was not correctly computing the operation wires from the input Hamiltonian. This did not affect computation with theApproxTimeEvolution
template, but did cause circuit drawing to fail. (#1952)Fixes a bug where the classical preprocessing Jacobian computed by
qml.transforms.classical_jacobian
with JAX returned a reduced submatrix of the Jacobian. (#1948)Fixes a bug where the operations are not accessed in the correct order in
qml.fourier.qnode_spectrum
, leading to wrong outputs. (#1935)Fixes several Pylint errors. (#1951)
Fixes a bug where the device test suite wasn’t testing certain operations. (#1943)
Fixes a bug where batch transforms would mutate a QNodes execution options. (#1934)
qml.draw
now supports arbitrary templates with matrix parameters. (#1917)QuantumTape.trainable_params
now is a list instead of a set, making it more stable in very rare edge cases. (#1904)ExpvalCost
now returns corrects results shape whenoptimize=True
with shots batch. (#1897)qml.circuit_drawer.MPLDrawer
was slightly modified to work with matplotlib version 3.5. (#1899)qml.CSWAP
andqml.CRot
now definecontrol_wires
, andqml.SWAP
returns the default empty wires object. (#1830)The
requires_grad
attribute ofqml.numpy.tensor
objects is now preserved when pickling/unpickling the object. (#1856)Device tests no longer throw warnings about the
requires_grad
attribute of variational parameters. (#1913)AdamOptimizer
andAdagradOptimizer
had small fixes to their optimization step updates. (#1929)Fixes a bug where differentiating a QNode with multiple array arguments via
qml.gradients.param_shift
throws an error. (#1989)AmplitudeEmbedding
template no longer produces aComplexWarning
when thefeatures
parameter is batched and provided as a 2D array. (#1990)qml.circuit_drawer.CircuitDrawer
no longer produces an error when attempting to draw tapes inside of circuits (e.g. from decomposition of an operation or manual placement). (#1994)Fixes a bug where using SciPy sparse matrices with the new QNode could lead to a warning being raised about prioritizing the TensorFlow and PyTorch interfaces. (#2001)
Fixed a bug where the
QueueContext
was not empty when first importing PennyLane. (#1957)Fixed circuit drawing problem with
Interferometer
andCVNeuralNet
. (#1953)
Documentation
Added examples in documentation for some operations. (#1902)
Improves the Developer’s Guide Testing document. (#1896)
Added documentation examples for
AngleEmbedding
,BasisEmbedding
,StronglyEntanglingLayers
,SqueezingEmbedding
,DisplacementEmbedding
,MottonenStatePreparation
andInterferometer
. (#1910) (#1908) (#1912) (#1920) (#1936) (#1937)
Contributors
This release contains contributions from (in alphabetical order):
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.
- orphan
Release 0.19.1¶
Bug fixes
Fixes several bugs when using parametric operations with the
default.qubit.tensor
device on GPU. The device takes thetorch_device
argument once again to allow running non-parametric QNodes on the GPU. (#1927)Fixes a bug where using JAX’s jit function on certain QNodes that contain the
qml.QubitStateVector
operation raised an error with earlier JAX versions (e.g.,jax==0.2.10
andjaxlib==0.1.64
). (#1924)
Contributors
This release contains contributions from (in alphabetical order):
Josh Izaac, Christina Lee, Romain Moyard, Lee James O’Riordan, Antal Száva.
- orphan
Release 0.19.0¶
New features since last release
Differentiable Hartree-Fock solver
A differentiable Hartree-Fock (HF) solver has been added. It can be used to construct molecular Hamiltonians that can be differentiated with respect to nuclear coordinates and basis-set parameters. (#1610)
The HF solver computes the integrals over basis functions, constructs the relevant matrices, and performs self-consistent-field iterations to obtain a set of optimized molecular orbital coefficients. These coefficients and the computed integrals over basis functions are used to construct the one- and two-body electron integrals in the molecular orbital basis which can be used to generate a differentiable second-quantized Hamiltonian in the fermionic and qubit basis.
The following code shows the construction of the Hamiltonian for the hydrogen molecule where the geometry of the molecule is differentiable.
symbols = ["H", "H"] geometry = np.array([[0.0000000000, 0.0000000000, -0.6943528941], [0.0000000000, 0.0000000000, 0.6943528941]], requires_grad=True) mol = qml.hf.Molecule(symbols, geometry) args_mol = [geometry] hamiltonian = qml.hf.generate_hamiltonian(mol)(*args_mol)
>>> hamiltonian.coeffs tensor([-0.09041082+0.j, 0.17220382+0.j, 0.17220382+0.j, 0.16893367+0.j, 0.04523101+0.j, -0.04523101+0.j, -0.04523101+0.j, 0.04523101+0.j, -0.22581352+0.j, 0.12092003+0.j, -0.22581352+0.j, 0.16615103+0.j, 0.16615103+0.j, 0.12092003+0.j, 0.17464937+0.j], requires_grad=True)
The generated Hamiltonian can be used in a circuit where the atomic coordinates and circuit parameters are optimized simultaneously.
symbols = ["H", "H"] geometry = np.array([[0.0000000000, 0.0000000000, 0.0], [0.0000000000, 0.0000000000, 2.0]], requires_grad=True) mol = qml.hf.Molecule(symbols, geometry) dev = qml.device("default.qubit", wires=4) params = [np.array([0.0], requires_grad=True)] def generate_circuit(mol): @qml.qnode(dev) def circuit(*args): qml.BasisState(np.array([1, 1, 0, 0]), wires=[0, 1, 2, 3]) qml.DoubleExcitation(*args[0][0], wires=[0, 1, 2, 3]) return qml.expval(qml.hf.generate_hamiltonian(mol)(*args[1:])) return circuit for n in range(25): mol = qml.hf.Molecule(symbols, geometry) args = [params, geometry] # initial values of the differentiable parameters g_params = qml.grad(generate_circuit(mol), argnum = 0)(*args) params = params - 0.5 * g_params[0] forces = qml.grad(generate_circuit(mol), argnum = 1)(*args) geometry = geometry - 0.5 * forces print(f'Step: {n}, Energy: {generate_circuit(mol)(*args)}, Maximum Force: {forces.max()}')
In addition, the new Hartree-Fock solver can further be used to optimize the basis set parameters. For details, please refer to the differentiable Hartree-Fock solver documentation.
Integration with Mitiq
Error mitigation using the zero-noise extrapolation method is now available through the
transforms.mitigate_with_zne
transform. This transform can integrate with the Mitiq package for unitary folding and extrapolation functionality. (#1813)Consider the following noisy device:
noise_strength = 0.05 dev = qml.device("default.mixed", wires=2) dev = qml.transforms.insert(qml.AmplitudeDamping, noise_strength)(dev)
We can mitigate the effects of this noise for circuits run on this device by using the added transform:
from mitiq.zne.scaling import fold_global from mitiq.zne.inference import RichardsonFactory n_wires = 2 n_layers = 2 shapes = qml.SimplifiedTwoDesign.shape(n_wires, n_layers) np.random.seed(0) w1, w2 = [np.random.random(s) for s in shapes] @qml.transforms.mitigate_with_zne([1, 2, 3], fold_global, RichardsonFactory.extrapolate) @qml.beta.qnode(dev) def circuit(w1, w2): qml.SimplifiedTwoDesign(w1, w2, wires=range(2)) return qml.expval(qml.PauliZ(0))
Now, when we execute
circuit
, errors will be automatically mitigated:>>> circuit(w1, w2) 0.19113067083636542
Powerful new transforms
The unitary matrix corresponding to a quantum circuit can now be generated using the new
get_unitary_matrix()
transform. (#1609) (#1786)This transform is fully differentiable across all supported PennyLane autodiff frameworks.
def circuit(theta): qml.RX(theta, wires=1) qml.PauliZ(wires=0) qml.CNOT(wires=[0, 1])
>>> theta = torch.tensor(0.3, requires_grad=True) >>> matrix = qml.transforms.get_unitary_matrix(circuit)(theta) >>> print(matrix) tensor([[ 0.9888+0.0000j, 0.0000+0.0000j, 0.0000-0.1494j, 0.0000+0.0000j], [ 0.0000+0.0000j, 0.0000+0.1494j, 0.0000+0.0000j, -0.9888+0.0000j], [ 0.0000-0.1494j, 0.0000+0.0000j, 0.9888+0.0000j, 0.0000+0.0000j], [ 0.0000+0.0000j, -0.9888+0.0000j, 0.0000+0.0000j, 0.0000+0.1494j]], grad_fn=<MmBackward>) >>> loss = torch.real(torch.trace(matrix)) >>> loss.backward() >>> theta.grad tensor(-0.1494)
Arbitrary two-qubit unitaries can now be decomposed into elementary gates. This functionality has been incorporated into the
qml.transforms.unitary_to_rot
transform, and is available separately asqml.transforms.two_qubit_decomposition
. (#1552)As an example, consider the following randomly-generated matrix and circuit that uses it:
U = np.array([ [-0.03053706-0.03662692j, 0.01313778+0.38162226j, 0.4101526 -0.81893687j, -0.03864617+0.10743148j], [-0.17171136-0.24851809j, 0.06046239+0.1929145j, -0.04813084-0.01748555j, -0.29544883-0.88202604j], [ 0.39634931-0.78959795j, -0.25521689-0.17045233j, -0.1391033 -0.09670952j, -0.25043606+0.18393466j], [ 0.29599198-0.19573188j, 0.55605806+0.64025769j, 0.06140516+0.35499559j, 0.02674726+0.1563311j ] ]) dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) @qml.transforms.unitary_to_rot def circuit(x, y): qml.QubitUnitary(U, wires=[0, 1]) return qml.expval(qml.PauliZ(wires=0))
If we run the circuit, we can see the new decomposition:
>>> circuit(0.3, 0.4) tensor(-0.81295986, requires_grad=True) >>> print(qml.draw(circuit)(0.3, 0.4)) 0: ──Rot(2.78, 0.242, -2.28)──╭X──RZ(0.176)───╭C─────────────╭X──Rot(-3.87, 0.321, -2.09)──┤ ⟨Z⟩ 1: ──Rot(4.64, 2.69, -1.56)───╰C──RY(-0.883)──╰X──RY(-1.47)──╰C──Rot(1.68, 0.337, 0.587)───┤
A new transform,
@qml.batch_params
, has been added, that makes QNodes handle a batch dimension in trainable parameters. (#1710) (#1761)This transform will create multiple circuits, one per batch dimension. As a result, it is both simulator and hardware compatible.
@qml.batch_params @qml.beta.qnode(dev) def circuit(x, weights): qml.RX(x, wires=0) qml.RY(0.2, wires=1) qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1, 2]) return qml.expval(qml.Hadamard(0))
The
qml.batch_params
decorator allows us to pass argumentsx
andweights
that have a batch dimension. For example,>>> batch_size = 3 >>> x = np.linspace(0.1, 0.5, batch_size) >>> weights = np.random.random((batch_size, 10, 3, 3))
If we evaluate the QNode with these inputs, we will get an output of shape
(batch_size,)
:>>> circuit(x, weights) tensor([0.08569816, 0.12619101, 0.21122004], requires_grad=True)
The
insert
transform has now been added, providing a way to insert single-qubit operations into a quantum circuit. The transform can apply to quantum functions, tapes, and devices. (#1795)The following QNode can be transformed to add noise to the circuit:
dev = qml.device("default.mixed", wires=2) @qml.qnode(dev) @qml.transforms.insert(qml.AmplitudeDamping, 0.2, position="end") def f(w, x, y, z): qml.RX(w, wires=0) qml.RY(x, wires=1) qml.CNOT(wires=[0, 1]) qml.RY(y, wires=0) qml.RX(z, wires=1) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
Executions of this circuit will differ from the noise-free value:
>>> f(0.9, 0.4, 0.5, 0.6) tensor(0.754847, requires_grad=True) >>> print(qml.draw(f)(0.9, 0.4, 0.5, 0.6)) 0: ──RX(0.9)──╭C──RY(0.5)──AmplitudeDamping(0.2)──╭┤ ⟨Z ⊗ Z⟩ 1: ──RY(0.4)──╰X──RX(0.6)──AmplitudeDamping(0.2)──╰┤ ⟨Z ⊗ Z⟩
Common tape expansion functions are now available in
qml.transforms
, alongside a newcreate_expand_fn
function for easily creating expansion functions from stopping criteria. (#1734) (#1760)create_expand_fn
takes the default depth to which the expansion function should expand a tape, a stopping criterion, an optional device, and a docstring to be set for the created function. The stopping criterion must take a queuable object and return a boolean.For example, to create an expansion function that decomposes all trainable, multi-parameter operations:
>>> stop_at = ~(qml.operation.has_multipar & qml.operation.is_trainable) >>> expand_fn = qml.transforms.create_expand_fn(depth=5, stop_at=stop_at)
The created expansion function can be used within a custom transform. Devices can also be provided, producing expansion functions that decompose tapes to support the native gate set of the device.
Batch execution of circuits
A new, experimental QNode has been added, that adds support for batch execution of circuits, custom quantum gradient support, and arbitrary order derivatives. This QNode is available via
qml.beta.QNode
, and@qml.beta.qnode
. (#1642) (#1646) (#1651) (#1804)It differs from the standard QNode in several ways:
Custom gradient transforms can be specified as the differentiation method:
@qml.gradients.gradient_transform def my_gradient_transform(tape): ... return tapes, processing_fn @qml.beta.qnode(dev, diff_method=my_gradient_transform) def circuit():
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).Internally, if multiple circuits are generated for execution simultaneously, 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.
When decomposing the circuit, the default decomposition strategy will prioritize decompositions that result in the smallest number of parametrized operations required to satisfy the differentiation method. Additional decompositions required to satisfy the native gate set of the quantum device will be performed later, by the device at execution time. While this may lead to a slight increase in classical processing, it significantly reduces the number of circuit evaluations needed to compute gradients of complex unitaries.
In an upcoming release, this QNode will replace the existing one. If you come across any bugs while using this QNode, please let us know via a bug report on our GitHub bug tracker.
Currently, this beta QNode does not support the following features:
Non-mutability via the
mutable
keyword argumentThe
reversible
QNode differentiation methodThe ability to specify a
dtype
when using PyTorch and TensorFlow.
It is also not tested with the
qml.qnn
module.
New operations and templates
Added a new operation
OrbitalRotation
, which implements the spin-adapted spatial orbital rotation gate. (#1665)An example circuit that uses
OrbitalRotation
operation is:dev = qml.device('default.qubit', wires=4) @qml.qnode(dev) def circuit(phi): qml.BasisState(np.array([1, 1, 0, 0]), wires=[0, 1, 2, 3]) qml.OrbitalRotation(phi, wires=[0, 1, 2, 3]) return qml.state()
If we run this circuit, we will get the following output
>>> circuit(0.1) array([ 0. +0.j, 0. +0.j, 0. +0.j, 0.00249792+0.j, 0. +0.j, 0. +0.j, -0.04991671+0.j, 0. +0.j, 0. +0.j, -0.04991671+0.j, 0. +0.j, 0. +0.j, 0.99750208+0.j, 0. +0.j, 0. +0.j, 0. +0.j])
Added a new template
GateFabric
, which implements a local, expressive, quantum-number-preserving ansatz proposed by Anselmetti et al. in arXiv:2104.05692. (#1687)An example of a circuit using
GateFabric
template is:coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614]) H, qubits = qml.qchem.molecular_hamiltonian(["H", "H"], coordinates) ref_state = qml.qchem.hf_state(electrons=2, orbitals=qubits) dev = qml.device('default.qubit', wires=qubits) @qml.qnode(dev) def ansatz(weights): qml.templates.GateFabric(weights, wires=[0,1,2,3], init_state=ref_state, include_pi=True) return qml.expval(H)
For more details, see the GateFabric documentation.
Added a new template
kUpCCGSD
, which implements a unitary coupled cluster ansatz with generalized singles and pair doubles excitation operators, proposed by Joonho Lee et al. in arXiv:1810.02327. (#1743)An example of a circuit using
kUpCCGSD
template is:coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614]) H, qubits = qml.qchem.molecular_hamiltonian(["H", "H"], coordinates) ref_state = qml.qchem.hf_state(electrons=2, orbitals=qubits) dev = qml.device('default.qubit', wires=qubits) @qml.qnode(dev) def ansatz(weights): qml.templates.kUpCCGSD(weights, wires=[0,1,2,3], k=0, delta_sz=0, init_state=ref_state) return qml.expval(H)
Improved utilities for quantum compilation and characterization
The new
qml.fourier.qnode_spectrum
function extends the formerqml.fourier.spectrum
function and takes classical processing of QNode arguments into account. The frequencies are computed per (requested) QNode argument instead of per gateid
. The gateid
s are ignored. (#1681) (#1720)Consider the following example, which uses non-trainable inputs
x
,y
andz
as well as trainable parametersw
as arguments to the QNode.import pennylane as qml import numpy as np n_qubits = 3 dev = qml.device("default.qubit", wires=n_qubits) @qml.qnode(dev) def circuit(x, y, z, w): for i in range(n_qubits): qml.RX(0.5*x[i], wires=i) qml.Rot(w[0,i,0], w[0,i,1], w[0,i,2], wires=i) qml.RY(2.3*y[i], wires=i) qml.Rot(w[1,i,0], w[1,i,1], w[1,i,2], wires=i) qml.RX(z, wires=i) return qml.expval(qml.PauliZ(wires=0)) x = np.array([1., 2., 3.]) y = np.array([0.1, 0.3, 0.5]) z = -1.8 w = np.random.random((2, n_qubits, 3))
This circuit looks as follows:
>>> print(qml.draw(circuit)(x, y, z, w)) 0: ──RX(0.5)──Rot(0.598, 0.949, 0.346)───RY(0.23)──Rot(0.693, 0.0738, 0.246)──RX(-1.8)──┤ ⟨Z⟩ 1: ──RX(1)────Rot(0.0711, 0.701, 0.445)──RY(0.69)──Rot(0.32, 0.0482, 0.437)───RX(-1.8)──┤ 2: ──RX(1.5)──Rot(0.401, 0.0795, 0.731)──RY(1.15)──Rot(0.756, 0.38, 0.38)─────RX(-1.8)──┤
Applying the
qml.fourier.qnode_spectrum
function to the circuit for the non-trainable parameters, we obtain:>>> spec = qml.fourier.qnode_spectrum(circuit, encoding_args={"x", "y", "z"})(x, y, z, w) >>> for inp, freqs in spec.items(): ... print(f"{inp}: {freqs}") "x": {(0,): [-0.5, 0.0, 0.5], (1,): [-0.5, 0.0, 0.5], (2,): [-0.5, 0.0, 0.5]} "y": {(0,): [-2.3, 0.0, 2.3], (1,): [-2.3, 0.0, 2.3], (2,): [-2.3, 0.0, 2.3]} "z": {(): [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]}
We can see that all three parameters in the QNode arguments
x
andy
contribute the spectrum of a Pauli rotation[-1.0, 0.0, 1.0]
, rescaled with the prefactor of the respective parameter in the circuit. The threeRX
rotations using the parameterz
accumulate, yielding a more complex frequency spectrum.For details on how to control for which parameters the spectrum is computed, a comparison to
qml.fourier.circuit_spectrum
, and other usage details, please see the fourier.qnode_spectrum docstring.Two new methods were added to the Device API, allowing PennyLane devices increased control over circuit decompositions. (#1651)
Device.expand_fn(tape) -> tape
: expands a tape such that it is supported by the device. By default, performs the standard device-specific gate set decomposition done in the default QNode. Devices may overwrite this method in order to define their own decomposition logic.Note that the numerical result after applying this method should remain unchanged; PennyLane will assume that the expanded tape returns exactly the same value as the original tape when executed.
Device.batch_transform(tape) -> (tapes, processing_fn)
: preprocesses the tape in the case where the device needs to generate multiple circuits to execute from the input circuit. The requirement of a post-processing function makes this distinct to theexpand_fn
method above.By default, this method applies the transform
\[\left\langle \sum_i c_i h_i\right\rangle → \sum_i c_i \left\langle h_i \right\rangle\]if
expval(H)
is present on devices that do not natively support Hamiltonians with non-commuting terms.
A new class has been added to store operator attributes, such as
self_inverses
, andcomposable_rotation
, as a list of operation names. (#1763)A number of such attributes, for the purpose of compilation transforms, can be found in
ops/qubit/attributes.py
, but the class can also be used to create your own. For example, we can create a new Attribute,pauli_ops
, like so:>>> from pennylane.ops.qubit.attributes import Attribute >>> pauli_ops = Attribute(["PauliX", "PauliY", "PauliZ"])
We can check either a string or an Operation for inclusion in this set:
>>> qml.PauliX(0) in pauli_ops True >>> "Hadamard" in pauli_ops False
We can also dynamically add operators to the sets at runtime. This is useful for adding custom operations to the attributes such as
composable_rotations
andself_inverses
that are used in compilation transforms. For example, suppose you have created a new Operation,MyGate
, which you know to be its own inverse. Adding it to the set, like so>>> from pennylane.ops.qubit.attributes import self_inverses >>> self_inverses.add("MyGate")
will enable the gate to be considered by the
cancel_inverses
compilation transform if two such gates are adjacent in a circuit.
Improvements
The
qml.metric_tensor
transform has been improved with regards to both function and performance. (#1638) (#1721)If the underlying device supports batch execution of circuits, the quantum circuits required to compute the metric tensor elements will be automatically submitted as a batched job. This can lead to significant performance improvements for devices with a non-trivial job submission overhead.
Previously, the transform would only return the metric tensor with respect to gate arguments, and ignore any classical processing inside the QNode, even very trivial classical processing such as parameter permutation. The metric tensor now takes into account classical processing, and returns the metric tensor with respect to QNode arguments, not simply gate arguments:
>>> @qml.qnode(dev) ... def circuit(x): ... qml.Hadamard(wires=1) ... qml.RX(x[0], wires=0) ... qml.CNOT(wires=[0, 1]) ... qml.RY(x[1] ** 2, wires=1) ... qml.RY(x[1], wires=0) ... return qml.expval(qml.PauliZ(0)) >>> x = np.array([0.1, 0.2], requires_grad=True) >>> qml.metric_tensor(circuit)(x) array([[0.25 , 0. ], [0. , 0.28750832]])
To revert to the previous behaviour of returning the metric tensor with respect to gate arguments,
qml.metric_tensor(qnode, hybrid=False)
can be passed.>>> qml.metric_tensor(circuit, hybrid=False)(x) array([[0.25 , 0. , 0. ], [0. , 0.25 , 0. ], [0. , 0. , 0.24750832]])
The metric tensor transform now works with a larger set of operations. In particular, all operations that have a single variational parameter and define a generator are now supported. In addition to a reduction in decomposition overhead, the change also results in fewer circuit evaluations.
The expansion rule in the
qml.metric_tensor
transform has been changed. (#1721)If
hybrid=False
, the changed expansion rule might lead to a changed output.The
ApproxTimeEvolution
template can now be used with Hamiltonians that have trainable coefficients. (#1789)Resulting QNodes can be differentiated with respect to both the time parameter and the Hamiltonian coefficients.
dev = qml.device('default.qubit', wires=2) obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)] @qml.qnode(dev) def circuit(coeffs, t): H = qml.Hamiltonian(coeffs, obs) qml.templates.ApproxTimeEvolution(H, t, 2) return qml.expval(qml.PauliZ(0))
>>> t = np.array(0.54, requires_grad=True) >>> coeffs = np.array([-0.6, 2.0], requires_grad=True) >>> qml.grad(circuit)(coeffs, t) (array([-1.07813375, -1.07813375]), array(-2.79516158))
All differentiation methods, including backpropagation and the parameter-shift rule, are supported.
Quantum function transforms and batch transforms can now be applied to devices. Once applied to a device, any quantum function executed on the modified device will be transformed prior to execution. (#1809) (#1810)
dev = qml.device("default.mixed", wires=1) dev = qml.transforms.merge_rotations()(dev) @qml.beta.qnode(dev) def f(w, x, y, z): qml.RX(w, wires=0) qml.RX(x, wires=0) qml.RX(y, wires=0) qml.RX(z, wires=0) return qml.expval(qml.PauliZ(0))
>>> print(f(0.9, 0.4, 0.5, 0.6)) -0.7373937155412453 >>> print(qml.draw(f, expansion_strategy="device")(0.9, 0.4, 0.5, 0.6)) 0: ──RX(2.4)──┤ ⟨Z⟩
It is now possible to draw QNodes that have been transformed by a ‘batch transform’; that is, a transform that maps a single QNode into multiple circuits under the hood. Examples of batch transforms include
@qml.metric_tensor
and@qml.gradients
. (#1762)For example, consider the parameter-shift rule, which generates two circuits per parameter; one circuit that has the parameter shifted forward, and another that has the parameter shifted backwards:
dev = qml.device("default.qubit", wires=2) @qml.gradients.param_shift @qml.beta.qnode(dev) def circuit(x): qml.RX(x, wires=0) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(wires=0))
>>> print(qml.draw(circuit)(0.6)) 0: ──RX(2.17)──╭C──┤ ⟨Z⟩ 1: ────────────╰X──┤ 0: ──RX(-0.971)──╭C──┤ ⟨Z⟩ 1: ──────────────╰X──┤
Support for differentiable execution of batches of circuits has been extended to the JAX interface for scalar functions, via the beta
pennylane.interfaces.batch
module. (#1634) (#1685)For example using the
execute
function from thepennylane.interfaces.batch
module:from pennylane.interfaces.batch import execute def cost_fn(x): with qml.tape.JacobianTape() as tape1: qml.RX(x[0], wires=[0]) qml.RY(x[1], wires=[1]) qml.CNOT(wires=[0, 1]) qml.var(qml.PauliZ(0) @ qml.PauliX(1)) with qml.tape.JacobianTape() as tape2: qml.RX(x[0], wires=0) qml.RY(x[0], wires=1) qml.CNOT(wires=[0, 1]) qml.probs(wires=1) result = execute( [tape1, tape2], dev, gradient_fn=qml.gradients.param_shift, interface="autograd" ) return (result[0] + result[1][0, 0])[0] res = jax.grad(cost_fn)(params)
All qubit operations have been re-written to use the
qml.math
framework for internal classical processing and the generation of their matrix representations. As a result these representations are now fully differentiable, and the framework-specific device classes no longer need to maintain framework-specific versions of these matrices. (#1749) (#1802)The use of
expval(H)
, whereH
is a cost Hamiltonian generated by theqaoa
module, has been sped up. This was achieved by making PennyLane decompose a circuit with anexpval(H)
measurement into subcircuits if theHamiltonian.grouping_indices
attribute is set, and setting this attribute in the relevantqaoa
module functions. (#1718)Operations can now have gradient recipes that depend on the state of the operation. (#1674)
For example, this allows for gradient recipes that are parameter dependent:
class RX(qml.RX): @property def grad_recipe(self): # The gradient is given by [f(2x) - f(0)] / (2 sin(x)), by subsituting # shift = x into the two term parameter-shift rule. x = self.data[0] c = 0.5 / np.sin(x) return ([[c, 0.0, 2 * x], [-c, 0.0, 0.0]],)
Shots can now be passed as a runtime argument to transforms that execute circuits in batches, similarly to QNodes. (#1707)
An example of such a transform are the gradient transforms in the
qml.gradients
module. As a result, we can now call gradient transforms (such asqml.gradients.param_shift
) and set the number of shots at runtime.>>> dev = qml.device("default.qubit", wires=1, shots=1000) >>> @qml.beta.qnode(dev) ... def circuit(x): ... qml.RX(x, wires=0) ... return qml.expval(qml.PauliZ(0)) >>> grad_fn = qml.gradients.param_shift(circuit) >>> param = np.array(0.564, requires_grad=True) >>> grad_fn(param, shots=[(1, 10)]).T array([[-1., -1., -1., -1., -1., 0., -1., 0., -1., 0.]]) >>> param2 = np.array(0.1233, requires_grad=True) >>> grad_fn(param2, shots=None) array([[-0.12298782]])
Templates are now top level imported and can be used directly e.g.
qml.QFT(wires=0)
. (#1779)qml.probs
now accepts an attributeop
that allows to rotate the computational basis and get the probabilities in the rotated basis. (#1692)Refactored the
expand_fn
functionality in the Device class to avoid any edge cases leading to failures with plugins. (#1838)Updated the
qml.QNGOptimizer.step_and_cost
method to avoid the use of deprecated functionality. (#1834)Added a custom
torch.to_numpy
implementation topennylane/math/single_dispatch.py
to ensure compabilitity with PyTorch 1.10. (#1824) (#1825)The default for an
Operation
‘scontrol_wires
attribute is now an emptyWires
object instead of the attribute raising aNonImplementedError
. (#1821)qml.circuit_drawer.MPLDrawer
will now automatically rotate and resize text to fit inside the rectangle created by thebox_gate
method. (#1764)Operators now have a
label
method to determine how they are drawn. This will eventually override theRepresentationResolver
class. (#1678)The operation
label
method now supports string variables. (#1815)A new utility class
qml.BooleanFn
is introduced. It wraps a function that takes a single argument and returns a Boolean. (#1734)After wrapping,
qml.BooleanFn
can be called like the wrapped function, and multiple instances can be manipulated and combined with the bitwise operators&
,|
and~
.There is a new utility function
qml.math.is_independent
that checks whether a callable is independent of its arguments. (#1700)This function is experimental and might behave differently than expected.
Note that the test relies on both numerical and analytical checks, except when using the PyTorch interface which only performs a numerical check. It is known that there are edge cases on which this test will yield wrong results, in particular non-smooth functions may be problematic. For details, please refer to the is_indpendent docstring.
The
qml.beta.QNode
now supports theqml.qnn
module. (#1748)@qml.beta.QNode
now supports theqml.specs
transform. (#1739)qml.circuit_drawer.drawable_layers
andqml.circuit_drawer.drawable_grid
process a list of operations to layer positions for drawing. (#1639)qml.transforms.batch_transform
now acceptsexpand_fn
s that take additional arguments and keyword arguments. In fact,expand_fn
andtransform_fn
now must have the same signature. (#1721)The
qml.batch_transform
decorator is now ignored during Sphinx builds, allowing the correct signature to display in the built documentation. (#1733)The tests for qubit operations are split into multiple files. (#1661)
The transform for the Jacobian of the classical preprocessing within a QNode,
qml.transforms.classical_jacobian
, now takes a keyword argumentargnum
to specify the QNode argument indices with respect to which the Jacobian is computed. (#1645)An example for the usage of
argnum
is@qml.qnode(dev) def circuit(x, y, z): qml.RX(qml.math.sin(x), wires=0) qml.CNOT(wires=[0, 1]) qml.RY(y ** 2, wires=1) qml.RZ(1 / z, wires=1) return qml.expval(qml.PauliZ(0)) jac_fn = qml.transforms.classical_jacobian(circuit, argnum=[1, 2])
The Jacobian can then be computed at specified parameters.
>>> x, y, z = np.array([0.1, -2.5, 0.71]) >>> jac_fn(x, y, z) (array([-0., -5., -0.]), array([-0. , -0. , -1.98373339]))
The returned arrays are the derivatives of the three parametrized gates in the circuit with respect to
y
andz
respectively.There also are explicit tests for
classical_jacobian
now, which previously was tested implicitly via its use in themetric_tensor
transform.For more usage details, please see the classical Jacobian docstring.
A new utility function
qml.math.is_abstract(tensor)
has been added. This function returnsTrue
if the tensor is abstract; that is, it has no value or shape. This can occur if within a function that has been just-in-time compiled. (#1845)qml.circuit_drawer.CircuitDrawer
can accept a string for thecharset
keyword, instead of aCharSet
object. (#1640)qml.math.sort
will now return only the sorted torch tensor and not the corresponding indices, making sort consistent across interfaces. (#1691)Specific QNode execution options are now re-used by batch transforms to execute transformed QNodes. (#1708)
To standardize across all optimizers,
qml.optimize.AdamOptimizer
now also usesaccumulation
(in form ofcollections.namedtuple
) to keep track of running quantities. Before it used three variablesfm
,sm
andt
. (#1757)
Breaking changes
The operator attributes
has_unitary_generator
,is_composable_rotation
,is_self_inverse
,is_symmetric_over_all_wires
, andis_symmetric_over_control_wires
have been removed as attributes from the base class. They have been replaced by the sets that store the names of operations with similar properties inops/qubit/attributes.py
. (#1763)The
qml.inv
function has been removed,qml.adjoint
should be used instead. (#1778)The input signature of an
expand_fn
used in abatch_transform
now must have the same signature as the providedtransform_fn
, and vice versa. (#1721)The
default.qubit.torch
device automatically determines if computations should be run on a CPU or a GPU and doesn’t take atorch_device
argument anymore. (#1705)The utility function
qml.math.requires_grad
now returnsTrue
when using Autograd if and only if therequires_grad=True
attribute is set on the NumPy array. Previously, this function would returnTrue
for all NumPy arrays and Python floats, unlessrequires_grad=False
was explicitly set. (#1638)The operation
qml.Interferometer
has been renamedqml.InterferometerUnitary
in order to distinguish it from the templateqml.templates.Interferometer
. (#1714)The
qml.transforms.invisible
decorator has been replaced withqml.tape.stop_recording
, which may act as a context manager as well as a decorator to ensure that contained logic is non-recordable or non-queueable within a QNode or quantum tape context. (#1754)Templates
SingleExcitationUnitary
andDoubleExcitationUnitary
have been renamed toFermionicSingleExcitation
andFermionicDoubleExcitation
, respectively. (#1822)
Deprecations
Allowing cost functions to be differentiated using
qml.grad
orqml.jacobian
without explicitly marking parameters as trainable is being deprecated, and will be removed in an upcoming release. Please specify therequires_grad
attribute for every argument, or specifyargnum
when usingqml.grad
orqml.jacobian
. (#1773)The following raises a warning in v0.19.0 and will raise an error in an upcoming release:
import pennylane as qml dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def test(x): qml.RY(x, wires=[0]) return qml.expval(qml.PauliZ(0)) par = 0.3 qml.grad(test)(par)
Preferred approaches include specifying the
requires_grad
attribute:import pennylane as qml from pennylane import numpy as np dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def test(x): qml.RY(x, wires=[0]) return qml.expval(qml.PauliZ(0)) par = np.array(0.3, requires_grad=True) qml.grad(test)(par)
Or specifying the
argnum
argument when usingqml.grad
orqml.jacobian
:import pennylane as qml dev = qml.device('default.qubit', wires=1) @qml.qnode(dev) def test(x): qml.RY(x, wires=[0]) return qml.expval(qml.PauliZ(0)) par = 0.3 qml.grad(test, argnum=0)(par)
The
default.tensor
device from the beta folder is no longer maintained and has been deprecated. It will be removed in future releases. (#1851)The
qml.metric_tensor
andqml.QNGOptimizer
keyword argumentdiag_approx
is deprecated. Approximations can be controlled with the more fine-grainedapprox
keyword argument, withapprox="block-diag"
(the default) reproducing the old behaviour. (#1721) (#1834)The
template
decorator is now deprecated with a warning message and will be removed in releasev0.20.0
. It has been removed from different PennyLane functions. (#1794) (#1808)The
qml.fourier.spectrum
function has been renamed toqml.fourier.circuit_spectrum
, in order to clearly separate the newqnode_spectrum
function from this one.qml.fourier.spectrum
is now an alias forcircuit_spectrum
but is flagged for deprecation and will be removed soon. (#1681)The
init
module, which contains functions to generate random parameter tensors for templates, is flagged for deprecation and will be removed in the next release cycle. Instead, the templates’shape
method can be used to get the desired shape of the tensor, which can then be generated manually. (#1689)To generate the parameter tensors, the
np.random.normal
andnp.random.uniform
functions can be used (just like in theinit
module). Considering the default arguments of these functions as of NumPy v1.21, some non-default options were used by theinit
module:All functions generating normally distributed parameters used
np.random.normal
by passingscale=0.1
;Most functions generating uniformly distributed parameters (except for certain CVQNN initializers) used
np.random.uniform
by passinghigh=2*math.pi
;The
cvqnn_layers_r_uniform
,cvqnn_layers_a_uniform
,cvqnn_layers_kappa_uniform
functions usednp.random.uniform
by passinghigh=0.1
.
The
QNode.draw
method has been deprecated, and will be removed in an upcoming release. Please use theqml.draw
transform instead. (#1746)The
QNode.metric_tensor
method has been deprecated, and will be removed in an upcoming release. Please use theqml.metric_tensor
transform instead. (#1638)The
pad
parameter of theqml.AmplitudeEmbedding
template has been removed. It has instead been renamed to thepad_with
parameter. (#1805)
Bug fixes
Fixes a bug where
qml.math.dot
failed to work with@tf.function
autograph mode. (#1842)Fixes a bug where in rare instances the parameters of a tape are returned unsorted by
Tape.get_parameters
. (#1836)Fixes a bug with the arrow width in the
measure
ofqml.circuit_drawer.MPLDrawer
. (#1823)The helper functions
qml.math.block_diag
andqml.math.scatter_element_add
now are entirely differentiable when using Autograd. Previously only indexed entries of the block diagonal could be differentiated, while the derivative w.r.t to the second argument ofqml.math.scatter_element_add
dispatched to NumPy instead of Autograd. (#1816) (#1818)Fixes a bug such that the original shot vector information of a device is preserved, so that outside the context manager the device remains unchanged. (#1792)
Modifies
qml.math.take
to be compatible with a breaking change released in JAX 0.2.24 and ensure that PennyLane supports this JAX version. (#1769)Fixes a bug where the GPU cannot be used with
qml.qnn.TorchLayer
. (#1705)Fix a bug where the devices cache the same result for different observables return types. (#1719)
Fixed a bug of the default circuit drawer where having more measurements compared to the number of measurements on any wire raised a
KeyError
. (#1702)Fix a bug where it was not possible to use
jax.jit
on aQNode
when usingQubitStateVector
. (#1683)The device suite tests can now execute successfully if no shots configuration variable is given. (#1641)
Fixes a bug where the
qml.gradients.param_shift
transform would raise an error while attempting to compute the variance of a QNode with ragged output. (#1646)Fixes a bug in
default.mixed
, to ensure that returned probabilities are always non-negative. (#1680)Fixes a bug where gradient transforms would fail to apply to QNodes containing classical processing. (#1699)
Fixes a bug where the the parameter-shift method was not correctly using the fallback gradient function when all circuit parameters required the fallback. (#1782)
Documentation
Adds a link to https://pennylane.ai/qml/demonstrations.html in the navbar. (#1624)
Corrects the docstring of
ExpvalCost
by addingwires
to the signature of theansatz
argument. (#1715)Updated docstring examples using the
qchem.molecular_hamiltonian
function. (#1724)Updates the ‘Gradients and training’ quickstart guide to provide information on gradient transforms. (#1751)
All instances of
qnode.draw()
have been updated to instead use the transformqml.draw(qnode)
. (#1750)Add the
jax
interface in QNode Documentation. (#1755)Reorganized all the templates related to quantum chemistry under a common header
Quantum Chemistry templates
. (#1822)
Contributors
This release contains contributions from (in alphabetical order):
Catalina Albornoz, Juan Miguel Arrazola, Utkarsh Azad, Akash Narayanan B, Sam Banning, Thomas Bromley, Jack Ceroni, Alain Delgado, Olivia Di Matteo, Andrew Gardhouse, Anthony Hayes, Theodor Isacsson, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Guillermo Alonso-Linaje, Romain Moyard, Lee James O’Riordan, Carrie-Anne Rubidge, Maria Schuld, Rishabh Singh, Jay Soni, Ingrid Strandberg, Antal Száva, Teresa Tamayo-Mendoza, Rodrigo Vargas, Cody Wang, David Wierichs, Moritz Willmann.
- orphan
Release 0.18.0¶
New features since last release
PennyLane now comes packaged with lightning.qubit
The C++-based lightning.qubit device is now included with installations of PennyLane. (#1663)
The
lightning.qubit
device is a fast state-vector simulator equipped with the efficient adjoint method for differentiating quantum circuits, check out the plugin release notes for more details! The device can be accessed in the following way:import pennylane as qml wires = 3 layers = 2 dev = qml.device("lightning.qubit", wires=wires) @qml.qnode(dev, diff_method="adjoint") def circuit(weights): qml.templates.StronglyEntanglingLayers(weights, wires=range(wires)) return qml.expval(qml.PauliZ(0)) weights = qml.init.strong_ent_layers_normal(layers, wires, seed=1967)
Evaluating circuits and their gradients on the device can be achieved using the standard approach:
>>> print(f"Circuit evaluated: {circuit(weights)}") Circuit evaluated: 0.9801286266677633 >>> print(f"Circuit gradient:\n{qml.grad(circuit)(weights)}") Circuit gradient: [[[-9.35301749e-17 -1.63051504e-01 -4.14810501e-04] [-7.88816484e-17 -1.50136528e-04 -1.77922957e-04] [-5.20670796e-17 -3.92874550e-02 8.14523075e-05]] [[-1.14472273e-04 3.85963953e-02 -9.39190132e-18] [-5.76791765e-05 -9.78478343e-02 0.00000000e+00] [ 0.00000000e+00 0.00000000e+00 0.00000000e+00]]]
The adjoint method operates after a forward pass by iteratively applying inverse gates to scan backwards through the circuit. The method is already available in PennyLane’s
default.qubit
device, but the version provided bylightning.qubit
integrates with the C++ backend and is more performant, as shown in the plot below:
Support for native backpropagation using PyTorch
The built-in PennyLane simulator
default.qubit
now supports backpropogation with PyTorch. (#1360) (#1598)As a result,
default.qubit
can now use end-to-end classical backpropagation as a means to compute gradients. End-to-end backpropagation can be faster than the parameter-shift rule for computing quantum gradients when the number of parameters to be optimized is large. This is now the default differentiation method when usingdefault.qubit
with PyTorch.Using this method, the created QNode is a ‘white-box’ that is tightly integrated with your PyTorch computation, including TorchScript and GPU support.
x = torch.tensor(0.43316321, dtype=torch.float64, requires_grad=True) y = torch.tensor(0.2162158, dtype=torch.float64, requires_grad=True) z = torch.tensor(0.75110998, dtype=torch.float64, requires_grad=True) p = torch.tensor([x, y, z], requires_grad=True) dev = qml.device("default.qubit", wires=1) @qml.qnode(dev, interface="torch", diff_method="backprop") def circuit(x): qml.Rot(x[0], x[1], x[2], wires=0) return qml.expval(qml.PauliZ(0)) res = circuit(p) res.backward()
>>> res = circuit(p) >>> res.backward() >>> print(p.grad) tensor([-9.1798e-17, -2.1454e-01, -1.0511e-16], dtype=torch.float64)
Improved quantum optimization methods
The
RotosolveOptimizer
now can tackle general parametrized circuits, and is no longer restricted to single-qubit Pauli rotations. (#1489)This includes:
layers of gates controlled by the same parameter,
controlled variants of parametrized gates, and
Hamiltonian time evolution.
Note that the eigenvalue spectrum of the gate generator needs to be known to use
RotosolveOptimizer
for a general gate, and it is required to produce equidistant frequencies. For details see Vidal and Theis, 2018 and Wierichs, Izaac, Wang, Lin 2021.Consider a circuit with a mixture of Pauli rotation gates, controlled Pauli rotations, and single-parameter layers of Pauli rotations:
dev = qml.device('default.qubit', wires=3, shots=None) @qml.qnode(dev) def cost_function(rot_param, layer_par, crot_param): for i, par in enumerate(rot_param): qml.RX(par, wires=i) for w in dev.wires: qml.RX(layer_par, wires=w) for i, par in enumerate(crot_param): qml.CRY(par, wires=[i, (i+1) % 3]) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2))
This cost function has one frequency for each of the first
RX
rotation angles, three frequencies for the layer ofRX
gates that depend onlayer_par
, and two frequencies for each of theCRY
gate parameters. Rotosolve can then be used to minimize thecost_function
:# Initial parameters init_param = [ np.array([0.3, 0.2, 0.67], requires_grad=True), np.array(1.1, requires_grad=True), np.array([-0.2, 0.1, -2.5], requires_grad=True), ] # Numbers of frequencies per parameter num_freqs = [[1, 1, 1], 3, [2, 2, 2]] opt = qml.RotosolveOptimizer() param = init_param.copy()
In addition, the optimization technique for the Rotosolve substeps can be chosen via the
optimizer
andoptimizer_kwargs
keyword arguments and the minimized cost of the intermediate univariate reconstructions can be read out viafull_output
, including the cost after the full Rotosolve step:for step in range(3): param, cost, sub_cost = opt.step_and_cost( cost_function, *param, num_freqs=num_freqs, full_output=True, optimizer="brute", ) print(f"Cost before step: {cost}") print(f"Minimization substeps: {np.round(sub_cost, 6)}")
Cost before step: 0.042008210392535605 Minimization substeps: [-0.230905 -0.863336 -0.980072 -0.980072 -1. -1. -1. ] Cost before step: -0.999999999068121 Minimization substeps: [-1. -1. -1. -1. -1. -1. -1.] Cost before step: -1.0 Minimization substeps: [-1. -1. -1. -1. -1. -1. -1.]
For usage details please consider the docstring of the optimizer.
Faster, trainable, Hamiltonian simulations
Hamiltonians are now trainable with respect to their coefficients. (#1483)
from pennylane import numpy as np dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(coeffs, param): qml.RX(param, wires=0) qml.RY(param, wires=0) return qml.expval( qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.PauliZ(0)], simplify=True) ) coeffs = np.array([-0.05, 0.17]) param = np.array(1.7) grad_fn = qml.grad(circuit)
>>> grad_fn(coeffs, param) (array([-0.12777055, 0.0166009 ]), array(0.0917819))
Furthermore, a gradient recipe for Hamiltonian coefficients has been added. This makes it possible to compute parameter-shift gradients of these coefficients on devices that natively support Hamiltonians. (#1551)
Hamiltonians are now natively supported on the
default.qubit
device ifshots=None
. This makes VQE workflows a lot faster in some cases. (#1551) (#1596)The Hamiltonian can now store grouping information, which can be accessed by a device to speed up computations of the expectation value of a Hamiltonian. (#1515)
obs = [qml.PauliX(0), qml.PauliX(1), qml.PauliZ(0)] coeffs = np.array([1., 2., 3.]) H = qml.Hamiltonian(coeffs, obs, grouping_type='qwc')
Initialization with a
grouping_type
other thanNone
stores the indices required to make groups of commuting observables and their coefficients.>>> H.grouping_indices [[0, 1], [2]]
Create multi-circuit quantum transforms and custom gradient rules
Custom gradient transforms can now be created using the new
@qml.gradients.gradient_transform
decorator on a batch-tape transform. (#1589)Quantum gradient transforms are a specific case of
qml.batch_transform
.Supported gradient transforms must be of the following form:
@qml.gradients.gradient_transform def my_custom_gradient(tape, argnum=None, **kwargs): ... return gradient_tapes, processing_fn
Various built-in quantum gradient transforms are provided within the
qml.gradients
module, includingqml.gradients.param_shift
. Once defined, quantum gradient transforms can be applied directly to QNodes:>>> @qml.qnode(dev) ... def circuit(x): ... qml.RX(x, wires=0) ... qml.CNOT(wires=[0, 1]) ... return qml.expval(qml.PauliZ(0)) >>> circuit(0.3) tensor(0.95533649, requires_grad=True) >>> qml.gradients.param_shift(circuit)(0.5) array([[-0.47942554]])
Quantum gradient transforms are fully differentiable, allowing higher order derivatives to be accessed:
>>> qml.grad(qml.gradients.param_shift(circuit))(0.5) tensor(-0.87758256, requires_grad=True)
Refer to the page of quantum gradient transforms for more details.
The ability to define batch transforms has been added via the new
@qml.batch_transform
decorator. (#1493)A batch transform is a transform that takes a single tape or QNode as input, and executes multiple tapes or QNodes independently. The results may then be post-processed before being returned.
For example, consider the following batch transform:
@qml.batch_transform def my_transform(tape, a, b): """Generates two tapes, one with all RX replaced with RY, and the other with all RX replaced with RZ.""" tape1 = qml.tape.JacobianTape() tape2 = qml.tape.JacobianTape() # loop through all operations on the input tape for op in tape.operations + tape.measurements: if op.name == "RX": with tape1: qml.RY(a * qml.math.abs(op.parameters[0]), wires=op.wires) with tape2: qml.RZ(b * qml.math.abs(op.parameters[0]), wires=op.wires) else: for t in [tape1, tape2]: with t: qml.apply(op) def processing_fn(results): return qml.math.sum(qml.math.stack(results)) return [tape1, tape2], processing_fn
We can transform a QNode directly using decorator syntax:
>>> @my_transform(0.65, 2.5) ... @qml.qnode(dev) ... def circuit(x): ... qml.Hadamard(wires=0) ... qml.RX(x, wires=0) ... return qml.expval(qml.PauliX(0)) >>> print(circuit(-0.5)) 1.2629730888100839
Batch tape transforms are fully differentiable:
>>> gradient = qml.grad(circuit)(-0.5) >>> print(gradient) 2.5800122591960153
Batch transforms can also be applied to existing QNodes,
>>> new_qnode = my_transform(existing_qnode, *transform_weights) >>> new_qnode(weights)
or to tapes (in which case, the processed tapes and classical post-processing functions are returned):
>>> tapes, fn = my_transform(tape, 0.65, 2.5) >>> from pennylane.interfaces.batch import execute >>> dev = qml.device("default.qubit", wires=1) >>> res = execute(tapes, dev, interface="autograd", gradient_fn=qml.gradients.param_shift) 1.2629730888100839
Vector-Jacobian product transforms have been added to the
qml.gradients
package. (#1494)The new transforms include:
qml.gradients.vjp
qml.gradients.batch_vjp
Support for differentiable execution of batches of circuits has been added, via the beta
pennylane.interfaces.batch
module. (#1501) (#1508) (#1542) (#1549) (#1608) (#1618) (#1637)For now, this is a low-level feature, and will be integrated into the QNode in a future release. For example:
from pennylane.interfaces.batch import execute def cost_fn(x): with qml.tape.JacobianTape() as tape1: qml.RX(x[0], wires=[0]) qml.RY(x[1], wires=[1]) qml.CNOT(wires=[0, 1]) qml.var(qml.PauliZ(0) @ qml.PauliX(1)) with qml.tape.JacobianTape() as tape2: qml.RX(x[0], wires=0) qml.RY(x[0], wires=1) qml.CNOT(wires=[0, 1]) qml.probs(wires=1) result = execute( [tape1, tape2], dev, gradient_fn=qml.gradients.param_shift, interface="autograd" ) return result[0] + result[1][0, 0] res = qml.grad(cost_fn)(params)
Improvements
A new operation
qml.SISWAP
has been added, the square-root of theqml.ISWAP
operation. (#1563)The
frobenius_inner_product
function has been moved to theqml.math
module, and is now differentiable using all autodiff frameworks. (#1388)A warning is raised to inform the user that specifying a list of shots is only supported for
QubitDevice
based devices. (#1659)The
qml.circuit_drawer.MPLDrawer
class provides manual circuit drawing functionality using Matplotlib. While not yet integrated with automatic circuit drawing, this class provides customization and control. (#1484)from pennylane.circuit_drawer import MPLDrawer drawer = MPLDrawer(n_wires=3, n_layers=3) drawer.label([r"$|\Psi\rangle$", r"$|\theta\rangle$", "aux"]) drawer.box_gate(layer=0, wires=[0, 1, 2], text="Entangling Layers", text_options={'rotation': 'vertical'}) drawer.box_gate(layer=1, wires=[0, 1], text="U(θ)") drawer.CNOT(layer=2, wires=[1, 2]) drawer.measure(layer=3, wires=2) drawer.fig.suptitle('My Circuit', fontsize='xx-large')
The slowest tests, more than 1.5 seconds, now have the pytest mark
slow
, and can be selected or deselected during local execution of tests. (#1633)The device test suite has been expanded to cover more qubit operations and observables. (#1510)
The
MultiControlledX
class now inherits fromOperation
instead ofControlledQubitUnitary
which makes theMultiControlledX
gate a non-parameterized gate. (#1557)The
utils.sparse_hamiltonian
function can now deal with non-integer wire labels, and it throws an error for the edge case of observables that are created from multi-qubit operations. (#1550)Added the matrix attribute to
qml.templates.subroutines.GroverOperator
(#1553)The
tape.to_openqasm()
method now has ameasure_all
argument that specifies whether the serialized OpenQASM script includes computational basis measurements on all of the qubits or just those specified by the tape. (#1559)An error is now raised when no arguments are passed to an observable, to inform that wires have not been supplied. (#1547)
The
group_observables
transform is now differentiable. (#1483)For example:
import jax from jax import numpy as jnp coeffs = jnp.array([1., 2., 3.]) obs = [PauliX(wires=0), PauliX(wires=1), PauliZ(wires=1)] def group(coeffs, select=None): _, grouped_coeffs = qml.grouping.group_observables(obs, coeffs) # in this example, grouped_coeffs is a list of two jax tensors # [DeviceArray([1., 2.], dtype=float32), DeviceArray([3.], dtype=float32)] return grouped_coeffs[select] jac_fn = jax.jacobian(group)
>>> jac_fn(coeffs, select=0) [[1. 0. 0.] [0. 1. 0.]] >>> jac_fn(coeffs, select=1) [[0., 0., 1.]]
The tape does not verify any more that all Observables have owners in the annotated queue. (#1505)
This allows manipulation of Observables inside a tape context. An example is
expval(Tensor(qml.PauliX(0), qml.Identity(1)).prune())
which makes the expval an owner of the pruned tensor and its constituent observables, but leaves the original tensor in the queue without an owner.The
qml.ResetError
is now supported fordefault.mixed
device. (#1541)QNode.diff_method
will now reflect which method was selected fromdiff_method="best"
. (#1568)QNodes now support
diff_method=None
. This works the same asinterface=None
. Such QNodes accept floats, ints, lists and NumPy arrays and return NumPy output but can not be differentiated. (#1585)QNodes now include validation to warn users if a supplied keyword argument is not one of the recognized arguments. (#1591)
Breaking changes
The
QFT
operation has been moved, and is now accessible viapennylane.templates.QFT
. (#1548)Specifying
shots=None
withqml.sample
was previously deprecated. From this release onwards, settingshots=None
when sampling will raise an error also fordefault.qubit.jax
. (#1629)An error is raised during QNode creation when a user requests backpropagation on a device with finite-shots. (#1588)
The class
qml.Interferometer
is deprecated and will be renamedqml.InterferometerUnitary
after one release cycle. (#1546)All optimizers except for Rotosolve and Rotoselect now have a public attribute
stepsize
. Temporary backward compatibility has been added to support the use of_stepsize
for one release cycle.update_stepsize
method is deprecated. (#1625)
Bug fixes
Fixed a bug with shot vectors and
Device
base class. (#1666)Fixed a bug where
@jax.jit
would fail on a QNode that usedqml.QubitStateVector
. (#1649)Fixed a bug related to an edge case of single-qubit
zyz_decomposition
when only off-diagonal elements are present. (#1643)MottonenStatepreparation
can now be run with a single wire label not in a list. (#1620)Fixed the circuit representation of CY gates to align with CNOT and CZ gates when calling the circuit drawer. (#1504)
Dask and CVXPY dependent tests are skipped if those packages are not installed. (#1617)
The
qml.layer
template now works with tensorflow variables. (#1615)Remove
QFT
from possible operations indefault.qubit
anddefault.mixed
. (#1600)Fixed a bug when computing expectations of Hamiltonians using TensorFlow. (#1586)
Fixed a bug when computing the specs of a circuit with a Hamiltonian observable. (#1533)
Documentation
The
qml.Identity
operation is placed under the sections Qubit observables and CV observables. (#1576)Updated the documentation of
qml.grouping
,qml.kernels
andqml.qaoa
modules to present the list of functions first followed by the technical details of the module. (#1581)Recategorized Qubit operations into new and existing categories so that code for each operation is easier to locate. (#1566)
Contributors
This release contains contributions from (in alphabetical order):
Vishnu Ajith, Akash Narayanan B, Thomas Bromley, Olivia Di Matteo, Sahaj Dhamija, Tanya Garg, Anthony Hayes, Theodor Isacsson, Josh Izaac, Prateek Jain, Ankit Khandelwal, Nathan Killoran, Christina Lee, Ian McLean, Johannes Jakob Meyer, Romain Moyard, Lee James O’Riordan, Esteban Payares, Pratul Saini, Maria Schuld, Arshpreet Singh, Jay Soni, Ingrid Strandberg, Antal Száva, Slimane Thabet, David Wierichs, Vincent Wong.
- orphan
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 tape does not verify any more that all Observables have owners in the annotated queue. (#1505)
This allows manipulation of Observables inside a tape context. An example is
expval(Tensor(qml.PauliX(0), qml.Identity(1)).prune())
which makes the expval an owner of the pruned tensor and its constituent observables, but leaves the original tensor in the queue without an owner.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.
- orphan
Release 0.16.0¶
First class support for quantum kernels
The new
qml.kernels
module provides basic functionalities for working with quantum kernels as well as post-processing methods to mitigate sampling errors and device noise: (#1102)num_wires = 6 wires = range(num_wires) dev = qml.device('default.qubit', wires=wires) @qml.qnode(dev) def kernel_circuit(x1, x2): qml.templates.AngleEmbedding(x1, wires=wires) qml.adjoint(qml.templates.AngleEmbedding)(x2, wires=wires) return qml.probs(wires) kernel = lambda x1, x2: kernel_circuit(x1, x2)[0] X_train = np.random.random((10, 6)) X_test = np.random.random((5, 6)) # Create symmetric square kernel matrix (for training) K = qml.kernels.square_kernel_matrix(X_train, kernel) # Compute kernel between test and training data. K_test = qml.kernels.kernel_matrix(X_train, X_test, kernel) K1 = qml.kernels.mitigate_depolarizing_noise(K, num_wires, method='single')
Extract the fourier representation of quantum circuits
PennyLane now has a
fourier
module, which hosts a growing library of methods that help with investigating the Fourier representation of functions implemented by quantum circuits. The Fourier representation can be used to examine and characterize the expressivity of the quantum circuit. (#1160) (#1378)For example, one can plot distributions over Fourier series coefficients like this one:
Seamless support for working with the Pauli group
Added functionality for constructing and manipulating the Pauli group (#1181).
The function
qml.grouping.pauli_group
provides a generator to easily loop over the group, or construct and store it in its entirety. For example, we can construct the single-qubit Pauli group like so:>>> from pennylane.grouping import pauli_group >>> pauli_group_1_qubit = list(pauli_group(1)) >>> pauli_group_1_qubit [Identity(wires=[0]), PauliZ(wires=[0]), PauliX(wires=[0]), PauliY(wires=[0])]
We can multiply together its members at the level of Pauli words using the
pauli_mult
andpauli_multi_with_phase
functions. This can be done on arbitrarily-labeled wires as well, by defining a wire map.>>> from pennylane.grouping import pauli_group, pauli_mult >>> wire_map = {'a' : 0, 'b' : 1, 'c' : 2} >>> pg = list(pauli_group(3, wire_map=wire_map)) >>> pg[3] PauliZ(wires=['b']) @ PauliZ(wires=['c']) >>> pg[55] PauliY(wires=['a']) @ PauliY(wires=['b']) @ PauliZ(wires=['c']) >>> pauli_mult(pg[3], pg[55], wire_map=wire_map) PauliY(wires=['a']) @ PauliX(wires=['b'])
Functions for conversion of Pauli observables to strings (and back), are included.
>>> from pennylane.grouping import pauli_word_to_string, string_to_pauli_word >>> pauli_word_to_string(pg[55], wire_map=wire_map) 'YYZ' >>> string_to_pauli_word('ZXY', wire_map=wire_map) PauliZ(wires=['a']) @ PauliX(wires=['b']) @ PauliY(wires=['c'])
Calculation of the matrix representation for arbitrary Paulis and wire maps is now also supported.
>>> from pennylane.grouping import pauli_word_to_matrix >>> wire_map = {'a' : 0, 'b' : 1} >>> pauli_word = qml.PauliZ('b') # corresponds to Pauli 'IZ' >>> pauli_word_to_matrix(pauli_word, wire_map=wire_map) array([[ 1., 0., 0., 0.], [ 0., -1., 0., -0.], [ 0., 0., 1., 0.], [ 0., -0., 0., -1.]])
New transforms
The
qml.specs
QNode transform creates a function that returns specifications or details about the QNode, including depth, number of gates, and number of gradient executions required. (#1245)For example:
dev = qml.device('default.qubit', wires=4) @qml.qnode(dev, diff_method='parameter-shift') def circuit(x, y): qml.RX(x[0], wires=0) qml.Toffoli(wires=(0, 1, 2)) qml.CRY(x[1], wires=(0, 1)) qml.Rot(x[2], x[3], y, wires=0) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))
We can now use the
qml.specs
transform to generate a function that returns details and resource information:>>> x = np.array([0.05, 0.1, 0.2, 0.3], requires_grad=True) >>> y = np.array(0.4, requires_grad=False) >>> specs_func = qml.specs(circuit) >>> specs_func(x, y) {'gate_sizes': defaultdict(int, {1: 2, 3: 1, 2: 1}), 'gate_types': defaultdict(int, {'RX': 1, 'Toffoli': 1, 'CRY': 1, 'Rot': 1}), 'num_operations': 4, 'num_observables': 2, 'num_diagonalizing_gates': 1, 'num_used_wires': 3, 'depth': 4, 'num_trainable_params': 4, 'num_parameter_shift_executions': 11, 'num_device_wires': 4, 'device_name': 'default.qubit', 'diff_method': 'parameter-shift'}
The tape methods
get_resources
andget_depth
are superseded byspecs
and will be deprecated after one release cycle.Adds a decorator
@qml.qfunc_transform
to easily create a transformation that modifies the behaviour of a quantum function. (#1315)For example, consider the following transform, which scales the parameter of all
RX
gates by \(x \rightarrow \sin(a) \sqrt{x}\), and the parameters of allRY
gates by \(y \rightarrow \cos(a * b) y\):@qml.qfunc_transform def my_transform(tape, a, b): for op in tape.operations + tape.measurements: if op.name == "RX": x = op.parameters[0] qml.RX(qml.math.sin(a) * qml.math.sqrt(x), wires=op.wires) elif op.name == "RY": y = op.parameters[0] qml.RX(qml.math.cos(a * b) * y, wires=op.wires) else: op.queue()
We can now apply this transform to any quantum function:
dev = qml.device("default.qubit", wires=2) def ansatz(x): qml.Hadamard(wires=0) qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[0, 1]) @qml.qnode(dev) def circuit(params, transform_weights): qml.RX(0.1, wires=0) # apply the transform to the ansatz my_transform(*transform_weights)(ansatz)(params) return qml.expval(qml.PauliZ(1))
We can print this QNode to show that the qfunc transform is taking place:
>>> x = np.array([0.5, 0.3], requires_grad=True) >>> transform_weights = np.array([0.1, 0.6], requires_grad=True) >>> print(qml.draw(circuit)(x, transform_weights)) 0: ──RX(0.1)────H──RX(0.0706)──╭C──┤ 1: ──RX(0.299)─────────────────╰X──┤ ⟨Z⟩
Evaluating the QNode, as well as the derivative, with respect to the gate parameter and the transform weights:
>>> circuit(x, transform_weights) tensor(0.00672829, requires_grad=True) >>> qml.grad(circuit)(x, transform_weights) (array([ 0.00671711, -0.00207359]), array([6.69695008e-02, 3.73694364e-06]))
Adds a
hamiltonian_expand
tape transform. This takes a tape ending inqml.expval(H)
, whereH
is a Hamiltonian, and maps it to a collection of tapes which can be executed and passed into a post-processing function yielding the expectation value. (#1142)Example use:
H = qml.PauliZ(0) + 3 * qml.PauliZ(0) @ qml.PauliX(1) with qml.tape.QuantumTape() as tape: qml.Hadamard(wires=1) qml.expval(H) tapes, fn = qml.transforms.hamiltonian_expand(tape)
We can now evaluate the transformed tapes, and apply the post-processing function:
>>> dev = qml.device("default.qubit", wires=3) >>> res = dev.batch_execute(tapes) >>> fn(res) 3.999999999999999
The
quantum_monte_carlo
transform has been added, allowing an input circuit to be transformed into the full quantum Monte Carlo algorithm. (#1316)Suppose we want to measure the expectation value of the sine squared function according to a standard normal distribution. We can calculate the expectation value analytically as
0.432332
, but we can also estimate using the quantum Monte Carlo algorithm. The first step is to discretize the problem:from scipy.stats import norm m = 5 M = 2 ** m 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 r_rotations = np.array([2 * np.arcsin(np.sqrt(func(i))) for i in range(M)])
The
quantum_monte_carlo
transform can then be used:from pennylane.templates.state_preparations.mottonen import ( _uniform_rotation_dagger as r_unitary, ) n = 6 N = 2 ** n a_wires = range(m) wires = range(m + 1) target_wire = m estimation_wires = range(m + 1, n + m + 1) dev = qml.device("default.qubit", wires=(n + m + 1)) def fn(): qml.templates.MottonenStatePreparation(np.sqrt(probs), wires=a_wires) r_unitary(qml.RY, r_rotations, control_wires=a_wires[::-1], target_wire=target_wire) @qml.qnode(dev) def qmc(): qml.quantum_monte_carlo(fn, wires, target_wire, estimation_wires)() return qml.probs(estimation_wires) phase_estimated = np.argmax(qmc()[:int(N / 2)]) / N
The estimated value can be retrieved using:
>>> (1 - np.cos(np.pi * phase_estimated)) / 2 0.42663476277231915
The resources required to perform the quantum Monte Carlo algorithm can also be inspected using the
specs
transform.
Extended QAOA module
Functionality to support solving the maximum-weighted cycle problem has been added to the
qaoa
module. (#1207) (#1209) (#1251) (#1213) (#1220) (#1214) (#1283) (#1297) (#1396) (#1403)The
max_weight_cycle
function returns the appropriate cost and mixer Hamiltonians:>>> a = np.random.random((3, 3)) >>> np.fill_diagonal(a, 0) >>> g = nx.DiGraph(a) # create a random directed graph >>> cost, mixer, mapping = qml.qaoa.max_weight_cycle(g) >>> print(cost) (-0.9775906842165344) [Z2] + (-0.9027248603361988) [Z3] + (-0.8722207409852838) [Z0] + (-0.6426184210832898) [Z5] + (-0.2832594164291379) [Z1] + (-0.0778133996933755) [Z4] >>> print(mapping) {0: (0, 1), 1: (0, 2), 2: (1, 0), 3: (1, 2), 4: (2, 0), 5: (2, 1)}
Additional functionality can be found in the qml.qaoa.cycle module.
Extended operations and templates
Added functionality to compute the sparse matrix representation of a
qml.Hamiltonian
object. (#1394)coeffs = [1, -0.45] obs = [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1)] H = qml.Hamiltonian(coeffs, obs) H_sparse = qml.utils.sparse_hamiltonian(H)
The resulting matrix is a sparse matrix in scipy coordinate list (COO) format:
>>> H_sparse <4x4 sparse matrix of type '<class 'numpy.complex128'>' with 8 stored elements in COOrdinate format>
The sparse matrix can be converted to an array as:
>>> H_sparse.toarray() array([[ 1.+0.j , 0.+0.j , 0.+0.45j, 0.+0.j ], [ 0.+0.j , -1.+0.j , 0.+0.j , 0.-0.45j], [ 0.-0.45j, 0.+0.j , -1.+0.j , 0.+0.j ], [ 0.+0.j , 0.+0.45j, 0.+0.j , 1.+0.j ]])
Adds the new template
AllSinglesDoubles
to prepare quantum states of molecules using theSingleExcitation
andDoubleExcitation
operations. The new template reduces significantly the number of operations and the depth of the quantum circuit with respect to the traditional UCCSD unitary. (#1383)For example, consider the case of two particles and four qubits. First, we define the Hartree-Fock initial state and generate all possible single and double excitations.
import pennylane as qml from pennylane import numpy as np electrons = 2 qubits = 4 hf_state = qml.qchem.hf_state(electrons, qubits) singles, doubles = qml.qchem.excitations(electrons, qubits)
Now we can use the template
AllSinglesDoubles
to define the quantum circuit,from pennylane.templates import AllSinglesDoubles wires = range(qubits) dev = qml.device('default.qubit', wires=wires) @qml.qnode(dev) def circuit(weights, hf_state, singles, doubles): AllSinglesDoubles(weights, wires, hf_state, singles, doubles) return qml.expval(qml.PauliZ(0)) params = np.random.normal(0, np.pi, len(singles) + len(doubles))
and execute it:
>>> circuit(params, hf_state, singles=singles, doubles=doubles) tensor(-0.73772194, requires_grad=True)
Adds
QubitCarry
andQubitSum
operations for basic arithmetic. (#1169)The following example adds two 1-bit numbers, returning a 2-bit answer:
dev = qml.device('default.qubit', wires = 4) a = 0 b = 1 @qml.qnode(dev) def circuit(): qml.BasisState(np.array([a, b]), wires=[1, 2]) qml.QubitCarry(wires=[0, 1, 2, 3]) qml.CNOT(wires=[1, 2]) qml.QubitSum(wires=[0, 1, 2]) return qml.probs(wires=[3, 2]) probs = circuit() bitstrings = tuple(itertools.product([0, 1], repeat = 2)) indx = np.argwhere(probs == 1).flatten()[0] output = bitstrings[indx]
>>> print(output) (0, 1)
Added the
qml.Projector
observable, which is available on all devices inheriting from theQubitDevice
class. (#1356) (#1368)Using
qml.Projector
, we can define the basis state projectors to use when computing expectation values. Let us take for example a circuit that prepares Bell states:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(basis_state): qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) return qml.expval(qml.Projector(basis_state, wires=[0, 1]))
We can then specify the
|00>
basis state to construct the|00><00|
projector and compute the expectation value:>>> basis_state = [0, 0] >>> circuit(basis_state) tensor(0.5, requires_grad=True)
As expected, we get similar results when specifying the
|11>
basis state:>>> basis_state = [1, 1] >>> circuit(basis_state) tensor(0.5, requires_grad=True)
The following new operations have been added:
Improvements
The
argnum
keyword argument can now be specified for a QNode to define a subset of trainable parameters used to estimate the Jacobian. (#1371)For example, consider two trainable parameters and a quantum function:
dev = qml.device("default.qubit", wires=2) x = np.array(0.543, requires_grad=True) y = np.array(-0.654, requires_grad=True) 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))
When computing the gradient of the QNode, we can specify the trainable parameters to consider by passing the
argnum
keyword argument:>>> qnode1 = qml.QNode(circuit, dev, diff_method="parameter-shift", argnum=[0,1]) >>> print(qml.grad(qnode1)(x,y)) (array(0.31434679), array(0.67949903))
Specifying a proper subset of the trainable parameters will estimate the Jacobian:
>>> qnode2 = qml.QNode(circuit, dev, diff_method="parameter-shift", argnum=[0]) >>> print(qml.grad(qnode2)(x,y)) (array(0.31434679), array(0.))
Allows creating differentiable observables that return custom objects such that the observable is supported by devices. (1291)
As an example, first we define
NewObservable
class:from pennylane.devices import DefaultQubit class NewObservable(qml.operation.Observable): """NewObservable""" num_wires = qml.operation.AnyWires num_params = 0 par_domain = None def diagonalizing_gates(self): """Diagonalizing gates""" return []
Once we have this new observable class, we define a
SpecialObject
class that can be used to encode data in an observable and a new device that supports our new observable and returns aSpecialObject
as the expectation value (the code is shortened for brevity, the extended example can be found as a test in the previously referenced pull request):class SpecialObject: def __init__(self, val): self.val = val def __mul__(self, other): new = SpecialObject(self.val) new *= other return new ... class DeviceSupportingNewObservable(DefaultQubit): name = "Device supporting NewObservable" short_name = "default.qubit.newobservable" observables = DefaultQubit.observables.union({"NewObservable"}) def expval(self, observable, **kwargs): if self.shots is None and isinstance(observable, NewObservable): val = super().expval(qml.PauliZ(wires=0), **kwargs) return SpecialObject(val) return super().expval(observable, **kwargs)
At this point, we can create a device that will support the differentiation of a
NewObservable
object:dev = DeviceSupportingNewObservable(wires=1, shots=None) @qml.qnode(dev, diff_method="parameter-shift") def qnode(x): qml.RY(x, wires=0) return qml.expval(NewObservable(wires=0))
We can then compute the jacobian of this object:
>>> result = qml.jacobian(qnode)(0.2) >>> print(result) <__main__.SpecialObject object at 0x7fd2c54721f0> >>> print(result.item().val) -0.19866933079506116
PennyLane NumPy now includes the random module’s
Generator
objects, the recommended way of random number generation. This allows for random number generation using a local, rather than global seed. (#1267)from pennylane import numpy as np rng = np.random.default_rng() random_mat1 = rng.random((3,2)) random_mat2 = rng.standard_normal(3, requires_grad=False)
The performance of adjoint jacobian differentiation was significantly improved as the method now reuses the state computed on the forward pass. This can be turned off to save memory with the Torch and TensorFlow interfaces by passing
adjoint_cache=False
during QNode creation. (#1341)The
Operator
(and by inheritance, theOperation
andObservable
class and their children) now have anid
attribute, which can mark an operator in a circuit, for example to identify it on the tape by a tape transform. (#1377)The
benchmark
module was deleted, since it was outdated and is superseded by the new separate benchmark repository. (#1343)Decompositions in terms of elementary gates has been added for:
qml.CSWAP
(#1306)qml.SWAP
(#1329)qml.SingleExcitation
(#1303)qml.SingleExcitationPlus
andqml.SingleExcitationMinus
(#1278)qml.DoubleExcitation
(#1303)qml.Toffoli
(#1320)qml.MultiControlledX
. (#1287) When controlling on three or more wires, an ancilla register of worker wires is required to support the decomposition.ctrl_wires = [f"c{i}" for i in range(5)] work_wires = [f"w{i}" for i in range(3)] target_wires = ["t0"] all_wires = ctrl_wires + work_wires + target_wires dev = qml.device("default.qubit", wires=all_wires) with qml.tape.QuantumTape() as tape: qml.MultiControlledX(control_wires=ctrl_wires, wires=target_wires, work_wires=work_wires)
>>> tape = tape.expand(depth=1) >>> print(tape.draw(wire_order=qml.wires.Wires(all_wires))) c0: ──────────────╭C──────────────────────╭C──────────┤ c1: ──────────────├C──────────────────────├C──────────┤ c2: ──────────╭C──│───╭C──────────────╭C──│───╭C──────┤ c3: ──────╭C──│───│───│───╭C──────╭C──│───│───│───╭C──┤ c4: ──╭C──│───│───│───│───│───╭C──│───│───│───│───│───┤ w0: ──│───│───├C──╰X──├C──│───│───│───├C──╰X──├C──│───┤ w1: ──│───├C──╰X──────╰X──├C──│───├C──╰X──────╰X──├C──┤ w2: ──├C──╰X──────────────╰X──├C──╰X──────────────╰X──┤ t0: ──╰X──────────────────────╰X──────────────────────┤
Added
qml.CPhase
as an alias for the existingqml.ControlledPhaseShift
operation. (#1319).The
Device
class now uses caching when mapping wires. (#1270)The
Wires
class now uses caching for computing itshash
. (#1270)Added custom gate application for Toffoli in
default.qubit
. (#1249)Added validation for noise channel parameters. Invalid noise parameters now raise a
ValueError
. (#1357)The device test suite now provides test cases for checking gates by comparing expectation values. (#1212)
PennyLane’s test suite is now code-formatted using
black -l 100
. (#1222)PennyLane’s
qchem
package and tests are now code-formatted usingblack -l 100
. (#1311)
Breaking changes
The
qml.inv()
function is now deprecated with a warning to use the more generalqml.adjoint()
. (#1325)Removes support for Python 3.6 and adds support for Python 3.9. (#1228)
The tape methods
get_resources
andget_depth
are superseded byspecs
and will be deprecated after one release cycle. (#1245)Using the
qml.sample()
measurement on devices withshots=None
continue to raise a warning with this functionality being fully deprecated and raising an error after one release cycle. (#1079) (#1196)
Bug fixes
QNodes now display readable information when in interactive environments or when printed. (#1359).
Fixes a bug with
qml.math.cast
where theMottonenStatePreparation
operation expected a float type instead of double. (#1400)Fixes a bug where a copy of
qml.ControlledQubitUnitary
was non-functional as it did not have all the necessary information. (#1411)Warns when adjoint or reversible differentiation specified or called on a device with finite shots. (#1406)
Fixes the differentiability of the operations
IsingXX
andIsingZZ
for Autograd, Jax and Tensorflow. (#1390)Fixes a bug where multiple identical Hamiltonian terms will produce a different result with
optimize=True
usingExpvalCost
. (#1405)Fixes bug where
shots=None
was not reset when changing shots temporarily in a QNode call likecircuit(0.1, shots=3)
. (#1392)Fixes floating point errors with
diff_method="finite-diff"
andorder=1
when parameters arefloat32
. (#1381)Fixes a bug where
qml.ctrl
would fail to transform gates that had no control defined and no decomposition defined. (#1376)Copying the
JacobianTape
now correctly also copies thejacobian_options
attribute. This fixes a bug allowing the JAX interface to support adjoint differentiation. (#1349)Fixes drawing QNodes that contain multiple measurements on a single wire. (#1353)
Fixes drawing QNodes with no operations. (#1354)
Fixes incorrect wires in the decomposition of the
ControlledPhaseShift
operation. (#1338)Fixed tests for the
Permute
operation that used a QNode and hence expanded tapes twice instead of once due to QNode tape expansion and an explicit tape expansion call. (#1318).Prevent Hamiltonians that share wires from being multiplied together. (#1273)
Fixed a bug where the custom range sequences could not be passed to the
StronglyEntanglingLayers
template. (#1332)Fixed a bug where
qml.sum()
andqml.dot()
do not support the JAX interface. (#1380)
Documentation
Math present in the
QubitParamShiftTape
class docstring now renders correctly. (#1402)Fix typo in the documentation of
qml.StronglyEntanglingLayers
. (#1367)Fixed typo in TensorFlow interface documentation (#1312)
Fixed typos in the mathematical expressions in documentation of
qml.DoubleExcitation
. (#1278)Remove unsupported
None
option from theqml.QNode
docstrings. (#1271)Updated the docstring of
qml.PolyXP
to reference the new location of internal usage. (#1262)Removes occurrences of the deprecated device argument
analytic
from the documentation. (#1261)Updated PyTorch and TensorFlow interface introductions. (#1333)
Updates the quantum chemistry quickstart to reflect recent changes to the
qchem
module. (#1227)
Contributors
This release contains contributions from (in alphabetical order):
Marius Aglitoiu, Vishnu Ajith, Juan Miguel Arrazola, Thomas Bromley, Jack Ceroni, Alaric Cheng, Miruna Daian, Olivia Di Matteo, Tanya Garg, Christian Gogolin, Alain Delgado Gran, Diego Guala, Anthony Hayes, Ryan Hill, Theodor Isacsson, Josh Izaac, Soran Jahangiri, Pavan Jayasinha, Nathan Killoran, Christina Lee, Ryan Levy, Alberto Maldonado, Johannes Jakob Meyer, Romain Moyard, Ashish Panigrahi, Nahum Sá, Maria Schuld, Brian Shi, Antal Száva, David Wierichs, Vincent Wong.
- orphan
Release 0.15.1¶
Bug fixes
Fixes two bugs in the parameter-shift Hessian. (#1260)
Fixes a bug where having an unused parameter in the Autograd interface would result in an indexing error during backpropagation.
The parameter-shift Hessian only supports the two-term parameter-shift rule currently, so raises an error if asked to differentiate any unsupported gates (such as the controlled rotation gates).
A bug which resulted in
qml.adjoint()
andqml.inv()
failing to work with templates has been fixed. (#1243)Deprecation warning instances in PennyLane have been changed to
UserWarning
, to account for recent changes to how Python warnings are filtered in PEP565. (#1211)
Documentation
Updated the order of the parameters to the
GaussianState
operation to match the way that the PennyLane-SF plugin uses them. (#1255)
Contributors
This release contains contributions from (in alphabetical order):
Thomas Bromley, Olivia Di Matteo, Diego Guala, Anthony Hayes, Ryan Hill, Josh Izaac, Christina Lee, Maria Schuld, Antal Száva.
- orphan
Release 0.15.0¶
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 \(f: \mathbb{R}^n \rightarrow\mathbb{R}^m\) between the input QNode arguments \(w\) and the gate arguments \(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, \(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 \(|01\rangle\) and \(|10\rangle\). For example, the following circuit performs the transformation \(|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 \(|1100\rangle\) and \(|0011\rangle\). For example, the following circuit performs the transformation \(|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.templates.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
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
, aUserWarning
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.
- orphan
Release 0.14.1¶
Bug fixes
Fixes a testing bug where tests that required JAX would fail if JAX was not installed. The tests will now instead be skipped if JAX can not be imported. (#1066)
Fixes a bug where inverse operations could not be differentiated using backpropagation on
default.qubit
. (#1072)The QNode has a new keyword argument,
max_expansion
, that determines the maximum number of times the internal circuit should be expanded when executed on a device. In addition, the default number of max expansions has been increased from 2 to 10, allowing devices that require more than two operator decompositions to be supported. (#1074)Fixes a bug where
Hamiltonian
objects created with non-list arguments raised an error for arithmetic operations. (#1082)Fixes a bug where
Hamiltonian
objects with no coefficients or operations would return a faulty result when used withExpvalCost
. (#1082)
Documentation
Updates mentions of
generate_hamiltonian
tomolecular_hamiltonian
in the docstrings of theExpvalCost
andHamiltonian
classes. (#1077)
Contributors
This release contains contributions from (in alphabetical order):
Thomas Bromley, Josh Izaac, Antal Száva.
- orphan
Release 0.14.0¶
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.
- orphan
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.
- orphan
Release 0.12.0¶
New features since last release
New and improved simulators
PennyLane now supports a new device,
default.mixed
, designed for simulating mixed-state quantum computations. This enables native support for implementing noisy channels in a circuit, which generally map pure states to mixed states. (#794) (#807) (#819)The device can be initialized as
>>> dev = qml.device("default.mixed", wires=1)
This allows the construction of QNodes that include non-unitary operations, such as noisy channels:
>>> @qml.qnode(dev) ... def circuit(params): ... qml.RX(params[0], wires=0) ... qml.RY(params[1], wires=0) ... qml.AmplitudeDamping(0.5, wires=0) ... return qml.expval(qml.PauliZ(0)) >>> print(circuit([0.54, 0.12])) 0.9257702929524184 >>> print(circuit([0, np.pi])) 0.0
New tools for optimizing measurements
The new
grouping
module provides functionality for grouping simultaneously measurable Pauli word observables. (#761) (#850) (#852)The
optimize_measurements
function will take as input a list of Pauli word observables and their corresponding coefficients (if any), and will return the partitioned Pauli terms diagonalized in the measurement basis and the corresponding diagonalizing circuits.from pennylane.grouping import optimize_measurements h, nr_qubits = qml.qchem.molecular_hamiltonian("h2", "h2.xyz") rotations, grouped_ops, grouped_coeffs = optimize_measurements(h.ops, h.coeffs, grouping="qwc")
The diagonalizing circuits of
rotations
correspond to the diagonalized Pauli word groupings ofgrouped_ops
.Pauli word partitioning utilities are performed by the
PauliGroupingStrategy
class. An input list of Pauli words can be partitioned into mutually commuting, qubit-wise-commuting, or anticommuting groupings.For example, partitioning Pauli words into anticommutative groupings by the Recursive Largest First (RLF) graph colouring heuristic:
from pennylane import PauliX, PauliY, PauliZ, Identity from pennylane.grouping import group_observables pauli_words = [ Identity('a') @ Identity('b'), Identity('a') @ PauliX('b'), Identity('a') @ PauliY('b'), PauliZ('a') @ PauliX('b'), PauliZ('a') @ PauliY('b'), PauliZ('a') @ PauliZ('b') ] groupings = group_observables(pauli_words, grouping_type='anticommuting', method='rlf')
Various utility functions are included for obtaining and manipulating Pauli words in the binary symplectic vector space representation.
For instance, two Pauli words may be converted to their binary vector representation:
>>> from pennylane.grouping import pauli_to_binary >>> from pennylane.wires import Wires >>> wire_map = {Wires('a'): 0, Wires('b'): 1} >>> pauli_vec_1 = pauli_to_binary(qml.PauliX('a') @ qml.PauliY('b')) >>> pauli_vec_2 = pauli_to_binary(qml.PauliZ('a') @ qml.PauliZ('b')) >>> pauli_vec_1 [1. 1. 0. 1.] >>> pauli_vec_2 [0. 0. 1. 1.]
Their product up to a phase may be computed by taking the sum of their binary vector representations, and returned in the operator representation.
>>> from pennylane.grouping import binary_to_pauli >>> binary_to_pauli((pauli_vec_1 + pauli_vec_2) % 2, wire_map) Tensor product ['PauliY', 'PauliX']: 0 params, wires ['a', 'b']
For more details on the grouping module, see the grouping module documentation
Returning the quantum state from simulators
The quantum state of a QNode can now be returned using the
qml.state()
return function. (#818)import pennylane as qml dev = qml.device("default.qubit", wires=3) qml.enable_tape() @qml.qnode(dev) def qfunc(x, y): qml.RZ(x, wires=0) qml.CNOT(wires=[0, 1]) qml.RY(y, wires=1) qml.CNOT(wires=[0, 2]) return qml.state() >>> qfunc(0.56, 0.1) array([0.95985437-0.27601028j, 0. +0.j , 0.04803275-0.01381203j, 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ])
Differentiating the state is currently available when using the classical backpropagation differentiation method (
diff_method="backprop"
) with a compatible device, and when using the new tape mode.
New operations and channels
PennyLane now includes standard channels such as the Amplitude-damping, Phase-damping, and Depolarizing channels, as well as the ability to make custom qubit channels. (#760) (#766) (#778)
The controlled-Y operation is now available via
qml.CY
. For devices that do not natively support the controlled-Y operation, it will be decomposed intoqml.RY
,qml.CNOT
, andqml.S
operations. (#806)
Preview the next-generation PennyLane QNode
The new PennyLane
tape
module provides a re-formulated QNode class, rewritten from the ground-up, that uses a newQuantumTape
object to represent the QNode’s quantum circuit. Tape mode provides several advantages over the standard PennyLane QNode. (#785) (#792) (#796) (#800) (#803) (#804) (#805) (#808) (#810) (#811) (#815) (#820) (#823) (#824) (#829)Support for in-QNode classical processing: Tape mode allows for differentiable classical processing within the QNode.
No more Variable wrapping: In tape mode, QNode arguments no longer become
Variable
objects within the QNode.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.
Unifying all QNodes: The tape-mode QNode merges all QNodes (including the
JacobianQNode
and thePassthruQNode
) into a single unified QNode, with identical behaviour regardless of the differentiation type.Optimizations: Tape mode provides various performance optimizations, reducing pre- and post-processing overhead, and reduces the number of quantum evaluations in certain cases.
Note that tape mode is experimental, and does not currently have feature-parity with the existing QNode. Feedback and bug reports are encouraged and will help improve the new tape mode.
Tape mode can be enabled globally via the
qml.enable_tape
function, without changing your PennyLane code:qml.enable_tape() dev = qml.device("default.qubit", wires=1) @qml.qnode(dev, interface="tf") def circuit(p): print("Parameter value:", p) qml.RX(tf.sin(p[0])**2 + p[1], wires=0) return qml.expval(qml.PauliZ(0))
For more details, please see the tape mode documentation.
Improvements
QNode caching has been introduced, allowing the QNode to keep track of the results of previous device executions and reuse those results in subsequent calls. Note that QNode caching is only supported in the new and experimental tape-mode. (#817)
Caching is available by passing a
caching
argument to the QNode:dev = qml.device("default.qubit", wires=2) qml.enable_tape() @qml.qnode(dev, caching=10) # cache up to 10 evaluations def qfunc(x): qml.RX(x, wires=0) qml.RX(0.3, wires=1) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(1)) qfunc(0.1) # first evaluation executes on the device qfunc(0.1) # second evaluation accesses the cached result
Sped up the application of certain gates in
default.qubit
by using array/tensor manipulation tricks. The following gates are affected:PauliX
,PauliY
,PauliZ
,Hadamard
,SWAP
,S
,T
,CNOT
,CZ
. (#772)The computation of marginal probabilities has been made more efficient for devices with a large number of wires, achieving in some cases a 5x speedup. (#799)
Adds arithmetic operations (addition, tensor product, subtraction, and scalar multiplication) between
Hamiltonian
,Tensor
, andObservable
objects, and inline arithmetic operations between Hamiltonians and other observables. (#765)Hamiltonians can now easily be defined as sums of observables:
>>> H = 3 * qml.PauliZ(0) - (qml.PauliX(0) @ qml.PauliX(1)) + qml.Hamiltonian([4], [qml.PauliZ(0)]) >>> print(H) (7.0) [Z0] + (-1.0) [X0 X1]
Adds
compare()
method toObservable
andHamiltonian
classes, which allows for comparison between observable quantities. (#765)>>> H = qml.Hamiltonian([1], [qml.PauliZ(0)]) >>> obs = qml.PauliZ(0) @ qml.Identity(1) >>> print(H.compare(obs)) True
>>> H = qml.Hamiltonian([2], [qml.PauliZ(0)]) >>> obs = qml.PauliZ(1) @ qml.Identity(0) >>> print(H.compare(obs)) False
Adds
simplify()
method to theHamiltonian
class. (#765)>>> H = qml.Hamiltonian([1, 2], [qml.PauliZ(0), qml.PauliZ(0) @ qml.Identity(1)]) >>> H.simplify() >>> print(H) (3.0) [Z0]
Added a new bit-flip mixer to the
qml.qaoa
module. (#774)Summation of two
Wires
objects is now supported and will return aWires
object containing the set of all wires defined by the terms in the summation. (#812)
Breaking changes
The PennyLane NumPy module now returns scalar (zero-dimensional) arrays where Python scalars were previously returned. (#820) (#833)
For example, this affects array element indexing, and summation:
>>> x = np.array([1, 2, 3], requires_grad=False) >>> x[0] tensor(1, requires_grad=False) >>> np.sum(x) tensor(6, requires_grad=True)
This may require small updates to user code. A convenience method,
np.tensor.unwrap()
, has been added to help ease the transition. This converts PennyLane NumPy tensors to standard NumPy arrays and Python scalars:>>> x = np.array(1.543, requires_grad=False) >>> x.unwrap() 1.543
Note, however, that information regarding array differentiability will be lost.
The device capabilities dictionary has been redesigned, for clarity and robustness. In particular, the capabilities dictionary is now inherited from the parent class, various keys have more expressive names, and all keys are now defined in the base device class. For more details, please refer to the developer documentation. (#781)
Bug fixes
Changed to use lists for storing variable values inside
BaseQNode
allowing complex matrices to be passed toQubitUnitary
. (#773)Fixed a bug within
default.qubit
, resulting in greater efficiency when applying a state vector to all wires on the device. (#849)
Documentation
Equations have been added to the
qml.sample
andqml.probs
docstrings to clarify the mathematical foundation of the performed measurements. (#843)
Contributors
This release contains contributions from (in alphabetical order):
Aroosa Ijaz, Juan Miguel Arrazola, Thomas Bromley, Jack Ceroni, Alain Delgado Gran, Josh Izaac, Soran Jahangiri, Nathan Killoran, Robert Lang, Cedric Lin, Olivia Di Matteo, Nicolás Quesada, Maria Schuld, Antal Száva.
- orphan
Release 0.11.0¶
New features since last release
New and improved simulators
Added a new device,
default.qubit.autograd
, a pure-state qubit simulator written using Autograd. This device supports classical backpropagation (diff_method="backprop"
); this can be faster than the parameter-shift rule for computing quantum gradients when the number of parameters to be optimized is large. (#721)>>> dev = qml.device("default.qubit.autograd", wires=1) >>> @qml.qnode(dev, 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 = np.array([0.2, 0.5