Release notes¶
This page contains the release notes for PennyLane.
- orphan
Release 0.30.0¶
New features since last release
Pulse programming on hardware ⚛️🔬
Support for loading time-dependent Hamiltonians that are compatible with quantum hardware has been added, making it possible to load a Hamiltonian that describes an ensemble of Rydberg atoms or a collection of transmon qubits. (#3749) (#3911) (#3930) (#3936) (#3966) (#3987) (#4021) (#4040)
Rydberg atoms are the foundational unit for neutral atom quantum computing. A Rydberg-system Hamiltonian can be constructed from a drive term —
qml.pulse.rydberg_drive
— and an interaction term —qml.pulse.rydberg_interaction
:from jax import numpy as jnp atom_coordinates = [[0, 0], [0, 4], [4, 0], [4, 4]] wires = [0, 1, 2, 3] amplitude = lambda p, t: p * jnp.sin(jnp.pi * t) phase = jnp.pi / 2 detuning = 3 * jnp.pi / 4 H_d = qml.pulse.rydberg_drive(amplitude, phase, detuning, wires) H_i = qml.pulse.rydberg_interaction(atom_coordinates, wires) H = H_d + H_i
The time-dependent Hamiltonian
H
can be used in a PennyLane pulse-level differentiable circuit:dev = qml.device("default.qubit.jax", wires=wires) @qml.qnode(dev, interface="jax") def circuit(params): qml.evolve(H)(params, t=[0, 10]) return qml.expval(qml.PauliZ(0))
>>> params = jnp.array([2.4]) >>> circuit(params) Array(0.6316659, dtype=float32) >>> import jax >>> jax.grad(circuit)(params) Array([1.3116529], dtype=float32)
The qml.pulse page contains additional details. Check out our release blog post for a demonstration of how to perform the execution on actual hardware!
A pulse-level circuit can now be differentiated using a stochastic parameter-shift method. (#3780) (#3900) (#4000) (#4004)
The new qml.gradient.stoch_pulse_grad differentiation method unlocks stochastic-parameter-shift differentiation for pulse-level circuits. The current version of this new method is restricted to Hamiltonians composed of parametrized Pauli words, but future updates to extend to parametrized Pauli sentences can allow this method to be compatible with hardware-based systems such as an ensemble of Rydberg atoms.
This method can be activated by setting
diff_method
to qml.gradient.stoch_pulse_grad:>>> dev = qml.device("default.qubit.jax", wires=2) >>> sin = lambda p, t: jax.numpy.sin(p * t) >>> ZZ = qml.PauliZ(0) @ qml.PauliZ(1) >>> H = 0.5 * qml.PauliX(0) + qml.pulse.constant * ZZ + sin * qml.PauliX(1) >>> @qml.qnode(dev, interface="jax", diff_method=qml.gradients.stoch_pulse_grad) >>> def ansatz(params): ... qml.evolve(H)(params, (0.2, 1.)) ... return qml.expval(qml.PauliY(1)) >>> params = [jax.numpy.array(0.4), jax.numpy.array(1.3)] >>> jax.grad(ansatz)(params) [Array(0.16921353, dtype=float32, weak_type=True), Array(-0.2537478, dtype=float32, weak_type=True)]
Quantum singular value transformation 🐛➡️🦋
PennyLane now supports the quantum singular value transformation (QSVT), which describes how a quantum circuit can be constructed to apply a polynomial transformation to the singular values of an input matrix. (#3756) (#3757) (#3758) (#3905) (#3909) (#3926) (#4023)
Consider a matrix
A
along with a vectorangles
that describes the target polynomial transformation. Theqml.qsvt
function creates a corresponding circuit:dev = qml.device("default.qubit", wires=2) A = np.array([[0.1, 0.2], [0.3, 0.4]]) angles = np.array([0.1, 0.2, 0.3]) @qml.qnode(dev) def example_circuit(A): qml.qsvt(A, angles, wires=[0, 1]) return qml.expval(qml.PauliZ(wires=0))
This circuit is composed of
qml.BlockEncode
andqml.PCPhase
operations.>>> example_circuit(A) tensor(0.97777078, requires_grad=True) >>> print(example_circuit.qtape.expand(depth=1).draw(decimals=2)) 0: ─╭∏_ϕ(0.30)─╭BlockEncode(M0)─╭∏_ϕ(0.20)─╭BlockEncode(M0)†─╭∏_ϕ(0.10)─┤ <Z> 1: ─╰∏_ϕ(0.30)─╰BlockEncode(M0)─╰∏_ϕ(0.20)─╰BlockEncode(M0)†─╰∏_ϕ(0.10)─┤
The qml.qsvt function creates a circuit that is targeted at simulators due to the use of matrix-based operations. For advanced users, you can use the operation-based
qml.QSVT
template to perform the transformation with a custom choice of unitary and projector operations, which may be hardware compatible if a decomposition is provided.The QSVT is a complex but powerful transformation capable of generalizing important algorithms like amplitude amplification. Stay tuned for a demo in the coming few weeks to learn more!
Intuitive QNode returns ↩️
An updated QNode return system has been introduced. PennyLane QNodes now return exactly what you tell them to! 🎉 (#3957) (#3969) (#3946) (#3913) (#3914) (#3934)
This was an experimental feature introduced in version 0.25 of PennyLane that was enabled via
qml.enable_return()
. Now, it’s the default return system. Let’s see how it works.Consider the following circuit:
import pennylane as qml dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) return qml.expval(qml.PauliZ(0)), qml.probs(0)
In version 0.29 and earlier of PennyLane,
circuit()
would return a single length-3 array:>>> circuit(0.5) tensor([0.87758256, 0.93879128, 0.06120872], requires_grad=True)
In versions 0.30 and above,
circuit()
returns a length-2 tuple containing the expectation value and probabilities separately:>>> circuit(0.5) (tensor(0.87758256, requires_grad=True), tensor([0.93879128, 0.06120872], requires_grad=True))
You can find more details about this change, along with help and troubleshooting tips to solve any issues. If you still have questions, comments, or concerns, we encourage you to post on the PennyLane discussion forum.
A bunch of performance tweaks 🏃💨
Single-qubit operations that have multi-qubit control can now be decomposed more efficiently using fewer CNOT gates. (#3851)
Three decompositions from arXiv:2302.06377 are provided and compare favourably to the already-available
qml.ops.ctrl_decomp_zyz
:wires = [0, 1, 2, 3, 4, 5] control_wires = wires[1:] @qml.qnode(qml.device('default.qubit', wires=6)) def circuit(): with qml.QueuingManager.stop_recording(): # the decomposition does not un-queue the target target = qml.RX(np.pi/2, wires=0) qml.ops.ctrl_decomp_bisect(target, (1, 2, 3, 4, 5)) return qml.state() print(qml.draw(circuit, expansion_strategy="device")())
0: ──H─╭X──U(M0)─╭X──U(M0)†─╭X──U(M0)─╭X──U(M0)†──H─┤ State 1: ────├●────────│──────────├●────────│─────────────┤ State 2: ────├●────────│──────────├●────────│─────────────┤ State 3: ────╰●────────│──────────╰●────────│─────────────┤ State 4: ──────────────├●───────────────────├●────────────┤ State 5: ──────────────╰●───────────────────╰●────────────┤ State
A new decomposition to
qml.SingleExcitation
has been added that halves the number of CNOTs required. (3976)>>> qml.SingleExcitation.compute_decomposition(1.23, wires=(0,1)) [Adjoint(T(wires=[0])), Hadamard(wires=[0]), S(wires=[0]), Adjoint(T(wires=[1])), Adjoint(S(wires=[1])), Hadamard(wires=[1]), CNOT(wires=[1, 0]), RZ(-0.615, wires=[0]), RY(0.615, wires=[1]), CNOT(wires=[1, 0]), Adjoint(S(wires=[0])), Hadamard(wires=[0]), T(wires=[0]), Hadamard(wires=[1]), S(wires=[1]), T(wires=[1])]
The adjoint differentiation method can now be more efficient, avoiding the decomposition of operations that can be differentiated directly. Any operation that defines a
generator()
can be differentiated with the adjoint method. (#3874)For example, in version 0.29 the
qml.CRY
operation would be decomposed when calculating the adjoint-method gradient. Executing the code below shows that this decomposition no longer takes place in version 0.30 andqml.CRY
is differentiated directly:import jax from jax import numpy as jnp def compute_decomposition(self, phi, wires): print("A decomposition has been performed!") decomp_ops = [ qml.RY(phi / 2, wires=wires[1]), qml.CNOT(wires=wires), qml.RY(-phi / 2, wires=wires[1]), qml.CNOT(wires=wires), ] return decomp_ops qml.CRY.compute_decomposition = compute_decomposition dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, diff_method="adjoint") def circuit(phi): qml.Hadamard(wires=0) qml.CRY(phi, wires=[0, 1]) return qml.expval(qml.PauliZ(1)) phi = jnp.array(0.5) jax.grad(circuit)(phi)
Derivatives are computed more efficiently when using
jax.jit
with gradient transforms; the trainable parameters are now set correctly instead of every parameter having to be set as trainable. (#3697)In the circuit below, only the derivative with respect to parameter
b
is now calculated:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, interface="jax-jit") def circuit(a, b): qml.RX(a, wires=0) qml.RY(b, wires=0) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) a = jnp.array(0.4) b = jnp.array(0.5) jac = jax.jacobian(circuit, argnums=[1]) jac_jit = jax.jit(jac) jac_jit(a, b) assert len(circuit.tape.trainable_params) == 1
Improvements 🛠
Next-generation device API
In this release and future releases, we will be making changes to our device API with the goal in mind to make developing plugins much easier for developers and unlock new device capabilities. Users shouldn’t yet feel any of these changes when using PennyLane, but here is what has changed this release:
Several functions in
devices/qubit
have been added or improved:sample_state
: returns a series of samples based on a given state vector and a number of shots. (#3720)simulate
: supports measuring expectation values of large observables such asqml.Hamiltonian
,qml.SparseHamiltonian
, andqml.Sum
. (#3759)apply_operation
: supports broadcasting. (#3852)adjoint_jacobian
: supports adjoint differentiation in the new qubit state-vector device. (#3790)
qml.devices.qubit.preprocess
now allows circuits with non-commuting observables. (#3857)qml.devices.qubit.measure
now computes the expectation values ofHamiltonian
andSum
in a backpropagation-compatible way. (#3862)
Pulse programming
Here are the functions, classes, and more that were added or improved to facilitate simulating ensembles of Rydberg atoms: (#3749) (#3911) (#3930) (#3936) (#3966) (#3987) (#3889) (#4021)
HardwareHamiltonian
: an internal class that contains additional information about pulses and settings.rydberg_interaction
: a user-facing function that returns aHardwareHamiltonian
containing the Hamiltonian of the interaction of all the Rydberg atoms.transmon_interaction
: a user-facing function for constructing the Hamiltonian that describes the circuit QED interaction Hamiltonian of superconducting transmon systems.drive
: a user-facing function function that returns aParametrizedHamiltonian
(HardwareHamiltonian
) containing the Hamiltonian of the interaction between a driving electro-magnetic field and a group of qubits.rydberg_drive
: a user-facing function that returns aParametrizedHamiltonian
(HardwareHamiltonian
) containing the Hamiltonian of the interaction between a driving laser field and a group of Rydberg atoms.max_distance
: a keyword argument added toqml.pulse.rydberg_interaction
to allow for the removal of negligible contributions from atoms beyondmax_distance
from each other.
ParametrizedEvolution
now takes two new Boolean keyword arguments:return_intermediate
andcomplementary
. They allow computing intermediate time evolution matrices. (#3900)Activating
return_intermediate
will return intermediate time evolution steps, for example for the matrix of the Operation, or of a quantum circuit when used in a QNode. Activatingcomplementary
will make these intermediate steps be the remaining time evolution complementary to the output forcomplementary=False
. See the docstring for details.Hardware-compatible pulse sequence gradients with
qml.gradient.stoch_pulse_grad
can now be calculated faster using the new keyword argumentuse_broadcasting
. Executing aParametrizedEvolution
that returns intermediate evolutions has increased performance using the state vector ODE solver, as well. (#4000) (#4004)
Intuitive QNode returns
The QNode keyword argument
mode
has been replaced by the booleangrad_on_execution
. (#3969)The
"default.gaussian"
device and parameter-shift CV both support the new return system, but only for single measurements. (#3946)Keras and Torch NN modules are now compatible with the new return type system. (#3913) (#3914)
DefaultQutrit
now supports the new return system. (#3934)
Performance improvements
The efficiency of
tapering()
,tapering_hf()
andclifford()
have been improved. (3942)The peak memory requirements of
tapering()
andtapering_hf()
have been improved when used for larger observables. (3977)Pauli arithmetic has been updated to convert to a Hamiltonian more efficiently. (#3939)
Operator
has a new Boolean attributehas_generator
. It returns whether or not theOperator
has agenerator
defined.has_generator
is used inqml.operation.has_gen
, which improves its performance and extends differentiation support. (#3875)The performance of
CompositeOp
has been significantly improved now that it overrides determining whether it is being used with a batch of parameters (seeOperator._check_batching
).Hamiltonian
also now overrides this, but it does nothing since it does not support batching. (#3915)The performance of a
Sum
operator has been significantly improved now thatis_hermitian
checks that all coefficients are real if the operator has a pre-computed Pauli representation. (#3915)The
coefficients
function and thevisualize
submodule of theqml.fourier
module now allow assigning different degrees for different parameters of the input function. (#3005)Previously, the arguments
degree
andfilter_threshold
toqml.fourier.coefficients
were expected to be integers. Now, they can be a sequences of integers with one integer per function parameter (i.e.len(degree)==n_inputs
), resulting in a returned array with shape(2*degrees[0]+1,..., 2*degrees[-1]+1)
. The functions inqml.fourier.visualize
accordingly accept such arrays of coefficients.
Other improvements
A
Shots
class has been added to themeasurements
module to hold shot-related data. (#3682)The custom JVP rules in PennyLane also now support non-scalar and mixed-shape tape parameters as well as multi-dimensional tape return types, like broadcasted
qml.probs
, for example. (#3766)The
qchem.jordan_wigner
function has been extended to support more fermionic operator orders. (#3754) (#3751)The
AdaptiveOptimizer
has been updated to use non-default user-defined QNode arguments. (#3765)Operators now use
TensorLike
types dunder methods. (#3749)qml.QubitStateVector.state_vector
now supports broadcasting. (#3852)qml.SparseHamiltonian
can now be applied to any wires in a circuit rather than being restricted to all wires in the circuit. (#3888)Operators can now be divided by scalars with
/
with the addition of theOperation.__truediv__
dunder method. (#3749)Printing an instance of
MutualInfoMP
now displays the distribution of the wires between the two subsystems. (#3898)Operator.num_wires
has been changed from an abstract value toAnyWires
. (#3919)qml.transforms.sum_expand
is not run inDevice.batch_transform
if the device supportsSum
observables. (#3915)The type of
n_electrons
inqml.qchem.Molecule
has been set toint
. (#3885)Explicit errors have been added to
QutritDevice
ifclassical_shadow
orshadow_expval
is measured. (#3934)QubitDevice
now defines the private_get_diagonalizing_gates(circuit)
method and uses it when executing circuits. This allows devices that inherit fromQubitDevice
to override and customize their definition of diagonalizing gates. (#3938)retworkx
has been renamed torustworkx
to accommodate the change in the package name. (#3975)Exp
,Sum
,Prod
, andSProd
operator data is now a flat list instead of nested. (#3958) (#3983)qml.transforms.convert_to_numpy_parameters
has been added to convert a circuit with interface-specific parameters to one with only numpy parameters. This transform is designed to replaceqml.tape.Unwrap
. (#3899)qml.operation.WiresEnum.AllWires
is now -2 instead of 0 to avoid the ambiguity betweenop.num_wires = 0
andop.num_wires = AllWires
. (#3978)Execution code has been updated to use the new
qml.transforms.convert_to_numpy_parameters
instead ofqml.tape.Unwrap
. (#3989)A sub-routine of
expand_tape
has been converted intoqml.tape.tape.rotations_and_diagonal_measurements
, a helper function that computes rotations and diagonal measurements for a tape with measurements with overlapping wires. (#3912)Various operators and templates have been updated to ensure that their decompositions only return lists of operators. (#3243)
The
qml.operation.enable_new_opmath
toggle has been introduced to cause dunder methods to return arithmetic operators instead of aHamiltonian
orTensor
. (#4008)>>> type(qml.PauliX(0) @ qml.PauliZ(1)) <class 'pennylane.operation.Tensor'> >>> qml.operation.enable_new_opmath() >>> type(qml.PauliX(0) @ qml.PauliZ(1)) <class 'pennylane.ops.op_math.prod.Prod'> >>> qml.operation.disable_new_opmath() >>> type(qml.PauliX(0) @ qml.PauliZ(1)) <class 'pennylane.operation.Tensor'>
A new data class called
Resources
has been added to store resources like the number of gates and circuit depth throughout a quantum circuit. (#3981)A new function called
_count_resources()
has been added to count the resources required when executing aQuantumTape
for a given number of shots. (#3996)QuantumScript.specs
has been modified to make use of the newResources
class. This also modifies the output ofqml.specs()
. (#4015)A new class called
ResourcesOperation
has been added to allow users to define operations with custom resource information. (#4026)For example, users can define a custom operation by inheriting from this new class:
>>> class CustomOp(qml.resource.ResourcesOperation): ... def resources(self): ... return qml.resource.Resources(num_wires=1, num_gates=2, ... gate_types={"PauliX": 2}) ... >>> CustomOp(wires=1) CustomOp(wires=[1])
Then, we can track and display the resources of the workflow using
qml.specs()
:>>> dev = qml.device("default.qubit", wires=[0,1]) >>> @qml.qnode(dev) ... def circ(): ... qml.PauliZ(wires=0) ... CustomOp(wires=1) ... return qml.state() ... >>> print(qml.specs(circ)()['resources']) wires: 2 gates: 3 depth: 1 shots: 0 gate_types: {'PauliZ': 1, 'PauliX': 2}
MeasurementProcess.shape
now accepts aShots
object as one of its arguments to reduce exposure to unnecessary execution details. (#4012)
Breaking changes 💔
The
seed_recipes
argument has been removed fromqml.classical_shadow
andqml.shadow_expval
. (#4020)The tape method
get_operation
has an updated signature. (#3998)Both JIT interfaces are no longer compatible with JAX
>0.4.3
(we raise an error for those versions). (#3877)An operation that implements a custom
generator
method, but does not always return a valid generator, also has to implement ahas_generator
property that reflects in which scenarios a generator will be returned. (#3875)Trainable parameters for the Jax interface are the parameters that are
JVPTracer
, defined by settingargnums
. Previously, all JAX tracers, including those used for JIT compilation, were interpreted to be trainable. (#3697)The keyword argument
argnums
is now used for gradient transforms using Jax instead ofargnum
.argnum
is automatically converted toargnums
when using Jax and will no longer be supported in v0.31 of PennyLane. (#3697) (#3847)qml.OrbitalRotation
and, consequently,qml.GateFabric
are now more consistent with the interleaved Jordan-Wigner ordering. Previously, they were consistent with the sequential Jordan-Wigner ordering. (#3861)Some
MeasurementProcess
classes can now only be instantiated with arguments that they will actually use. For example, you can no longer createStateMP(qml.PauliX(0))
orPurityMP(eigvals=(-1,1), wires=Wires(0))
. (#3898)Exp
,Sum
,Prod
, andSProd
operator data is now a flat list, instead of nested. (#3958) (#3983)qml.tape.tape.expand_tape
and, consequentially,QuantumScript.expand
no longer update the input tape with rotations and diagonal measurements. Note that the newly expanded tape that is returned will still have the rotations and diagonal measurements. (#3912)qml.Evolution
now initializes the coefficient with a factor of-1j
instead of1j
. (#4024)
Deprecations 👋
Nothing for this release!
Documentation 📝
The documentation of
QubitUnitary
andDiagonalQubitUnitary
was clarified regarding the parameters of the operations. (#4031)A typo has been corrected in the documentation for the introduction to
inspecting_circuits
andchemistry
. (#3844)Usage Details
andTheory
sections have been separated in the documentation forqml.qchem.taper_operation
. (3977)
Bug fixes 🐛
ctrl_decomp_bisect
andctrl_decomp_zyz
are no longer used by default when decomposing controlled operations due to the presence of a global phase difference in the zyz decomposition of some target operators. (#4065)Fixed a bug where
qml.math.dot
returned a numpy array instead of an autograd array, breaking autograd derivatives in certain circumstances. (#4019)Operators now cast a
tuple
to annp.ndarray
as well aslist
. (#4022)Fixed a bug where
qml.ctrl
with parametric gates was incompatible with PyTorch tensors on GPUs. (#4002)Fixed a bug where the broadcast expand results were stacked along the wrong axis for the new return system. (#3984)
A more informative error message is raised in
qml.jacobian
to explain potential problems with the new return types specification. (#3997)Fixed a bug where calling
Evolution.generator
withcoeff
being a complex ArrayBox raised an error. (#3796)MeasurementProcess.hash
now uses the hash property of the observable. The property now depends on all properties that affect the behaviour of the object, such asVnEntropyMP.log_base
or the distribution of wires between the two subsystems inMutualInfoMP
. (#3898)The enum
measurements.Purity
has been added so thatPurityMP.return_type
is defined.str
andrepr
forPurityMP
are also now defined. (#3898)Sum.hash
andProd.hash
have been changed slightly to work with non-numeric wire labels.sum_expand
should now return correct results and not treat some products as the same operation. (#3898)Fixed bug where the coefficients where not ordered correctly when summing a
ParametrizedHamiltonian
with other operators. (#3749) (#3902)The metric tensor transform is now fully compatible with Jax and therefore users can provide multiple parameters. (#3847)
qml.math.ndim
andqml.math.shape
are now registered for built-ins and autograd to accomodate Autoray 0.6.1. #3864Ensured that
qml.data.load
returns datasets in a stable and expected order. (#3856)The
qml.equal
function now handles comparisons ofParametrizedEvolution
operators. (#3870)qml.devices.qubit.apply_operation
catches thetf.errors.UnimplementedError
that occurs whenPauliZ
orCNOT
gates are applied to a large (>8 wires) tensorflow state. When that occurs, the logic falls back to the tensordot logic instead. (#3884)Fixed parameter broadcasting support with
qml.counts
in most cases and introduced explicit errors otherwise. (#3876)An error is now raised if a QNode with Jax-jit in use returns
counts
while having trainable parameters (#3892)A correction has been added to the reference values in
test_dipole_of
to account for small changes (~2e-8
) in the computed dipole moment values resulting from the new PySCF 2.2.0 release. (#3908)SampleMP.shape
is now correct when sampling only occurs on a subset of the device wires. (#3921)An issue has been fixed in
qchem.Molecule
to allow basis sets other than the hard-coded ones to be used in theMolecule
class. (#3955)Fixed bug where all devices that inherit from
DefaultQubit
claimed to supportParametrizedEvolution
. Now, onlyDefaultQubitJax
supports the operator, as expected. (#3964)Ensured that parallel
AnnotatedQueues
do not queue each other’s contents. (#3924)Added a
map_wires
method toPauliWord
andPauliSentence
, and ensured that operators call it in their respectivemap_wires
methods if they have a Pauli rep. (#3985)Fixed a bug when a
Tensor
is multiplied by aHamiltonian
or vice versa. (#4036)
Contributors ✍️
This release contains contributions from (in alphabetical order):
Komi Amiko, Utkarsh Azad, Thomas Bromley, Isaac De Vlugt, Olivia Di Matteo, Lillian M. A. Frederiksen, Diego Guala, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Vincent Michaud-Rioux, Albert Mitjans Coma, Romain Moyard, Lee J. O’Riordan, Mudit Pandey, Matthew Silverman, Jay Soni, David Wierichs.
- orphan
Release 0.29.0¶
New features since last release
Pulse programming 🔊
Support for creating pulse-based circuits that describe evolution under a time-dependent Hamiltonian has now been added, as well as the ability to execute and differentiate these pulse-based circuits on simulator. (#3586) (#3617) (#3645) (#3652) (#3665) (#3673) (#3706) (#3730)
A time-dependent Hamiltonian can be created using
qml.pulse.ParametrizedHamiltonian
, which holds information representing a linear combination of operators with parametrized coefficents and can be constructed as follows:from jax import numpy as jnp f1 = lambda p, t: p * jnp.sin(t) * (t - 1) f2 = lambda p, t: p[0] * jnp.cos(p[1]* t ** 2) XX = qml.PauliX(0) @ qml.PauliX(1) YY = qml.PauliY(0) @ qml.PauliY(1) ZZ = qml.PauliZ(0) @ qml.PauliZ(1) H = 2 * ZZ + f1 * XX + f2 * YY
>>> H ParametrizedHamiltonian: terms=3 >>> p1 = jnp.array(1.2) >>> p2 = jnp.array([2.3, 3.4]) >>> H((p1, p2), t=0.5) (2*(PauliZ(wires=[0]) @ PauliZ(wires=[1]))) + ((-0.2876553231625218*(PauliX(wires=[0]) @ PauliX(wires=[1]))) + (1.517961235535459*(PauliY(wires=[0]) @ PauliY(wires=[1]))))
The time-dependent Hamiltonian can be used within a circuit with
qml.evolve
:def pulse_circuit(params, time): qml.evolve(H)(params, time) return qml.expval(qml.PauliX(0) @ qml.PauliY(1))
Pulse-based circuits can be executed and differentiated on the
default.qubit.jax
simulator using JAX as an interface:>>> dev = qml.device("default.qubit.jax", wires=2) >>> qnode = qml.QNode(pulse_circuit, dev, interface="jax") >>> params = (p1, p2) >>> qnode(params, time=0.5) Array(0.72153819, dtype=float64) >>> jax.grad(qnode)(params, time=0.5) (Array(-0.11324919, dtype=float64), Array([-0.64399616, 0.06326374], dtype=float64))
Check out the qml.pulse documentation page for more details!
Special unitary operation 🌞
A new operation
qml.SpecialUnitary
has been added, providing access to an arbitrary unitary gate via a parametrization in the Pauli basis. (#3650) (#3651) (#3674)qml.SpecialUnitary
creates a unitary that exponentiates a linear combination of all possible Pauli words in lexicographical order — except for the identity operator — fornum_wires
wires, of which there are4**num_wires - 1
. As its first argument,qml.SpecialUnitary
takes a list of the4**num_wires - 1
parameters that are the coefficients of the linear combination.To see all possible Pauli words for
num_wires
wires, you can use theqml.ops.qubit.special_unitary.pauli_basis_strings
function:>>> qml.ops.qubit.special_unitary.pauli_basis_strings(1) # 4**1-1 = 3 Pauli words ['X', 'Y', 'Z'] >>> qml.ops.qubit.special_unitary.pauli_basis_strings(2) # 4**2-1 = 15 Pauli words ['IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ']
To use
qml.SpecialUnitary
, for example, on a single qubit, we may define>>> thetas = np.array([0.2, 0.1, -0.5]) >>> U = qml.SpecialUnitary(thetas, 0) >>> qml.matrix(U) array([[ 0.8537127 -0.47537233j, 0.09507447+0.19014893j], [-0.09507447+0.19014893j, 0.8537127 +0.47537233j]])
A single non-zero entry in the parameters will create a Pauli rotation:
>>> x = 0.412 >>> theta = x * np.array([1, 0, 0]) # The first entry belongs to the Pauli word "X" >>> su = qml.SpecialUnitary(theta, wires=0) >>> rx = qml.RX(-2 * x, 0) # RX introduces a prefactor -0.5 that has to be compensated >>> qml.math.allclose(qml.matrix(su), qml.matrix(rx)) True
This operation can be differentiated with hardware-compatible methods like parameter shifts and it supports parameter broadcasting/batching, but not both at the same time. Learn more by visiting the qml.SpecialUnitary documentation.
Always differentiable 📈
The Hadamard test gradient transform is now available via
qml.gradients.hadamard_grad
. This transform is also available as a differentiation method withinQNode
s. (#3625) (#3736)qml.gradients.hadamard_grad
is a hardware-compatible transform that calculates the gradient of a quantum circuit using the Hadamard test. Note that the device requires an auxiliary wire to calculate the gradient.>>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev) ... def circuit(params): ... qml.RX(params[0], wires=0) ... qml.RY(params[1], wires=0) ... qml.RX(params[2], wires=0) ... return qml.expval(qml.PauliZ(0)) >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True) >>> qml.gradients.hadamard_grad(circuit)(params) (tensor(-0.3875172, requires_grad=True), tensor(-0.18884787, requires_grad=True), tensor(-0.38355704, requires_grad=True))
This transform can be registered directly as the quantum gradient transform to use during autodifferentiation:
>>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev, interface="jax", diff_method="hadamard") ... def circuit(params): ... qml.RX(params[0], wires=0) ... qml.RY(params[1], wires=0) ... qml.RX(params[2], wires=0) ... return qml.expval(qml.PauliZ(0)) >>> params = jax.numpy.array([0.1, 0.2, 0.3]) >>> jax.jacobian(circuit)(params) Array([-0.3875172 , -0.18884787, -0.38355705], dtype=float32)
The gradient transform
qml.gradients.spsa_grad
is now registered as a differentiation method for QNodes. (#3440)The SPSA gradient transform can now be used implicitly by marking a QNode as differentiable with SPSA. It can be selected via
>>> dev = qml.device("default.qubit", wires=1) >>> @qml.qnode(dev, interface="jax", diff_method="spsa", h=0.05, num_directions=20) ... def circuit(x): ... qml.RX(x, 0) ... return qml.expval(qml.PauliZ(0)) >>> jax.jacobian(circuit)(jax.numpy.array(0.5)) Array(-0.4792258, dtype=float32, weak_type=True)
The argument
num_directions
determines how many directions of simultaneous perturbation are used and therefore the number of circuit evaluations, up to a prefactor. See the SPSA gradient transform documentation for details. Note: The full SPSA optimization method is already available asqml.SPSAOptimizer
.The default interface is now
auto
. There is no need to specify the interface anymore; it is automatically determined by checking your QNode parameters. (#3677) (#3752) (#3829)import jax import jax.numpy as jnp qml.enable_return() a = jnp.array(0.1) b = jnp.array(0.2) dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(a, b): qml.RY(a, wires=0) qml.RX(b, wires=1) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))
>>> circuit(a, b) (Array(0.9950042, dtype=float32), Array(-0.19767681, dtype=float32)) >>> jac = jax.jacobian(circuit)(a, b) >>> jac (Array(-0.09983341, dtype=float32, weak_type=True), Array(0.01983384, dtype=float32, weak_type=True))
The JAX-JIT interface now supports higher-order gradient computation with the new return types system. (#3498)
Here is an example of using JAX-JIT to compute the Hessian of a circuit:
import pennylane as qml import jax from jax import numpy as jnp jax.config.update("jax_enable_x64", True) qml.enable_return() dev = qml.device("default.qubit", wires=2) @jax.jit @qml.qnode(dev, interface="jax-jit", diff_method="parameter-shift", max_diff=2) def circuit(a, b): qml.RY(a, wires=0) qml.RX(b, wires=1) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) a, b = jnp.array(1.0), jnp.array(2.0)
>>> jax.hessian(circuit, argnums=[0, 1])(a, b) (((Array(-0.54030231, dtype=float64, weak_type=True), Array(0., dtype=float64, weak_type=True)), (Array(-1.76002563e-17, dtype=float64, weak_type=True), Array(0., dtype=float64, weak_type=True))), ((Array(0., dtype=float64, weak_type=True), Array(-1.00700085e-17, dtype=float64, weak_type=True)), (Array(0., dtype=float64, weak_type=True), Array(0.41614684, dtype=float64, weak_type=True))))
The
qchem
workflow has been modified to support both Autograd and JAX frameworks. (#3458) (#3462) (#3495)The JAX interface is automatically used when the differentiable parameters are JAX objects. Here is an example for computing the Hartree-Fock energy gradients with respect to the atomic coordinates.
import pennylane as qml from pennylane import numpy as np import jax 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 = [jax.numpy.array(mol.coordinates)]
>>> jax.grad(qml.qchem.hf_energy(mol))(*args) Array([[ 0. , 0. , 0.3650435], [ 0. , 0. , -0.3650435]], dtype=float64)
The kernel matrix utility functions in
qml.kernels
are now autodifferentiation-compatible. In addition, they support batching, for example for quantum kernel execution with shot vectors. (#3742)This allows for the following:
dev = qml.device('default.qubit', wires=2, shots=(100, 100)) @qml.qnode(dev) def circuit(x1, x2): qml.templates.AngleEmbedding(x1, wires=dev.wires) qml.adjoint(qml.templates.AngleEmbedding)(x2, wires=dev.wires) return qml.probs(wires=dev.wires) kernel = lambda x1, x2: circuit(x1, x2)
We can then compute the kernel matrix on a set of 4 (random) feature vectors
X
but using two sets of 100 shots each via>>> X = np.random.random((4, 2)) >>> qml.kernels.square_kernel_matrix(X, kernel)[:, 0] tensor([[[1. , 0.86, 0.88, 0.92], [0.86, 1. , 0.75, 0.97], [0.88, 0.75, 1. , 0.91], [0.92, 0.97, 0.91, 1. ]], [[1. , 0.93, 0.91, 0.92], [0.93, 1. , 0.8 , 1. ], [0.91, 0.8 , 1. , 0.91], [0.92, 1. , 0.91, 1. ]]], requires_grad=True)
Note that we have extracted the first probability vector entry for each 100-shot evaluation.
Smartly decompose Hamiltonian evolution 💯
Hamiltonian evolution using
qml.evolve
orqml.exp
can now be decomposed into operations. (#3691) (#3777)If the time-evolved Hamiltonian is equivalent to another PennyLane operation, then that operation is returned as the decomposition:
>>> exp_op = qml.evolve(qml.PauliX(0) @ qml.PauliX(1)) >>> exp_op.decomposition() [IsingXX((2+0j), wires=[0, 1])]
If the Hamiltonian is a Pauli word, then the decomposition is provided as a
qml.PauliRot
operation:>>> qml.evolve(qml.PauliZ(0) @ qml.PauliX(1)).decomposition() [PauliRot((2+0j), ZX, wires=[0, 1])]
Otherwise, the Hamiltonian is a linear combination of operators and the Suzuki-Trotter decomposition is used:
>>> qml.evolve(qml.sum(qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0)), num_steps=2).decomposition() [RX((1+0j), wires=[0]), RY((1+0j), wires=[0]), RZ((1+0j), wires=[0]), RX((1+0j), wires=[0]), RY((1+0j), wires=[0]), RZ((1+0j), wires=[0])]
Tools for quantum chemistry and other applications 🛠️
A new method called
qml.qchem.givens_decomposition
has been added, which decomposes a unitary into a sequence of Givens rotation gates with phase shifts and a diagonal phase matrix. (#3573)unitary = np.array([[ 0.73678+0.27511j, -0.5095 +0.10704j, -0.06847+0.32515j], [-0.21271+0.34938j, -0.38853+0.36497j, 0.61467-0.41317j], [ 0.41356-0.20765j, -0.00651-0.66689j, 0.32839-0.48293j]]) phase_mat, ordered_rotations = qml.qchem.givens_decomposition(unitary)
>>> phase_mat tensor([-0.20604358+0.9785369j , -0.82993272+0.55786114j, 0.56230612-0.82692833j], requires_grad=True) >>> ordered_rotations [(tensor([[-0.65087861-0.63937521j, -0.40933651-0.j ], [-0.29201359-0.28685265j, 0.91238348-0.j ]], requires_grad=True), (0, 1)), (tensor([[ 0.47970366-0.33308926j, -0.8117487 -0.j ], [ 0.66677093-0.46298215j, 0.5840069 -0.j ]], requires_grad=True), (1, 2)), (tensor([[ 0.36147547+0.73779454j, -0.57008306-0.j ], [ 0.2508207 +0.51194108j, 0.82158706-0.j ]], requires_grad=True), (0, 1))]
A new template called
qml.BasisRotation
has been added, which performs a basis transformation defined by a set of fermionic ladder operators. (#3573)import pennylane as qml from pennylane import numpy as np V = np.array([[ 0.53672126+0.j , -0.1126064 -2.41479668j], [-0.1126064 +2.41479668j, 1.48694623+0.j ]]) eigen_vals, eigen_vecs = np.linalg.eigh(V) umat = eigen_vecs.T wires = range(len(umat)) def circuit(): qml.adjoint(qml.BasisRotation(wires=wires, unitary_matrix=umat)) for idx, eigenval in enumerate(eigen_vals): qml.RZ(eigenval, wires=[idx]) qml.BasisRotation(wires=wires, unitary_matrix=umat)
>>> circ_unitary = qml.matrix(circuit)() >>> np.round(circ_unitary/circ_unitary[0][0], 3) tensor([[ 1. -0.j , -0. +0.j , -0. +0.j , -0. +0.j ], [-0. +0.j , -0.516-0.596j, -0.302-0.536j, -0. +0.j ], [-0. +0.j , 0.35 +0.506j, -0.311-0.724j, -0. +0.j ], [-0. +0.j , -0. +0.j , -0. +0.j , -0.438+0.899j]], requires_grad=True)
A new function called
qml.qchem.load_basisset
has been added to extractqml.qchem
basis set data from the Basis Set Exchange library. (#3363)A new function called
qml.math.max_entropy
has been added to compute the maximum entropy of a quantum state. (#3594)A new template called
qml.TwoLocalSwapNetwork
has been added that implements a canonical 2-complete linear (2-CCL) swap network described in arXiv:1905.05118. (#3447)dev = qml.device('default.qubit', wires=5) weights = np.random.random(size=qml.templates.TwoLocalSwapNetwork.shape(len(dev.wires))) acquaintances = lambda index, wires, param: (qml.CRY(param, wires=index) if np.abs(wires[0]-wires[1]) else qml.CRZ(param, wires=index)) @qml.qnode(dev) def swap_network_circuit(): qml.templates.TwoLocalSwapNetwork(dev.wires, acquaintances, weights, fermionic=False) return qml.state()
>>> print(weights) tensor([0.20308242, 0.91906199, 0.67988804, 0.81290256, 0.08708985, 0.81860084, 0.34448344, 0.05655892, 0.61781612, 0.51829044], requires_grad=True) >>> print(qml.draw(swap_network_circuit, expansion_strategy = 'device')()) 0: ─╭●────────╭SWAP─────────────────╭●────────╭SWAP─────────────────╭●────────╭SWAP─┤ State 1: ─╰RY(0.20)─╰SWAP─╭●────────╭SWAP─╰RY(0.09)─╰SWAP─╭●────────╭SWAP─╰RY(0.62)─╰SWAP─┤ State 2: ─╭●────────╭SWAP─╰RY(0.68)─╰SWAP─╭●────────╭SWAP─╰RY(0.34)─╰SWAP─╭●────────╭SWAP─┤ State 3: ─╰RY(0.92)─╰SWAP─╭●────────╭SWAP─╰RY(0.82)─╰SWAP─╭●────────╭SWAP─╰RY(0.52)─╰SWAP─┤ State 4: ─────────────────╰RY(0.81)─╰SWAP─────────────────╰RY(0.06)─╰SWAP─────────────────┤ State
Improvements 🛠
Pulse programming
A new function called
qml.pulse.pwc
has been added as a convenience function for defining aqml.pulse.ParametrizedHamiltonian
. This function can be used to create a callable coefficient by setting the timespan over which the function should be non-zero. The resulting callable can be passed an array of parameters and a time. (#3645)>>> timespan = (2, 4) >>> f = qml.pulse.pwc(timespan) >>> f * qml.PauliX(0) ParametrizedHamiltonian: terms=1
The
params
array will be used as bin values evenly distributed over the timespan, and the parametert
will determine which of the bins is returned.>>> f(params=[1.2, 2.3, 3.4, 4.5], t=3.9) DeviceArray(4.5, dtype=float32) >>> f(params=[1.2, 2.3, 3.4, 4.5], t=6) # zero outside the range (2, 4) DeviceArray(0., dtype=float32)
A new function called
qml.pulse.pwc_from_function
has been added as a decorator for defining aqml.pulse.ParametrizedHamiltonian
. This function can be used to decorate a function and create a piecewise constant approximation of it. (#3645)>>> @qml.pulse.pwc_from_function((2, 4), num_bins=10) ... def f1(p, t): ... return p * t
The resulting function approximates the same of
p**2 * t
on the intervalt=(2, 4)
in 10 bins, and returns zero outside the interval.# t=2 and t=2.1 are within the same bin >>> f1(3, 2), f1(3, 2.1) (DeviceArray(6., dtype=float32), DeviceArray(6., dtype=float32)) # next bin >>> f1(3, 2.2) DeviceArray(6.6666665, dtype=float32) # outside the interval t=(2, 4) >>> f1(3, 5) DeviceArray(0., dtype=float32)
Add
ParametrizedHamiltonianPytree
class, which is a pytree jax object representing a parametrized Hamiltonian, where the matrix computation is delayed to improve performance. (#3779)
Operations and batching
The function
qml.dot
has been updated to compute the dot product between a vector and a list of operators. (#3586)>>> coeffs = np.array([1.1, 2.2]) >>> ops = [qml.PauliX(0), qml.PauliY(0)] >>> qml.dot(coeffs, ops) (1.1*(PauliX(wires=[0]))) + (2.2*(PauliY(wires=[0]))) >>> qml.dot(coeffs, ops, pauli=True) 1.1 * X(0) + 2.2 * Y(0)
qml.evolve
returns the evolution of anOperator
or aParametrizedHamiltonian
. (#3617) (#3706)qml.ControlledQubitUnitary
now inherits fromqml.ops.op_math.ControlledOp
, which definesdecomposition
,expand
, andsparse_matrix
rather than raising an error. (#3450)Parameter broadcasting support has been added for the
qml.ops.op_math.Controlled
class if the base operator supports broadcasting. (#3450)The
qml.generator
function now checks if the generator is Hermitian, rather than whether it is a subclass ofObservable
. This allows it to return valid generators fromSymbolicOp
andCompositeOp
classes. (#3485)The
qml.equal
function has been extended to compareProd
andSum
operators. (#3516)qml.purity
has been added as a measurement process for purity (#3551)In-place inversion has been removed for qutrit operations in preparation for the removal of in-place inversion. (#3566)
The
qml.utils.sparse_hamiltonian
function has been moved to theeqml.Hamiltonian.sparse_matrix
method. (#3585)The
qml.pauli.PauliSentence.operation()
method has been improved to avoid instantiating anSProd
operator when the coefficient is equal to 1. (#3595)Batching is now allowed in all
SymbolicOp
operators, which includeExp
,Pow
andSProd
. (#3597)The
Sum
andProd
operations now have broadcasted operands. (#3611)The XYX single-qubit unitary decomposition has been implemented. (#3628)
All dunder methods now return
NotImplemented
, allowing the right dunder method (e.g.__radd__
) of the other class to be called. (#3631)The
qml.GellMann
operators now include their index when displayed. (#3641)qml.ops.ctrl_decomp_zyz
has been added to compute the decomposition of a controlled single-qubit operation given a single-qubit operation and the control wires. (#3681)qml.pauli.is_pauli_word
now supportsProd
andSProd
operators, and it returnsFalse
when aHamiltonian
contains more than one term. (#3692)qml.pauli.pauli_word_to_string
now supportsProd
,SProd
andHamiltonian
operators. (#3692)qml.ops.op_math.Controlled
can now decompose single qubit target operations more effectively using the ZYZ decomposition. (#3726)The
qml.qchem.Molecule
class raises an error when the molecule has an odd number of electrons or when the spin multiplicity is not 1. (#3748)
qml.qchem.basis_rotation
now accounts for spin, allowing it to perform Basis Rotation Groupings for molecular hamiltonians. (#3714) (#3774)The gradient transforms work for the new return type system with non-trivial classical jacobians. (#3776)
The
default.mixed
device has received a performance improvement for multi-qubit operations. This also allows to apply channels that act on more than seven qubits, which was not possible before. (#3584)qml.dot
now groups coefficients together. (#3691)>>> qml.dot(coeffs=[2, 2, 2], ops=[qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]) 2*(PauliX(wires=[0]) + PauliY(wires=[1]) + PauliZ(wires=[2]))
qml.generator
now supports operators withSum
andProd
generators. (#3691)The
Sum._sort
method now takes into account the name of the operator when sorting. (#3691)A new tape transform called
qml.transforms.sign_expand
has been added. It implements the optimal decomposition of a fast-forwardable Hamiltonian that minimizes the variance of its estimator in the Single-Qubit-Measurement from arXiv:2207.09479. (#2852)
Differentiability and interfaces
The
qml.math
module now also contains a submodule for fast Fourier transforms,qml.math.fft
. (#1440)The submodule in particular provides differentiable versions of the following functions, available in all common interfaces for PennyLane
Note that the output of the derivative of these functions may differ when used with complex-valued inputs, due to different conventions on complex-valued derivatives.
Validation has been added on gradient keyword arguments when initializing a QNode — if unexpected keyword arguments are passed, a
UserWarning
is raised. A list of the current expected gradient function keyword arguments can be accessed viaqml.gradients.SUPPORTED_GRADIENT_KWARGS
. (#3526)The
numpy
version has been constrained to<1.24
. (#3563)Support for two-qubit unitary decomposition with JAX-JIT has been added. (#3569)
qml.math.size
now supports PyTorch tensors. (#3606)Most quantum channels are now fully differentiable on all interfaces. (#3612)
qml.math.matmul
now supports PyTorch and Autograd tensors. (#3613)Add
qml.math.detach
, which detaches a tensor from its trace. This stops automatic gradient computations. (#3674)Add
typing.TensorLike
type. (#3675)qml.QuantumMonteCarlo
template is now JAX-JIT compatible when passingjax.numpy
arrays to the template. (#3734)DefaultQubitJax
now supports evolving the state vector when executingqml.pulse.ParametrizedEvolution
gates. (#3743)SProd.sparse_matrix
now supports interface-specific variables with a single element as thescalar
. (#3770)Added
argnum
argument tometric_tensor
. By passing a sequence of indices referring to trainable tape parameters, the metric tensor is only computed with respect to these parameters. This reduces the number of tapes that have to be run. (#3587)The parameter-shift derivative of variances saves a redundant evaluation of the corresponding unshifted expectation value tape, if possible (#3744)
Next generation device API
The
apply_operation
single-dispatch function is added todevices/qubit
that applies an operation to a state and returns a new state. (#3637)The
preprocess
function is added todevices/qubit
that validates, expands, and transforms a batch ofQuantumTape
objects to abstract preprocessing details away from the device. (#3708)The
create_initial_state
function is added todevices/qubit
that returns an initial state for an execution. (#3683)The
simulate
function is added todevices/qubit
that turns a single quantum tape into a measurement result. The function only supports state based measurements with either no observables or observables with diagonalizing gates. It supports simultaneous measurement of non-commuting observables. (#3700)The
ExecutionConfig
data class has been added. (#3649)The
StatePrep
class has been added as an interface that state-prep operators must implement. (#3654)qml.QubitStateVector
now implements theStatePrep
interface. (#3685)qml.BasisState
now implements theStatePrep
interface. (#3693)New Abstract Base Class for devices
Device
is added to thedevices.experimental
submodule. This interface is still in experimental mode and not integrated with the rest of pennylane. (#3602)
Other improvements
Writing Hamiltonians to a file using the
qml.data
module has been improved by employing a condensed writing format. (#3592)Lazy-loading in the
qml.data.Dataset.read()
method is more universally supported. (#3605)The
qchem.Molecule
class raises an error when the molecule has an odd number of electrons or when the spin multiplicity is not 1. (#3748)qml.draw
andqml.draw_mpl
have been updated to draw any quantum function, which allows for visualizing only part of a complete circuit/QNode. (#3760)The string representation of a Measurement Process now includes the
_eigvals
property if it is set. (#3820)
Breaking changes 💔
The argument
mode
in execution has been replaced by the booleangrad_on_execution
in the new execution pipeline. (#3723)qml.VQECost
has been removed. (#3735)The default interface is now
auto
. (#3677) (#3752) (#3829)The interface is determined during the QNode call instead of the initialization. It means that the
gradient_fn
andgradient_kwargs
are only defined on the QNode at the beginning of the call. Moreover, without specifying the interface it is not possible to guarantee that the device will not be changed during the call if you are using backprop (such asdefault.qubit
changing todefault.qubit.jax
) whereas before it was happening at initialization.The tape method
get_operation
can also now return the operation index in the tape, and it can be activated by setting thereturn_op_index
toTrue
:get_operation(idx, return_op_index=True)
. It will become the default in version0.30
. (#3667)Operation.inv()
and theOperation.inverse
setter have been removed. Please useqml.adjoint
orqml.pow
instead. (#3618)For example, instead of
>>> qml.PauliX(0).inv()
use
>>> qml.adjoint(qml.PauliX(0))
The
Operation.inverse
property has been removed completely. (#3725)The target wires of
qml.ControlledQubitUnitary
are no longer available viaop.hyperparameters["u_wires"]
. Instead, they can be accesses viaop.base.wires
orop.target_wires
. (#3450)The tape constructed by a
QNode
is no longer queued to surrounding contexts. (#3509)Nested operators like
Tensor
,Hamiltonian
, andAdjoint
now remove their owned operators from the queue instead of updating their metadata to have an"owner"
. (#3282)qml.qchem.scf
,qml.RandomLayers.compute_decomposition
, andqml.Wires.select_random
now use local random number generators instead of global random number generators. This may lead to slightly different random numbers and an independence of the results from the global random number generation state. Please provide a seed to each individual function instead if you want controllable results. (#3624)qml.transforms.measurement_grouping
has been removed. Users should useqml.transforms.hamiltonian_expand
instead. (#3701)op.simplify()
for operators which are linear combinations of Pauli words will use a builtin Pauli representation to more efficiently compute the simplification of the operator. (#3481)All
Operator
‘s input parameters that are lists are cast into vanilla numpy arrays. (#3659)QubitDevice.expval
no longer permutes an observable’s wire order before passing it toQubitDevice.probability
. The associated downstream changes fordefault.qubit
have been made, but this may still affect expectations for other devices that inherit fromQubitDevice
and overrideprobability
(or any other helper functions that take a wire order such asmarginal_prob
,estimate_probability
oranalytic_probability
). (#3753)
Deprecations 👋
qml.utils.sparse_hamiltonian
function has been deprecated, and usage will now raise a warning. Instead, one should use theqml.Hamiltonian.sparse_matrix
method. (#3585)qml.op_sum
has been deprecated. Users should useqml.sum
instead. (#3686)The use of
Evolution
directly has been deprecated. Users should useqml.evolve
instead. This new function changes the sign of the given parameter. (#3706)Use of
qml.dot
with aQNodeCollection
has been deprecated. (#3586)
Documentation 📝
Revise note on GPU support in the circuit introduction. (#3836)
Make warning about vanilla version of NumPy for differentiation more prominent. (#3838)
The documentation for
qml.operation
has been improved. (#3664)The code example in
qml.SparseHamiltonian
has been updated with the correct wire range. (#3643)A hyperlink has been added in the text for a URL in the
qml.qchem.mol_data
docstring. (#3644)A typo was corrected in the documentation for
qml.math.vn_entropy
. (#3740)
Bug fixes 🐛
Fixed a bug where measuring
qml.probs
in the computational basis with non-commuting measurements returned incorrect results. Now an error is raised. (#3811)Fixed a bug where measuring
qml.probs
in the computational basis with non-commuting measurements returned incorrect results. Now an error is raised. (#3811)Fixed a bug in the drawer where nested controlled operations would output the label of the operation being controlled, rather than the control values. (#3745)
Fixed a bug in
qml.transforms.metric_tensor
where prefactors of operation generators were taken into account multiple times, leading to wrong outputs for non-standard operations. (#3579)Local random number generators are now used where possible to avoid mutating the global random state. (#3624)
The
networkx
version change being broken has been fixed by selectively skipping aqcut
TensorFlow-JIT test. (#3609) (#3619)Fixed the wires for the
Y
decomposition in the ZX calculus transform. (#3598)qml.pauli.PauliWord
is now pickle-able. (#3588)Child classes of
QuantumScript
now return their own type when usingSomeChildClass.from_queue
. (#3501)A typo has been fixed in the calculation and error messages in
operation.py
(#3536)qml.data.Dataset.write()
now ensures that any lazy-loaded values are loaded before they are written to a file. (#3605)Tensor._batch_size
is now set toNone
during initialization, copying andmap_wires
. (#3642) (#3661)Tensor.has_matrix
is now set toTrue
. (#3647)Fixed typo in the example of
qml.IsingZZ
gate decomposition. (#3676)Fixed a bug that made tapes/qnodes using
qml.Snapshot
incompatible withqml.drawer.tape_mpl
. (#3704)Tensor._pauli_rep
is set toNone
during initialization andTensor.data
has been added to its setter. (#3722)qml.math.ndim
has been redirected tojnp.ndim
when using it on ajax
tensor. (#3730)Implementations of
marginal_prob
(and subsequently,qml.probs
) now return probabilities with the expected wire order. (#3753)This bug affected most probabilistic measurement processes on devices that inherit from
QubitDevice
when the measured wires are out of order with respect to the device wires and 3 or more wires are measured. The assumption was that marginal probabilities would be computed with the device’s state and wire order, then re-ordered according to the measurement process wire order. Instead, the re-ordering went in the inverse direction (that is, from measurement process wire order to device wire order). This is now fixed. Note that this only occurred for 3 or more measured wires because this mapping is identical otherwise. More details and discussion of this bug can be found in the original bug report.Empty iterables can no longer be returned from QNodes. (#3769)
The keyword arguments for
qml.equal
now are used when comparing the observables of a Measurement Process. The eigvals of measurements are only requested if both observables areNone
, saving computational effort. (#3820)Only converts input to
qml.Hermitian
to a numpy array if the input is a list. (#3820)
Contributors ✍
This release contains contributions from (in alphabetical order):
Gian-Luca Anselmetti, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ikko Ashimine, Utkarsh Azad, Miriam Beddig, Cristian Boghiu, Thomas Bromley, Astral Cai, Isaac De Vlugt, Olivia Di Matteo, Lillian M. A. Frederiksen, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Albert Mitjans Coma, Romain Moyard, Mudit Pandey, Borja Requena, Matthew Silverman, Jay Soni, Antal Száva, Frederik Wilde, David Wierichs, Moritz Willmann.
- orphan
Release 0.28.0¶
New features since last release
Custom measurement processes 📐
Custom measurements can now be facilitated with the addition of the
qml.measurements
module. (#3286) (#3343) (#3288) (#3312) (#3287) (#3292) (#3287) (#3326) (#3327) (#3388) (#3439) (#3466)Within
qml.measurements
are new subclasses that allow for the possibility to create custom measurements:SampleMeasurement
: represents a sample-based measurementStateMeasurement
: represents a state-based measurementMeasurementTransform
: represents a measurement process that requires the application of a batch transform
Creating a custom measurement involves making a class that inherits from one of the classes above. An example is given below. Here, the measurement computes the number of samples obtained of a given state:
from pennylane.measurements import SampleMeasurement class CountState(SampleMeasurement): def __init__(self, state: str): self.state = state # string identifying the state, e.g. "0101" wires = list(range(len(state))) super().__init__(wires=wires) def process_samples(self, samples, wire_order, shot_range, bin_size): counts_mp = qml.counts(wires=self._wires) counts = counts_mp.process_samples(samples, wire_order, shot_range, bin_size) return counts.get(self.state, 0) def __copy__(self): return CountState(state=self.state)
We can now execute the new measurement in a QNode as follows.
dev = qml.device("default.qubit", wires=1, shots=10000) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) return CountState(state="1")
>>> circuit(1.23) tensor(3303., requires_grad=True)
Differentiability is also supported for this new measurement process:
>>> x = qml.numpy.array(1.23, requires_grad=True) >>> qml.grad(circuit)(x) 4715.000000000001
For more information about these new features, see the documentation for ``qml.measurements` <https://docs.pennylane.ai/en/stable/code/qml_measurements.html>`_.
ZX Calculus 🧮
QNodes can now be converted into ZX diagrams via the PyZX framework. (#3446)
ZX diagrams are the medium for which we can envision a quantum circuit as a graph in the ZX-calculus language, showing properties of quantum protocols in a visually compact and logically complete fashion.
QNodes decorated with
@qml.transforms.to_zx
will return a PyZX graph that represents the computation in the ZX-calculus language.dev = qml.device("default.qubit", wires=2) @qml.transforms.to_zx @qml.qnode(device=dev) def circuit(p): qml.RZ(p[0], wires=1), qml.RZ(p[1], wires=1), qml.RX(p[2], wires=0), qml.PauliZ(wires=0), qml.RZ(p[3], wires=1), qml.PauliX(wires=1), qml.CNOT(wires=[0, 1]), qml.CNOT(wires=[1, 0]), qml.SWAP(wires=[0, 1]), return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
>>> params = [5 / 4 * np.pi, 3 / 4 * np.pi, 0.1, 0.3] >>> circuit(params) Graph(20 vertices, 23 edges)
Information about PyZX graphs can be found in the PyZX Graphs API.
QChem databases and basis sets ⚛️
The symbols and geometry of a compound from the PubChem database can now be accessed via
qchem.mol_data()
. (#3289) (#3378)>>> import pennylane as qml >>> from pennylane.qchem import mol_data >>> mol_data("BeH2") (['Be', 'H', 'H'], tensor([[ 4.79404621, 0.29290755, 0. ], [ 3.77945225, -0.29290755, 0. ], [ 5.80882913, -0.29290755, 0. ]], requires_grad=True)) >>> mol_data(223, "CID") (['N', 'H', 'H', 'H', 'H'], tensor([[ 0. , 0. , 0. ], [ 1.82264085, 0.52836742, 0.40402345], [ 0.01417295, -1.67429735, -0.98038991], [-0.98927163, -0.22714508, 1.65369933], [-0.84773114, 1.373075 , -1.07733286]], requires_grad=True))
Perform quantum chemistry calculations with two new basis sets:
6-311g
andCC-PVDZ
. (#3279)>>> symbols = ["H", "He"] >>> geometry = np.array([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], requires_grad=False) >>> charge = 1 >>> basis_names = ["6-311G", "CC-PVDZ"] >>> for basis_name in basis_names: ... mol = qml.qchem.Molecule(symbols, geometry, charge=charge, basis_name=basis_name) ... print(qml.qchem.hf_energy(mol)()) [-2.84429531] [-2.84061284]
A bunch of new operators 👀
The controlled CZ gate and controlled Hadamard gate are now available via
qml.CCZ
andqml.CH
, respectively. (#3408)>>> ccz = qml.CCZ(wires=[0, 1, 2]) >>> qml.matrix(ccz) [[ 1 0 0 0 0 0 0 0] [ 0 1 0 0 0 0 0 0] [ 0 0 1 0 0 0 0 0] [ 0 0 0 1 0 0 0 0] [ 0 0 0 0 1 0 0 0] [ 0 0 0 0 0 1 0 0] [ 0 0 0 0 0 0 1 0] [ 0 0 0 0 0 0 0 -1]] >>> ch = qml.CH(wires=[0, 1]) >>> qml.matrix(ch) [[ 1. 0. 0. 0. ] [ 0. 1. 0. 0. ] [ 0. 0. 0.70710678 0.70710678] [ 0. 0. 0.70710678 -0.70710678]]
Three new parametric operators,
qml.CPhaseShift00
,qml.CPhaseShift01
, andqml.CPhaseShift10
, are now available. Each of these operators performs a phase shift akin toqml.ControlledPhaseShift
but on different positions of the state vector. (#2715)>>> dev = qml.device("default.qubit", wires=2) >>> @qml.qnode(dev) >>> def circuit(): ... qml.PauliX(wires=1) ... qml.CPhaseShift01(phi=1.23, wires=[0,1]) ... return qml.state() ... >>> circuit() tensor([0. +0.j , 0.33423773+0.9424888j, 1. +0.j , 0. +0.j ], requires_grad=True)
A new gate operation called
qml.FermionicSWAP
has been added. This implements the exchange of spin orbitals representing fermionic-modes while maintaining proper anti-symmetrization. (#3380)dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(phi): qml.BasisState(np.array([0, 1]), wires=[0, 1]) qml.FermionicSWAP(phi, wires=[0, 1]) return qml.state()
>>> circuit(0.1) tensor([0. +0.j , 0.99750208+0.04991671j, 0.00249792-0.04991671j, 0. +0.j ], requires_grad=True)
Create operators defined from a generator via
qml.ops.op_math.Evolution
. (#3375)qml.ops.op_math.Evolution
defines the exponential of an operator $hat{O}$ of the form $e^{ixhat{O}}$, with a single trainable parameter, $x$. Limiting to a single trainable parameter allows the use ofqml.gradients.param_shift
to find the gradient with respect to the parameter $x$.dev = qml.device('default.qubit', wires=2) @qml.qnode(dev, diff_method=qml.gradients.param_shift) def circuit(phi): qml.ops.op_math.Evolution(qml.PauliX(0), -.5 * phi) return qml.expval(qml.PauliZ(0))
>>> phi = np.array(1.2) >>> circuit(phi) tensor(0.36235775, requires_grad=True) >>> qml.grad(circuit)(phi) -0.9320390495504149
The qutrit Hadamard gate,
qml.THadamard
, is now available. (#3340)The operation accepts a
subspace
keyword argument which determines which variant of the qutrit Hadamard to use.>>> th = qml.THadamard(wires=0, subspace=[0, 1]) >>> qml.matrix(th) array([[ 0.70710678+0.j, 0.70710678+0.j, 0. +0.j], [ 0.70710678+0.j, -0.70710678+0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 1. +0.j]])
New transforms, functions, and more 😯
Calculating the purity of arbitrary quantum states is now supported. (#3290)
The purity can be calculated in an analogous fashion to, say, the Von Neumann entropy:
qml.math.purity
can be used as an in-line function:>>> x = [1, 0, 0, 1] / np.sqrt(2) >>> qml.math.purity(x, [0, 1]) 1.0 >>> qml.math.purity(x, [0]) 0.5 >>> x = [[1 / 2, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1 / 2]] >>> qml.math.purity(x, [0, 1]) 0.5
qml.qinfo.transforms.purity
can transform a QNode returning a state to a function that returns the purity:dev = qml.device("default.mixed", wires=2) @qml.qnode(dev) def circuit(x): qml.IsingXX(x, wires=[0, 1]) return qml.state()
>>> qml.qinfo.transforms.purity(circuit, wires=[0])(np.pi / 2) 0.5 >>> qml.qinfo.transforms.purity(circuit, wires=[0, 1])(np.pi / 2) 1.0
As with the other methods in
qml.qinfo
, the purity is fully differentiable:>>> param = np.array(np.pi / 4, requires_grad=True) >>> qml.grad(qml.qinfo.transforms.purity(circuit, wires=[0]))(param) -0.5
A new gradient transform,
qml.gradients.spsa_grad
, that is based on the idea of SPSA is now available. (#3366)This new transform allows users to compute a single estimate of a quantum gradient using simultaneous perturbation of parameters and a stochastic approximation. A QNode that takes, say, an argument
x
, the approximate gradient can be computed as follows.>>> dev = qml.device("default.qubit", wires=2) >>> x = np.array(0.4, requires_grad=True) >>> @qml.qnode(dev) ... def circuit(x): ... qml.RX(x, 0) ... qml.RX(x, 1) ... return qml.expval(qml.PauliZ(0)) >>> grad_fn = qml.gradients.spsa_grad(circuit, h=0.1, num_directions=1) >>> grad_fn(x) array(-0.38876964)
The argument
num_directions
determines how many directions of simultaneous perturbation are used, which is proportional to the number of circuit evaluations. See the SPSA gradient transform documentation for details. Note that the full SPSA optimizer is already available asqml.SPSAOptimizer
.Multiple mid-circuit measurements can now be combined arithmetically to create new conditionals. (#3159)
dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) qml.Hadamard(wires=1) m0 = qml.measure(wires=0) m1 = qml.measure(wires=1) combined = 2 * m1 + m0 qml.cond(combined == 2, qml.RX)(1.3, wires=2) return qml.probs(wires=2)
>>> circuit() [0.90843735 0.09156265]
A new method called
pauli_decompose()
has been added to theqml.pauli
module, which takes a hermitian matrix, decomposes it in the Pauli basis, and returns it either as aqml.Hamiltonian
orqml.PauliSentence
instance. (#3384)Operation
orHamiltonian
instances can now be generated from aqml.PauliSentence
orqml.PauliWord
via the newoperation()
andhamiltonian()
methods. (#3391)A
sum_expand
function has been added for tapes, which splits a tape measuring aSum
expectation into mutliple tapes of summand expectations, and provides a function to recombine the results. (#3230)
(Experimental) More interface support for multi-measurement and gradient output types 🧪
The autograd and Tensorflow interfaces now support devices with shot vectors when
qml.enable_return()
has been called. (#3374) (#3400)Here is an example using Tensorflow:
import tensorflow as tf qml.enable_return() dev = qml.device("default.qubit", wires=2, shots=[1000, 2000, 3000]) @qml.qnode(dev, diff_method="parameter-shift", interface="tf") def circuit(a): qml.RY(a, wires=0) qml.RX(0.2, wires=0) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)), qml.probs([0, 1])
>>> a = tf.Variable(0.4) >>> with tf.GradientTape() as tape: ... res = circuit(a) ... res = tf.stack([tf.experimental.numpy.hstack(r) for r in res]) ... >>> res <tf.Tensor: shape=(3, 5), dtype=float64, numpy= array([[0.902, 0.951, 0. , 0. , 0.049], [0.898, 0.949, 0. , 0. , 0.051], [0.892, 0.946, 0. , 0. , 0.054]])> >>> tape.jacobian(res, a) <tf.Tensor: shape=(3, 5), dtype=float64, numpy= array([[-0.345 , -0.1725 , 0. , 0. , 0.1725 ], [-0.383 , -0.1915 , 0. , 0. , 0.1915 ], [-0.38466667, -0.19233333, 0. , 0. , 0.19233333]])>
The PyTorch interface is now fully supported when
qml.enable_return()
has been called, allowing the calculation of the Jacobian and the Hessian using custom differentiation methods (e.g., parameter-shift, finite difference, or adjoint). (#3416)import torch qml.enable_return() dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, diff_method="parameter-shift", interface="torch") def circuit(a, b): qml.RY(a, wires=0) qml.RX(b, wires=1) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)), qml.probs([0, 1])
>>> a = torch.tensor(0.1, requires_grad=True) >>> b = torch.tensor(0.2, requires_grad=True) >>> torch.autograd.functional.jacobian(circuit, (a, b)) ((tensor(-0.0998), tensor(0.)), (tensor([-0.0494, -0.0005, 0.0005, 0.0494]), tensor([-0.0991, 0.0991, 0.0002, -0.0002])))
The JAX-JIT interface now supports first-order gradient computation when
qml.enable_return()
has been called. (#3235) (#3445)import jax from jax import numpy as jnp jax.config.update("jax_enable_x64", True) qml.enable_return() dev = qml.device("lightning.qubit", wires=2) @jax.jit @qml.qnode(dev, interface="jax-jit", diff_method="parameter-shift") def circuit(a, b): qml.RY(a, wires=0) qml.RX(b, wires=0) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) a, b = jnp.array(1.0), jnp.array(2.0)
>>> jax.jacobian(circuit, argnums=[0, 1])(a, b) ((Array(0.35017549, dtype=float64, weak_type=True), Array(-0.4912955, dtype=float64, weak_type=True)), (Array(5.55111512e-17, dtype=float64, weak_type=True), Array(0., dtype=float64, weak_type=True)))
Improvements 🛠
qml.pauli.is_pauli_word
now supports instances ofqml.Hamiltonian
. (#3389)When
qml.probs
,qml.counts
, andqml.sample
are called with no arguments, they measure all wires. Calling any of the aforementioned measurements with an empty wire list (e.g.,qml.sample(wires=[])
) will raise an error. (#3299)Made
qml.gradients.finite_diff
more convenient to use with custom data type observables/devices by reducing the number of magic methods that need to be defined in the custom data type to supportfinite_diff
. (#3426)The
qml.ISWAP
gate is now natively supported ondefault.mixed
, improving on its efficiency. (#3284)Added more input validation to
qml.transforms.hamiltonian_expand
such that Hamiltonian objects with no terms raise an error. (#3339)Continuous integration checks are now performed for Python 3.11 and Torch v1.13. Python 3.7 is dropped. (#3276)
qml.Tracker
now also logs results intracker.history
when tracking the execution of a circuit.
The execution time of
Wires.all_wires
has been improved by avoiding data type changes and making use ofitertools.chain
. (#3302)Printing an instance of
qml.qchem.Molecule
is now more concise and informational. (#3364)The error message for
qml.transforms.insert
when it fails to diagonalize non-qubit-wise-commuting observables is now more detailed. (#3381)Extended the
qml.equal
function toqml.Hamiltonian
andTensor
objects. (#3390)QuantumTape._process_queue
has been moved toqml.queuing.process_queue
to disentangle its functionality from theQuantumTape
class. (#3401)QPE can now accept a target operator instead of a matrix and target wires pair. (#3373)
The
qml.ops.op_math.Controlled.map_wires
method now usesbase.map_wires
internally instead of the private_wires
property setter. (#3405)A new function called
qml.tape.make_qscript
has been created for converting a quantum function into a quantum script. This replacesqml.transforms.make_tape
. (#3429)Add a
_pauli_rep
attribute to operators to integrate the new Pauli arithmetic classes with native PennyLane objects. (#3443)Extended the functionality of
qml.matrix
to qutrits. (#3508)The
qcut.py
file inpennylane/transforms/
has been reorganized into multiple files that are now inpennylane/transforms/qcut/
. (#3413)A warning now appears when creating a
Tensor
object with overlapping wires, informing that this can lead to undefined behaviour. (#3459)Extended the
qml.equal
function toqml.ops.op_math.Controlled
andqml.ops.op_math.ControlledOp
objects. (#3463)Nearly every instance of
with QuantumTape()
has been replaced withQuantumScript
construction. (#3454)Added
validate_subspace
static method toqml.Operator
to check the validity of the subspace of certain qutrit operations. (#3340)qml.equal
now supports operators created viaqml.s_prod
,qml.pow
,qml.exp
, andqml.adjoint
. (#3471)Devices can now disregard observable grouping indices in Hamiltonians through the optional
use_grouping
attribute. (#3456)Add the optional argument
lazy=True
to functionsqml.s_prod
,qml.prod
andqml.op_sum
to allow simplification. (#3483)Updated the
qml.transforms.zyz_decomposition
function such that it now supports broadcast operators. This means that single-qubitqml.QubitUnitary
operators, instantiated from a batch of unitaries, can now be decomposed. (#3477)The performance of executing circuits under the
jax.vmap
transformation has been improved by being able to leverage the batch-execution capabilities of some devices. (#3452)The tolerance for converting openfermion Hamiltonian complex coefficients to real ones has been modified to prevent conversion errors. (#3367)
OperationRecorder
now inherits fromAnnotatedQueue
andQuantumScript
instead ofQuantumTape
. (#3496)Updated
qml.transforms.split_non_commuting
to support the new return types. (#3414)Updated
qml.transforms.mitigate_with_zne
to support the new return types. (#3415)Updated
qml.transforms.metric_tensor
,qml.transforms.adjoint_metric_tensor
,qml.qinfo.classical_fisher
, andqml.qinfo.quantum_fisher
to support the new return types. (#3449)Updated
qml.transforms.batch_params
andqml.transforms.batch_input
to support the new return types. (#3431)Updated
qml.transforms.cut_circuit
andqml.transforms.cut_circuit_mc
to support the new return types. (#3346)Limit NumPy version to
<1.24
. (#3346)
Breaking changes 💔
Python 3.7 support is no longer maintained. PennyLane will be maintained for versions 3.8 and up. (#3276)
The
log_base
attribute has been moved fromMeasurementProcess
to the newVnEntropyMP
andMutualInfoMP
classes, which inherit fromMeasurementProcess
. (#3326)qml.utils.decompose_hamiltonian()
has been removed. Please useqml.pauli.pauli_decompose()
instead. (#3384)The
return_type
attribute ofMeasurementProcess
has been removed where possible. Useisinstance
checks instead. (#3399)Instead of having an
OrderedDict
attribute called_queue
,AnnotatedQueue
now inherits fromOrderedDict
and encapsulates the queue. Consequentially, this also applies to theQuantumTape
class which inherits fromAnnotatedQueue
. (#3401)The
ShadowMeasurementProcess
class has been renamed toClassicalShadowMP
. (#3388)The
qml.Operation.get_parameter_shift
method has been removed. Thegradients
module should be used for general parameter-shift rules instead. (#3419)The signature of the
QubitDevice.statistics
method has been changed fromdef statistics(self, observables, shot_range=None, bin_size=None, circuit=None):
to
def statistics(self, circuit: QuantumTape, shot_range=None, bin_size=None):
The
MeasurementProcess
class is now an abstract class andreturn_type
is now a property of the class. (#3434)
Deprecations 👋
Deprecations cycles are tracked at doc/developement/deprecations.rst.
The following methods are deprecated: (#3281)
qml.tape.get_active_tape
: Useqml.QueuingManager.active_context()
insteadqml.transforms.qcut.remap_tape_wires
: Useqml.map_wires
insteadqml.tape.QuantumTape.inv()
: Useqml.tape.QuantumTape.adjoint()
insteadqml.tape.stop_recording()
: Useqml.QueuingManager.stop_recording()
insteadqml.tape.QuantumTape.stop_recording()
: Useqml.QueuingManager.stop_recording()
insteadqml.QueuingContext
is nowqml.QueuingManager
QueuingManager.safe_update_info
andAnnotatedQueue.safe_update_info
: Useupdate_info
instead.
qml.transforms.measurement_grouping
has been deprecated. Useqml.transforms.hamiltonian_expand
instead. (#3417)The
observables
argument inQubitDevice.statistics
is deprecated. Please usecircuit
instead. (#3433)The
seed_recipes
argument inqml.classical_shadow
andqml.shadow_expval
is deprecated. A new argumentseed
has been added, which defaults to None and can contain an integer with the wanted seed. (#3388)qml.transforms.make_tape
has been deprecated. Please useqml.tape.make_qscript
instead. (#3478)
Documentation 📝
Added documentation on parameter broadcasting regarding both its usage and technical aspects. (#3356)
The quickstart guide on circuits as well as the the documentation of QNodes and Operators now contain introductions and details on parameter broadcasting. The QNode documentation mostly contains usage details, the Operator documentation is concerned with implementation details and a guide to support broadcasting in custom operators.
The return type statements of gradient and Hessian transforms and a series of other functions that are a
batch_transform
have been corrected. (#3476)Developer documentation for the queuing module has been added. (#3268)
More mentions of diagonalizing gates for all relevant operations have been corrected. (#3409)
The docstrings for
compute_eigvals
used to say that the diagonalizing gates implemented $U$, the unitary such that $O = U Sigma U^{dagger}$, where $O$ is the original observable and $Sigma$ a diagonal matrix. However, the diagonalizing gates actually implement $U^{dagger}$, since $langle psi | O | psi rangle = langle psi | U Sigma U^{dagger} | psi rangle$, making $U^{dagger} | psi rangle$ the actual state being measured in the $Z$-basis.A warning about using
dill
to pickle and unpickle datasets has been added. (#3505)
Bug fixes 🐛
Fixed a bug that prevented
qml.gradients.param_shift
from being used for broadcasted tapes. (#3528)Fixed a bug where
qml.transforms.hamiltonian_expand
didn’t preserve the type of the input results in its output. (#3339)Fixed a bug that made
qml.gradients.param_shift
raise an error when used with unshifted terms only in a custom recipe, and when using any unshifted terms at all under the new return type system. (#3177)The original tape
_obs_sharing_wires
attribute is updated during its expansion. (#3293)An issue with
drain=False
in the adaptive optimizer has been fixed. Before the fix, the operator pool needed to be reconstructed inside the optimization pool whendrain=False
. With this fix, this reconstruction is no longer needed. (#3361)If the device originally has no shots but finite shots are dynamically specified, Hamiltonian expansion now occurs. (#3369)
qml.matrix(op)
now fails if the operator truly has no matrix (e.g.,qml.Barrier
) to matchop.matrix()
. (#3386)The
pad_with
argument in theqml.AmplitudeEmbedding
template is now compatible with all interfaces. (#3392)Operator.pow
now queues its constituents by default. (#3373)Fixed a bug where a QNode returning
qml.sample
would produce incorrect results when run on a device defined with a shot vector. (#3422)The
qml.data
module now works as expected on Windows. (#3504)
Contributors ✍️
This release contains contributions from (in alphabetical order):
Guillermo Alonso, Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Thomas Bromley, Astral Cai, Albert Mitjans Coma, Ahmed Darwish, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Pieter Eendebak, Lillian M. A. Frederiksen, Diego Guala, Katharine Hyatt, Josh Izaac, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Christina Lee, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Kevin Shen, Matthew Silverman, Jay Soni, Antal Száva, David Wierichs, Moritz Willmann, and Filippo Vicentini.
- orphan
Release 0.27.0¶
New features since last release
An all-new data module 💾
The
qml.data
module is now available, allowing users to download, load, and create quantum datasets. (#3156)Datasets are hosted on Xanadu Cloud and can be downloaded by using
qml.data.load()
:>>> H2_datasets = qml.data.load( ... data_name="qchem", molname="H2", basis="STO-3G", bondlength=1.1 ... ) >>> H2data = H2_datasets[0] >>> H2data <Dataset = description: qchem/H2/STO-3G/1.1, attributes: ['molecule', 'hamiltonian', ...]>
Datasets available to be downloaded can be listed with
qml.data.list_datasets()
.To download or load only specific properties of a dataset, we can specify the desired properties in
qml.data.load
with theattributes
keyword argument:>>> H2_hamiltonian = qml.data.load( ... data_name="qchem", molname="H2", basis="STO-3G", bondlength=1.1, ... attributes=["molecule", "hamiltonian"] ... )[0] >>> H2_hamiltonian.hamiltonian <Hamiltonian: terms=15, wires=[0, 1, 2, 3]>
The available attributes can be found using
qml.data.list_attributes()
:To select data interactively, we can use
qml.data.load_interactive()
:>>> qml.data.load_interactive() Please select a data name: 1) qspin 2) qchem Choice [1-2]: 1 Please select a sysname: ... Please select a periodicity: ... Please select a lattice: ... Please select a layout: ... Please select attributes: ... Force download files? (Default is no) [y/N]: N Folder to download to? (Default is pwd, will download to /datasets subdirectory): Please confirm your choices: dataset: qspin/Ising/open/rectangular/4x4 attributes: ['parameters', 'ground_states'] force: False dest folder: datasets Would you like to continue? (Default is yes) [Y/n]: <Dataset = description: qspin/Ising/open/rectangular/4x4, attributes: ['parameters', 'ground_states']>
Once a dataset is loaded, its properties can be accessed as follows:
>>> dev = qml.device("default.qubit",wires=4) >>> @qml.qnode(dev) ... def circuit(): ... qml.BasisState(H2data.hf_state, wires = [0, 1, 2, 3]) ... for op in H2data.vqe_gates: ... qml.apply(op) ... return qml.expval(H2data.hamiltonian) >>> print(circuit()) -1.0791430411076344
It’s also possible to create custom datasets with
qml.data.Dataset
:>>> example_hamiltonian = qml.Hamiltonian(coeffs=[1,0.5], observables=[qml.PauliZ(wires=0),qml.PauliX(wires=1)]) >>> example_energies, _ = np.linalg.eigh(qml.matrix(example_hamiltonian)) >>> example_dataset = qml.data.Dataset( ... data_name = 'Example', hamiltonian=example_hamiltonian, energies=example_energies ... ) >>> example_dataset.data_name 'Example' >>> example_dataset.hamiltonian (0.5) [X1] + (1) [Z0] >>> example_dataset.energies array([-1.5, -0.5, 0.5, 1.5])
Custom datasets can be saved and read with the
qml.data.Dataset.write()
andqml.data.Dataset.read()
methods, respectively.>>> example_dataset.write('./path/to/dataset.dat') >>> read_dataset = qml.data.Dataset() >>> read_dataset.read('./path/to/dataset.dat') >>> read_dataset.data_name 'Example' >>> read_dataset.hamiltonian (0.5) [X1] + (1) [Z0] >>> read_dataset.energies array([-1.5, -0.5, 0.5, 1.5])
We will continue to work on adding more datasets and features for
qml.data
in future releases.
Adaptive optimization 🏃🏋️🏊
Optimizing quantum circuits can now be done adaptively with
qml.AdaptiveOptimizer
. (#3192)The
qml.AdaptiveOptimizer
takes an initial circuit and a collection of operators as input and adds a selected gate to the circuit at each optimization step. The process of growing the circuit can be repeated until the circuit gradients converge to zero within a given threshold. The adaptive optimizer can be used to implement algorithms such as ADAPT-VQE as shown in the following example.Firstly, we define some preliminary variables needed for VQE:
symbols = ["H", "H", "H"] geometry = np.array([[0.01076341, 0.04449877, 0.0], [0.98729513, 1.63059094, 0.0], [1.87262415, -0.00815842, 0.0]], requires_grad=False) H, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, charge = 1)
The collection of gates to grow the circuit is built to contain all single and double excitations:
n_electrons = 2 singles, doubles = qml.qchem.excitations(n_electrons, qubits) singles_excitations = [qml.SingleExcitation(0.0, x) for x in singles] doubles_excitations = [qml.DoubleExcitation(0.0, x) for x in doubles] operator_pool = doubles_excitations + singles_excitations
Next, an initial circuit that prepares a Hartree-Fock state and returns the expectation value of the Hamiltonian is defined:
hf_state = qml.qchem.hf_state(n_electrons, qubits) dev = qml.device("default.qubit", wires=qubits) @qml.qnode(dev) def circuit(): qml.BasisState(hf_state, wires=range(qubits)) return qml.expval(H)
Finally, the optimizer is instantiated and then the circuit is created and optimized adaptively:
opt = qml.optimize.AdaptiveOptimizer() for i in range(len(operator_pool)): circuit, energy, gradient = opt.step_and_cost(circuit, operator_pool, drain_pool=True) print('Energy:', energy) print(qml.draw(circuit)()) print('Largest Gradient:', gradient) print() if gradient < 1e-3: break
Energy: -1.246549938420637 0: ─╭BasisState(M0)─╭G²(0.20)─┤ ╭<𝓗> 1: ─├BasisState(M0)─├G²(0.20)─┤ ├<𝓗> 2: ─├BasisState(M0)─│─────────┤ ├<𝓗> 3: ─├BasisState(M0)─│─────────┤ ├<𝓗> 4: ─├BasisState(M0)─├G²(0.20)─┤ ├<𝓗> 5: ─╰BasisState(M0)─╰G²(0.20)─┤ ╰<𝓗> Largest Gradient: 0.14399872776755085 Energy: -1.2613740231529604 0: ─╭BasisState(M0)─╭G²(0.20)─╭G²(0.19)─┤ ╭<𝓗> 1: ─├BasisState(M0)─├G²(0.20)─├G²(0.19)─┤ ├<𝓗> 2: ─├BasisState(M0)─│─────────├G²(0.19)─┤ ├<𝓗> 3: ─├BasisState(M0)─│─────────╰G²(0.19)─┤ ├<𝓗> 4: ─├BasisState(M0)─├G²(0.20)───────────┤ ├<𝓗> 5: ─╰BasisState(M0)─╰G²(0.20)───────────┤ ╰<𝓗> Largest Gradient: 0.1349349562423238 Energy: -1.2743971719780331 0: ─╭BasisState(M0)─╭G²(0.20)─╭G²(0.19)──────────┤ ╭<𝓗> 1: ─├BasisState(M0)─├G²(0.20)─├G²(0.19)─╭G(0.00)─┤ ├<𝓗> 2: ─├BasisState(M0)─│─────────├G²(0.19)─│────────┤ ├<𝓗> 3: ─├BasisState(M0)─│─────────╰G²(0.19)─╰G(0.00)─┤ ├<𝓗> 4: ─├BasisState(M0)─├G²(0.20)────────────────────┤ ├<𝓗> 5: ─╰BasisState(M0)─╰G²(0.20)────────────────────┤ ╰<𝓗> Largest Gradient: 0.00040841755397108586
For a detailed breakdown of its implementation, check out the Adaptive circuits for quantum chemistry demo.
Automatic interface detection 🧩
QNodes now accept an
auto
interface argument which automatically detects the machine learning library to use. (#3132)from pennylane import numpy as np import torch import tensorflow as tf from jax import numpy as jnp dev = qml.device("default.qubit", wires=2) @qml.qnode(dev, interface="auto") def circuit(weight): qml.RX(weight[0], wires=0) qml.RY(weight[1], wires=1) return qml.expval(qml.PauliZ(0)) interface_tensors = [[0, 1], np.array([0, 1]), torch.Tensor([0, 1]), tf.Variable([0, 1], dtype=float), jnp.array([0, 1])] for tensor in interface_tensors: res = circuit(weight=tensor) print(f"Result value: {res:.2f}; Result type: {type(res)}")
Result value: 1.00; Result type: <class 'pennylane.numpy.tensor.tensor'> Result value: 1.00; Result type: <class 'pennylane.numpy.tensor.tensor'> Result value: 1.00; Result type: <class 'torch.Tensor'> Result value: 1.00; Result type: <class 'tensorflow.python.framework.ops.EagerTensor'> Result value: 1.00; Result type: <class 'jaxlib.xla_extension.Array'>
Upgraded JAX-JIT gradient support 🏎
JAX-JIT support for computing the gradient of QNodes that return a single vector of probabilities or multiple expectation values is now available. (#3244) (#3261)
import jax from jax import numpy as jnp from jax.config import config config.update("jax_enable_x64", True) dev = qml.device("lightning.qubit", wires=2) @jax.jit @qml.qnode(dev, diff_method="parameter-shift", interface="jax") def circuit(x, y): qml.RY(x, wires=0) qml.RY(y, wires=1) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) x = jnp.array(1.0) y = jnp.array(2.0)
>>> jax.jacobian(circuit, argnums=[0, 1])(x, y) (Array([-0.84147098, 0.35017549], dtype=float64, weak_type=True), Array([ 4.47445479e-18, -4.91295496e-01], dtype=float64, weak_type=True))
Note that this change depends on
jax.pure_callback
, which requiresjax>=0.3.17
.
Construct Pauli words and sentences 🔤
We’ve reorganized and grouped everything in PennyLane responsible for manipulating Pauli operators into a
pauli
module. Thegrouping
module has been deprecated as a result, and logic was moved frompennylane/grouping
topennylane/pauli/grouping
. (#3179)qml.pauli.PauliWord
andqml.pauli.PauliSentence
can be used to represent tensor products and linear combinations of Pauli operators, respectively. These provide a more performant method to compute sums and products of Pauli operators. (#3195)qml.pauli.PauliWord
represents tensor products of Pauli operators. We can efficiently multiply and extract the matrix of these operators using this representation.>>> pw1 = qml.pauli.PauliWord({0:"X", 1:"Z"}) >>> pw2 = qml.pauli.PauliWord({0:"Y", 1:"Z"}) >>> pw1, pw2 (X(0) @ Z(1), Y(0) @ Z(1)) >>> pw1 * pw2 (Z(0), 1j) >>> pw1.to_mat(wire_order=[0,1]) array([[ 0, 0, 1, 0], [ 0, 0, 0, -1], [ 1, 0, 0, 0], [ 0, -1, 0, 0]])
qml.pauli.PauliSentence
represents linear combinations of Pauli words. We can efficiently add, multiply and extract the matrix of these operators in this representation.>>> ps1 = qml.pauli.PauliSentence({pw1: 1.2, pw2: 0.5j}) >>> ps2 = qml.pauli.PauliSentence({pw1: -1.2}) >>> ps1 1.2 * X(0) @ Z(1) + 0.5j * Y(0) @ Z(1) >>> ps1 + ps2 0.0 * X(0) @ Z(1) + 0.5j * Y(0) @ Z(1) >>> ps1 * ps2 -1.44 * I + (-0.6+0j) * Z(0) >>> (ps1 + ps2).to_mat(wire_order=[0,1]) array([[ 0. +0.j, 0. +0.j, 0.5+0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, -0.5+0.j], [-0.5+0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0.5+0.j, 0. +0.j, 0. +0.j]])
(Experimental) More support for multi-measurement and gradient output types 🧪
qml.enable_return()
now supports QNodes returning multiple measurements, including shots vectors, and gradient output types. (#2886) (#3052) (#3041) (#3090) (#3069) (#3137) (#3127) (#3099) (#3098) (#3095) (#3091) (#3176) (#3170) (#3194) (#3267) (#3234) (#3232) (#3223) (#3222) (#3315)In v0.25, we introduced
qml.enable_return()
, which separates measurements into their own tensors. The motivation of this change is the deprecation of raggedndarray
creation in NumPy.With this release, we’re continuing to elevate this feature by adding support for:
Execution (
qml.execute
)Jacobian vector product (JVP) computation
Gradient transforms (
qml.gradients.param_shift
,qml.gradients.finite_diff
,qml.gradients.hessian_transform
,qml.gradients.param_shift_hessian
).Interfaces (Autograd, TensorFlow, and JAX, although without JIT)
With this added support, the JAX interface can handle multiple shots (shots vectors), measurements, and gradient output types with
qml.enable_return()
:import jax qml.enable_return() dev = qml.device("default.qubit", wires=2, shots=(1, 10000)) params = jax.numpy.array([0.1, 0.2]) @qml.qnode(dev, interface="jax", diff_method="parameter-shift", max_diff=2) def circuit(x): qml.RX(x[0], wires=[0]) qml.RY(x[1], wires=[1]) qml.CNOT(wires=[0, 1]) return qml.var(qml.PauliZ(0) @ qml.PauliX(1)), qml.probs(wires=[0])
>>> jax.hessian(circuit)(params) ((Array([[ 0., 0.], [ 2., -3.]], dtype=float32), Array([[[-0.5, 0. ], [ 0. , 0. ]], [[ 0.5, 0. ], [ 0. , 0. ]]], dtype=float32)), (Array([[ 0.07677898, 0.0563341 ], [ 0.07238522, -1.830669 ]], dtype=float32), Array([[[-4.9707499e-01, 2.9999996e-04], [-6.2500127e-04, 1.2500001e-04]], [[ 4.9707499e-01, -2.9999996e-04], [ 6.2500127e-04, -1.2500001e-04]]], dtype=float32)))
For more details, please refer to the documentation.
New basis rotation and tapering features in qml.qchem 🤓
Grouped coefficients, observables, and basis rotation transformation matrices needed to construct a qubit Hamiltonian in the rotated basis of molecular orbitals are now calculable via
qml.qchem.basis_rotation()
. (#3011)>>> symbols = ['H', 'H'] >>> geometry = np.array([[0.0, 0.0, 0.0], [1.398397361, 0.0, 0.0]], requires_grad = False) >>> mol = qml.qchem.Molecule(symbols, geometry) >>> core, one, two = qml.qchem.electron_integrals(mol)() >>> coeffs, ops, unitaries = qml.qchem.basis_rotation(one, two, tol_factor=1.0e-5) >>> unitaries [tensor([[-1.00000000e+00, -5.46483514e-13], [ 5.46483514e-13, -1.00000000e+00]], requires_grad=True), tensor([[-1.00000000e+00, 3.17585063e-14], [-3.17585063e-14, -1.00000000e+00]], requires_grad=True), tensor([[-0.70710678, -0.70710678], [-0.70710678, 0.70710678]], requires_grad=True), tensor([[ 2.58789009e-11, 1.00000000e+00], [-1.00000000e+00, 2.58789009e-11]], requires_grad=True)]
Any gate operation can now be tapered according to \(\mathbb{Z}_2\) symmetries of the Hamiltonian via
qml.qchem.taper_operation
. (#3002) (#3121)>>> symbols = ['He', 'H'] >>> geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4589]]) >>> mol = qml.qchem.Molecule(symbols, geometry, charge=1) >>> H, n_qubits = qml.qchem.molecular_hamiltonian(symbols, geometry) >>> generators = qml.qchem.symmetry_generators(H) >>> paulixops = qml.qchem.paulix_ops(generators, n_qubits) >>> paulix_sector = qml.qchem.optimal_sector(H, generators, mol.n_electrons) >>> tap_op = qml.qchem.taper_operation(qml.SingleExcitation, generators, paulixops, ... paulix_sector, wire_order=H.wires, op_wires=[0, 2]) >>> tap_op(3.14159) [Exp(1.5707949999999993j PauliY)]
Moreover, the obtained tapered operation can be used directly within a QNode.
>>> dev = qml.device('default.qubit', wires=[0, 1]) >>> @qml.qnode(dev) ... def circuit(params): ... tap_op(params[0]) ... return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) >>> drawer = qml.draw(circuit, show_all_wires=True) >>> print(drawer(params=[3.14159])) 0: ──Exp(0.00+1.57j Y)─┤ ╭<[email protected]> 1: ────────────────────┤ ╰<[email protected]>
Functionality has been added to estimate the number of measurements required to compute an expectation value with a target error and estimate the error in computing an expectation value with a given number of measurements. (#3000)
New functions, operations, and observables 🤩
Wires of operators or entire QNodes can now be mapped to other wires via
qml.map_wires()
. (#3143) (#3145)The
qml.map_wires()
function requires a dictionary representing a wire map. Use it witharbitrary operators:
>>> op = qml.RX(0.54, wires=0) + qml.PauliX(1) + (qml.PauliZ(2) @ qml.RY(1.23, wires=3)) >>> op (RX(0.54, wires=[0]) + PauliX(wires=[1])) + (PauliZ(wires=[2]) @ RY(1.23, wires=[3])) >>> wire_map = {0: 10, 1: 11, 2: 12, 3: 13} >>> qml.map_wires(op, wire_map) (RX(0.54, wires=[10]) + PauliX(wires=[11])) + (PauliZ(wires=[12]) @ RY(1.23, wires=[13]))
A
map_wires
method has also been added to operators, which returns a copy of the operator with its wires changed according to the given wire map.entire QNodes:
dev = qml.device("default.qubit", wires=["A", "B", "C", "D"]) wire_map = {0: "A", 1: "B", 2: "C", 3: "D"} @qml.qnode(dev) def circuit(): qml.RX(0.54, wires=0) qml.PauliX(1) qml.PauliZ(2) qml.RY(1.23, wires=3) return qml.probs(wires=0)
>>> mapped_circuit = qml.map_wires(circuit, wire_map) >>> mapped_circuit() tensor([0.92885434, 0.07114566], requires_grad=True) >>> print(qml.draw(mapped_circuit)()) A: ──RX(0.54)─┤ Probs B: ──X────────┤ C: ──Z────────┤ D: ──RY(1.23)─┤
The
qml.IntegerComparator
arithmetic operation is now available. (#3113)Given a basis state \(\vert n \rangle\), where \(n\) is a positive integer, and a fixed positive integer \(L\),
qml.IntegerComparator
flips a target qubit if \(n \geq L\). Alternatively, the flipping condition can be \(n < L\) as demonstrated below:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(): qml.BasisState(np.array([0, 1]), wires=range(2)) qml.broadcast(qml.Hadamard, wires=range(2), pattern='single') qml.IntegerComparator(2, geq=False, wires=[0, 1]) return qml.state()
>>> circuit() [-0.5+0.j 0.5+0.j -0.5+0.j 0.5+0.j]
The
qml.GellMann
qutrit observable, the ternary generalization of the Pauli observables, is now available. (#3035)When using
qml.GellMann
, theindex
keyword argument determines which of the 8 Gell-Mann matrices is used.dev = qml.device("default.qutrit", wires=2) @qml.qnode(dev) def circuit(): qml.TClock(wires=0) qml.TShift(wires=1) qml.TAdd(wires=[0, 1]) return qml.expval(qml.GellMann(wires=0, index=8) + qml.GellMann(wires=1, index=3))
>>> circuit() -0.42264973081037416
Controlled qutrit operations can now be performed with
qml.ControlledQutritUnitary
. (#2844)The control wires and values that define the operation are defined analogously to the qubit operation.
dev = qml.device("default.qutrit", wires=3) @qml.qnode(dev) def circuit(U): qml.TShift(wires=0) qml.TAdd(wires=[0, 1]) qml.ControlledQutritUnitary(U, control_wires=[0, 1], control_values='12', wires=2) return qml.state()
>>> U = np.array([[1, 1, 0], [1, -1, 0], [0, 0, np.sqrt(2)]]) / np.sqrt(2) >>> circuit(U) tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], requires_grad=True)
Improvements
PennyLane now supports Python 3.11! (#3297)
qml.sample
andqml.counts
work more efficiently and track if computational basis samples are being generated when they are called without specifying an observable. (#3207)The parameters of a basis set containing a different number of Gaussian functions are now easier to differentiate. (#3213)
Printing a
qml.MultiControlledX
operator now shows thecontrol_values
keyword argument. (#3113)qml.simplify
and transforms likeqml.matrix
,batch_transform
,hamiltonian_expand
, andsplit_non_commuting
now work withQuantumScript
as well asQuantumTape
. (#3209)A redundant flipping of the initial state in the UCCSD and kUpCCGSD templates has been removed. (#3148)
qml.adjoint
now supports batching if the base operation supports batching. (#3168)qml.OrbitalRotation
is now decomposed into twoqml.SingleExcitation
operations for faster execution and more efficient parameter-shift gradient calculations on devices that natively supportqml.SingleExcitation
. (#3171)The
Exp
class decomposes into aPauliRot
class if the coefficient is imaginary and the base operator is a Pauli Word. (#3249)Added the operator attributes
has_decomposition
andhas_adjoint
that indicate whether a correspondingdecomposition
oradjoint
method is available. (#2986)Structural improvements are made to
QueuingManager
, formerlyQueuingContext
, andAnnotatedQueue
. (#2794) (#3061) (#3085)QueuingContext
is renamed toQueuingManager
.QueuingManager
should now be the global communication point for putting queuable objects into the active queue.QueuingManager
is no longer an abstract base class.AnnotatedQueue
and its children no longer inherit fromQueuingManager
.QueuingManager
is no longer a context manager.Recording queues should start and stop recording via the
QueuingManager.add_active_queue
andQueuingContext.remove_active_queue
class methods instead of directly manipulating the_active_contexts
property.AnnotatedQueue
and its children no longer provide global information about actively recording queues. This information is now only available throughQueuingManager
.AnnotatedQueue
and its children no longer have the private_append
,_remove
,_update_info
,_safe_update_info
, and_get_info
methods. The public analogues should be used instead.QueuingManager.safe_update_info
andAnnotatedQueue.safe_update_info
are deprecated. Their functionality is moved toupdate_info
.
qml.Identity
now accepts multiple wires.
>>> id_op = qml.Identity([0, 1]) >>> id_op.matrix() array([[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]]) >>> id_op.sparse_matrix() <4x4 sparse matrix of type '<class 'numpy.float64'>' with 4 stored elements in Compressed Sparse Row format> >>> id_op.eigvals() array([1., 1., 1., 1.])
Added
unitary_check
keyword argument to the constructor of theQubitUnitary
class which indicates whether the user wants to check for unitarity of the input matrix or not. Its default value isfalse
. (#3063)Modified the representation of
WireCut
by usingqml.draw_mpl
. (#3067)Improved the performance of
qml.math.expand_matrix
function for dense and sparse matrices. (#3060) (#3064)Added support for sums and products of operator classes with scalar tensors of any interface (NumPy, JAX, Tensorflow, PyTorch…). (#3149)
>>> s_prod = torch.tensor(4) * qml.RX(1.23, 0) >>> s_prod 4*(RX(1.23, wires=[0])) >>> s_prod.scalar tensor(4)
Added
overlapping_ops
property to theComposite
class to improve the performance of theeigvals
,diagonalizing_gates
andProd.matrix
methods. (#3084)Added the
map_wires
method to the operators, which returns a copy of the operator with its wires changed according to the given wire map. (#3143)>>> op = qml.Toffoli([0, 1, 2]) >>> wire_map = {0: 2, 2: 0} >>> op.map_wires(wire_map=wire_map) Toffoli(wires=[2, 1, 0])
Calling
compute_matrix
andcompute_sparse_matrix
of simple non-parametric operations is now faster and more memory-efficient with the addition of caching. (#3134)Added details to the output of
Exp.label()
. (#3126)qml.math.unwrap
no longer creates ragged arrays. Lists remain lists. (#3163)New
null.qubit
device. Thenull.qubit
performs no operations or memory allocations. (#2589)default.qubit
favours decomposition and avoids matrix construction forQFT
andGroverOperator
at larger qubit numbers. (#3193)qml.ControlledQubitUnitary
now has acontrol_values
property. (#3206)Added a new
qml.tape.QuantumScript
class that contains all the non-queuing behavior ofQuantumTape
. Now,QuantumTape
inherits fromQuantumScript
as well asAnnotatedQueue
. (#3097)Extended the
qml.equal
function to MeasurementProcesses (#3189)qml.drawer.draw.draw_mpl
now accepts astyle
kwarg to select a style for plotting, rather than callingqml.drawer.use_style(style)
before plotting. Setting a style fordraw_mpl
does not change the global configuration for matplotlib plotting. If nostyle
is passed, the function defaults to plotting with theblack_white
style. (#3247)
Breaking changes
QuantumTape._par_info
is now a list of dictionaries, instead of a dictionary whose keys are integers starting from zero. (#3185)QueuingContext
has been renamed toQueuingManager
. (#3061)Deprecation patches for the return types enum’s location and
qml.utils.expand
are removed. (#3092)_multi_dispatch
functionality has been moved inside theget_interface
function. This function can now be called with one or multiple tensors as arguments. (#3136)>>> torch_scalar = torch.tensor(1) >>> torch_tensor = torch.Tensor([2, 3, 4]) >>> numpy_tensor = np.array([5, 6, 7]) >>> qml.math.get_interface(torch_scalar) 'torch' >>> qml.math.get_interface(numpy_tensor) 'numpy'
_multi_dispatch
previously had only one argument which contained a list of the tensors to be dispatched:>>> qml.math._multi_dispatch([torch_scalar, torch_tensor, numpy_tensor]) 'torch'
To differentiate whether the user wants to get the interface of a single tensor or multiple tensors,
get_interface
now accepts a different argument per tensor to be dispatched:>>> qml.math.get_interface(*[torch_scalar, torch_tensor, numpy_tensor]) 'torch' >>> qml.math.get_interface(torch_scalar, torch_tensor, numpy_tensor) 'torch'
Operator.compute_terms
is removed. On a specific instance of an operator,op.terms()
can be used instead. There is no longer a static method for this. (#3215)
Deprecations
QueuingManager.safe_update_info
andAnnotatedQueue.safe_update_info
are deprecated. Instead,update_info
no longer raises errorsif the object isn’t in the queue. (#3085)
qml.tape.stop_recording
andQuantumTape.stop_recording
have been moved toqml.QueuingManager.stop_recording
. The old functions will still be available until v0.29. (#3068)qml.tape.get_active_tape
has been deprecated. Useqml.QueuingManager.active_context()
instead. (#3068)Operator.compute_terms
has been removed. On a specific instance of an operator, useop.terms()
instead. There is no longer a static method for this. (#3215)qml.tape.QuantumTape.inv()
has been deprecated. Useqml.tape.QuantumTape.adjoint
instead. (#3237)qml.transforms.qcut.remap_tape_wires
has been deprecated. Useqml.map_wires
instead. (#3186)The grouping module
qml.grouping
has been deprecated. Useqml.pauli
orqml.pauli.grouping
instead. The module will still be available until v0.28. (#3262)
Documentation
The code block in the usage details of the UCCSD template has been updated. (#3140)
Added a “Deprecations” page to the developer documentation. (#3093)
The example of the
qml.FlipSign
template has been updated. (#3219)
Bug fixes
qml.SparseHamiltonian
now validates the size of the input matrix. (#3278)Users no longer see unintuitive errors when inputing sequences to
qml.Hermitian
. (#3181)The evaluation of QNodes that return either
vn_entropy
ormutual_info
raises an informative error message when using devices that define a vector of shots. (#3180)Fixed a bug that made
qml.AmplitudeEmbedding
incompatible with JITting. (#3166)Fixed the
qml.transforms.transpile
transform to work correctly for all two-qubit operations. (#3104)Fixed a bug with the control values of a controlled version of a
ControlledQubitUnitary
. (#3119)Fixed a bug where
qml.math.fidelity(non_trainable_state, trainable_state)
failed unexpectedly. (#3160)Fixed a bug where
qml.QueuingManager.stop_recording
did not clean up if yielded code raises an exception. (#3182)Returning
qml.sample()
orqml.counts()
with other measurements of non-commuting observables now raises a QuantumFunctionError (e.g.,return qml.expval(PauliX(wires=0)), qml.sample()
now raises an error). (#2924)Fixed a bug where
op.eigvals()
would return an incorrect result if the operator was a non-hermitian composite operator. (#3204)Fixed a bug where
qml.BasisStatePreparation
andqml.BasisEmbedding
were not jit-compilable with JAX. (#3239)Fixed a bug where
qml.MottonenStatePreparation
was not jit-compilable with JAX. (#3260)Fixed a bug where
qml.MottonenStatePreparation
was not jit-compilable with JAX. (#3260)Fixed a bug where
qml.expval(qml.Hamiltonian())
would not raise an error if the Hamiltonian involved some wires that are not present on the device. (#3266)Fixed a bug where
qml.tape.QuantumTape.shape()
did not account for the batch dimension of the tape (#3269)
Contributors
This release contains contributions from (in alphabetical order):
Kamal Mohamed Ali, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Utkarsh Azad, Thomas Bromley, Albert Mitjans Coma, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Lillian M. A. Frederiksen, Diego Guala, Josh Izaac, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Christina Lee, Romain Moyard, Lee J. O’Riordan, Mudit Pandey, Matthew Silverman, Jay Soni, Antal Száva, David Wierichs,
- orphan
Release 0.26.0¶
New features since last release
Classical shadows 👤
PennyLane now provides built-in support for implementing the classical-shadows measurement protocol. (#2820) (#2821) (#2871) (#2968) (#2959) (#2968)
The classical-shadow measurement protocol is described in detail in the paper Predicting Many Properties of a Quantum System from Very Few Measurements. As part of the support for classical shadows in this release, two new finite-shot and fully-differentiable measurements are available:
QNodes returning the new measurement
qml.classical_shadow()
will return two entities;bits
(0 or 1 if the 1 or -1 eigenvalue is sampled, respectively) andrecipes
(the randomized Pauli measurements that are performed for each qubit, labelled by integer):dev = qml.device("default.qubit", wires=2, shots=3) @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) return qml.classical_shadow(wires=[0, 1])
>>> bits, recipes = circuit() >>> bits tensor([[0, 0], [1, 0], [0, 1]], dtype=uint8, requires_grad=True) >>> recipes tensor([[2, 2], [0, 2], [0, 2]], dtype=uint8, requires_grad=True)
QNodes returning
qml.shadow_expval()
yield the expectation value estimation using classical shadows:dev = qml.device("default.qubit", wires=range(2), shots=10000) @qml.qnode(dev) def circuit(x, H): qml.Hadamard(0) qml.CNOT((0,1)) qml.RX(x, wires=0) return qml.shadow_expval(H) x = np.array(0.5, requires_grad=True) H = qml.Hamiltonian( [1., 1.], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliX(1)] )
>>> circuit(x, H) tensor(1.8486, requires_grad=True) >>> qml.grad(circuit)(x, H) -0.4797000000000001
Fully-differentiable QNode transforms for both new classical-shadows measurements are also available via
qml.shadows.shadow_state
andqml.shadows.shadow_expval
, respectively.For convenient post-processing, we’ve also added the ability to calculate general Renyi entropies by way of the
ClassicalShadow
class’entropy
method, which requires the wires of the subsystem of interest and the Renyi entropy order:>>> shadow = qml.ClassicalShadow(bits, recipes) >>> vN_entropy = shadow.entropy(wires=[0, 1], alpha=1)
Qutrits: quantum circuits for tertiary degrees of freedom ☘️
An entirely new framework for quantum computing is now simulatable with the addition of qutrit functionalities. (#2699) (#2781) (#2782) (#2783) (#2784) (#2841) (#2843)
Qutrits are like qubits, but instead live in a three-dimensional Hilbert space; they are not binary degrees of freedom, they are tertiary. The advent of qutrits allows for all sorts of interesting theoretical, practical, and algorithmic capabilities that have yet to be discovered.
To facilitate qutrit circuits requires a new device:
default.qutrit
. Thedefault.qutrit
device is a Python-based simulator, akin todefault.qubit
, and is defined as per usual:>>> dev = qml.device("default.qutrit", wires=1)
The following operations are supported on
default.qutrit
devices:The qutrit shift operator,
qml.TShift
, and the ternary clock operator,qml.TClock
, as defined in this paper by Yeh et al. (2022), which are the qutrit analogs of the Pauli X and Pauli Z operations, respectively.The
qml.TAdd
andqml.TSWAP
operations which are the qutrit analogs of the CNOT and SWAP operations, respectively.Custom unitary operations via
qml.QutritUnitary
.qml.state
andqml.probs
measurements.Measuring user-specified Hermitian matrix observables via
qml.THermitian
.
A comprehensive example of these features is given below:
dev = qml.device("default.qutrit", wires=1) U = np.array([ [1, 1, 1], [1, 1, 1], [1, 1, 1] ] ) / np.sqrt(3) obs = np.array([ [1, 1, 0], [1, -1, 0], [0, 0, np.sqrt(2)] ] ) / np.sqrt(2) @qml.qnode(dev) def qutrit_state(U, obs): qml.TShift(0) qml.TClock(0) qml.QutritUnitary(U, wires=0) return qml.state() @qml.qnode(dev) def qutrit_expval(U, obs): qml.TShift(0) qml.TClock(0) qml.QutritUnitary(U, wires=0) return qml.expval(qml.THermitian(obs, wires=0))
>>> qutrit_state(U, obs) tensor([-0.28867513+0.5j, -0.28867513+0.5j, -0.28867513+0.5j], requires_grad=True) >>> qutrit_expval(U, obs) tensor(0.80473785, requires_grad=True)
We will continue to add more and more support for qutrits in future releases.
Simplifying just got... simpler 😌
The
qml.simplify()
function has several intuitive improvements with this release. (#2978) (#2982) (#2922) (#3012)qml.simplify
can now perform the following:simplify parametrized operations
simplify the adjoint and power of specific operators
group like terms in a sum
resolve products of Pauli operators
combine rotation angles of identical rotation gates
Here is an example of
qml.simplify
in action with parameterized rotation gates. In this case, the angles of rotation are simplified to be modulo \(4\pi\).>>> op1 = qml.RX(30.0, wires=0) >>> qml.simplify(op1) RX(4.867258771281655, wires=[0]) >>> op2 = qml.RX(4 * np.pi, wires=0) >>> qml.simplify(op2) Identity(wires=[0])
All of these simplification features can be applied directly to quantum functions, QNodes, and tapes via decorating with
@qml.simplify
, as well:dev = qml.device("default.qubit", wires=2) @qml.simplify @qml.qnode(dev) def circuit(): qml.adjoint(qml.prod(qml.RX(1, 0) ** 1, qml.RY(1, 0), qml.RZ(1, 0))) return qml.probs(wires=0)
>>> circuit() >>> list(circuit.tape) [RZ(11.566370614359172, wires=[0]) @ RY(11.566370614359172, wires=[0]) @ RX(11.566370614359172, wires=[0]), probs(wires=[0])]
QNSPSA optimizer 💪
A new optimizer called
qml.QNSPSAOptimizer
is available that implements the quantum natural simultaneous perturbation stochastic approximation (QNSPSA) method based on Simultaneous Perturbation Stochastic Approximation of the Quantum Fisher Information. (#2818)qml.QNSPSAOptimizer
is a second-order SPSA algorithm, which combines the convergence power of the quantum-aware Quantum Natural Gradient (QNG) optimization method with the reduced quantum evaluations of SPSA methods.While the QNSPSA optimizer requires additional circuit executions (10 executions per step) compared to standard SPSA optimization (3 executions per step), these additional evaluations are used to provide a stochastic estimation of a second-order metric tensor, which often helps the optimizer to achieve faster convergence.
Use
qml.QNSPSAOptimizer
like you would any other optimizer:max_iterations = 50 opt = qml.QNSPSAOptimizer() for _ in range(max_iterations): params, cost = opt.step_and_cost(cost, params)
Check out our demo on the QNSPSA optimizer for more information.
Operator and parameter broadcasting supplements 📈
Operator methods for exponentiation and raising to a power have been added. (#2799) (#3029)
The
qml.exp
function can be used to create observables or generic rotation gates:>>> x = 1.234 >>> t = qml.PauliX(0) @ qml.PauliX(1) + qml.PauliY(0) @ qml.PauliY(1) >>> isingxy = qml.exp(t, 0.25j * x) >>> isingxy.matrix() array([[1. +0.j , 0. +0.j , 1. +0.j , 0. +0.j ], [0. +0.j , 0.8156179+0.j , 1. +0.57859091j, 0. +0.j ], [0. +0.j , 0. +0.57859091j, 0.8156179+0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 1. +0.j , 1. +0.j ]])
The
qml.pow
function raises a given operator to a power:>>> op = qml.pow(qml.PauliX(0), 2) >>> op.matrix() array([[1, 0], [0, 1]])
An operator called
qml.PSWAP
is now available. (#2667)The
qml.PSWAP
gate – or phase-SWAP gate – was previously available within the PennyLane-Braket plugin only. Enjoy it natively in PennyLane with v0.26.Check whether or not an operator is hermitian or unitary with
qml.is_hermitian
andqml.is_unitary
. (#2960)>>> op1 = qml.PauliX(wires=0) >>> qml.is_hermitian(op1) True >>> op2 = qml.PauliX(0) + qml.RX(np.pi/3, 0) >>> qml.is_unitary(op2) False
Embedding templates now support parameter broadcasting. (#2810)
Embedding templates like
AmplitudeEmbedding
orIQPEmbedding
now support parameter broadcasting with a leading broadcasting dimension in their variational parameters.AmplitudeEmbedding
, for example, would usually use a one-dimensional input vector of features. With broadcasting, we can now compute>>> features = np.array([ ... [0.5, 0.5, 0., 0., 0.5, 0., 0.5, 0.], ... [1., 0., 0., 0., 0., 0., 0., 0.], ... [0.5, 0.5, 0., 0., 0., 0., 0.5, 0.5], ... ]) >>> op = qml.AmplitudeEmbedding(features, wires=[1, 5, 2]) >>> op.batch_size 3
An exception is
BasisEmbedding
, which is not broadcastable.
Improvements
The
qml.math.expand_matrix()
method now allows the sparse matrix representation of an operator to be extended to a larger hilbert space. (#2998)>>> from scipy import sparse >>> mat = sparse.csr_matrix([[0, 1], [1, 0]]) >>> qml.math.expand_matrix(mat, wires=[1], wire_order=[0,1]).toarray() array([[0., 1., 0., 0.], [1., 0., 0., 0.], [0., 0., 0., 1.], [0., 0., 1., 0.]])
qml.ctrl
now usesControlled
instead ofControlledOperation
. The newControlled
class wraps individualOperator
‘s instead of a tape. It provides improved representations and integration. (#2990)qml.matrix
can now compute the matrix of tapes and QNodes that contain multiple broadcasted operations or non-broadcasted operations after broadcasted ones. (#3025)A common scenario in which this becomes relevant is the decomposition of broadcasted operations: the decomposition in general will contain one or multiple broadcasted operations as well as operations with no or fixed parameters that are not broadcasted.
Lists of operators are now internally sorted by their respective wires while also taking into account their commutativity property. (#2995)
Some methods of the
QuantumTape
class have been simplified and reordered to improve both readability and performance. (#2963)The
qml.qchem.molecular_hamiltonian
function is modified to support observable grouping. (#2997)qml.ops.op_math.Controlled
now has basic decomposition functionality. (#2938)Automatic circuit cutting has been improved by making better partition imbalance derivations. Now it is more likely to generate optimal cuts for larger circuits. (#2517)
By default,
qml.counts
only returns the outcomes observed in sampling. Optionally, specifyingqml.counts(all_outcomes=True)
will return a dictionary containing all possible outcomes. (#2889)>>> dev = qml.device("default.qubit", wires=2, shots=1000) >>> >>> @qml.qnode(dev) >>> def circuit(): ... qml.Hadamard(wires=0) ... qml.CNOT(wires=[0, 1]) ... return qml.counts(all_outcomes=True) >>> result = circuit() >>> result {'00': 495, '01': 0, '10': 0, '11': 505}
Internal use of in-place inversion is eliminated in preparation for its deprecation. (#2965)
Controlled
operators now work withqml.is_commuting
. (#2994)qml.prod
andqml.op_sum
now support thesparse_matrix()
method. (#3006)>>> xy = qml.prod(qml.PauliX(1), qml.PauliY(1)) >>> op = qml.op_sum(xy, qml.Identity(0)) >>> >>> sparse_mat = op.sparse_matrix(wire_order=[0,1]) >>> type(sparse_mat) <class 'scipy.sparse.csr.csr_matrix'> >>> sparse_mat.toarray() [[1.+1.j 0.+0.j 0.+0.j 0.+0.j] [0.+0.j 1.-1.j 0.+0.j 0.+0.j] [0.+0.j 0.+0.j 1.+1.j 0.+0.j] [0.+0.j 0.+0.j 0.+0.j 1.-1.j]]
Provided
sparse_matrix()
support for single qubit observables. (#2964)qml.Barrier
withonly_visual=True
now simplifies viaop.simplify()
to the identity operator or a product of identity operators. (#3016)More accurate and intuitive outputs for printing some operators have been added. (#3013)
Results for the matrix of the sum or product of operators are stored in a more efficient manner. (#3022)
The computation of the (sparse) matrix for the sum or product of operators is now more efficient. (#3030)
When the factors of
qml.prod
don’t share any wires, the matrix and sparse matrix are computed using a kronecker product for improved efficiency. (#3040)qml.grouping.is_pauli_word
now returnsFalse
for operators that don’t inherit fromqml.Observable
instead of raising an error. (#3039)Added functionality to iterate over operators created from
qml.op_sum
andqml.prod
. (#3028)>>> op = qml.op_sum(qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)) >>> len(op) 3 >>> op[1] PauliY(wires=[1]) >>> [o.name for o in op] ['PauliX', 'PauliY', 'PauliZ']
Deprecations
In-place inversion is now deprecated. This includes
op.inv()
andop.inverse=value
. Please useqml.adjoint
orqml.pow
instead. Support for these methods will remain till v0.28. (#2988)Don’t use:
>>> v1 = qml.PauliX(0).inv() >>> v2 = qml.PauliX(0) >>> v2.inverse = True
Instead use:
>>> qml.adjoint(qml.PauliX(0)) Adjoint(PauliX(wires=[0])) >>> qml.pow(qml.PauliX(0), -1) PauliX(wires=[0])**-1 >>> qml.pow(qml.PauliX(0), -1, lazy=False) PauliX(wires=[0]) >>> qml.PauliX(0) ** -1 PauliX(wires=[0])**-1
qml.adjoint
takes the conjugate transpose of an operator, whileqml.pow(op, -1)
indicates matrix inversion. For unitary operators,adjoint
will be more efficient thanqml.pow(op, -1)
, even though they represent the same thing.The
supports_reversible_diff
device capability is unused and has been removed. (#2993)
Breaking changes
Measuring an operator that might not be hermitian now raises a warning instead of an error. To definitively determine whether or not an operator is hermitian, use
qml.is_hermitian
. (#2960)The
ControlledOperation
class has been removed. This was a developer-only class, so the change should not be evident to any users. It is replaced byControlled
. (#2990)The default
execute
method for theQubitDevice
base class now callsself.statistics
with an additional keyword argumentcircuit
, which represents the quantum tape being executed. Any device that overridesstatistics
should edit the signature of the method to include the newcircuit
keyword argument. (#2820)The
expand_matrix()
has been moved frompennylane.operation
topennylane.math.matrix_manipulation
(#3008)qml.grouping.utils.is_commuting
has been removed, and its Pauli word logic is now part ofqml.is_commuting
. (#3033)qml.is_commuting
has been moved frompennylane.transforms.commutation_dag
topennylane.ops.functions
. (#2991)
Documentation
Updated the Fourier transform docs to use
circuit_spectrum
instead ofspectrum
, which has been deprecated. (#3018)Corrected the docstrings for diagonalizing gates for all relevant operations. The docstrings used to say that the diagonalizing gates implemented \(U\), the unitary such that \(O = U \Sigma U^{\dagger}\), where \(O\) is the original observable and \(\Sigma\) a diagonal matrix. However, the diagonalizing gates actually implement \(U^{\dagger}\), since \(\langle \psi | O | \psi \rangle = \langle \psi | U \Sigma U^{\dagger} | \psi \rangle\), making \(U^{\dagger} | \psi \rangle\) the actual state being measured in the Z-basis. (#2981)
Bug fixes
Fixed a bug with
qml.ops.Exp
operators when the coefficient is autograd but the diagonalizing gates don’t act on all wires. (#3057)Fixed a bug where the tape transform
single_qubit_fusion
computed wrong rotation angles for specific combinations of rotations. (#3024)Jax gradients now work with a QNode when the quantum function was transformed by
qml.simplify
. (#3017)Operators that have
num_wires = AnyWires
ornum_wires = AnyWires
now raise an error, with certain exceptions, when instantiated withwires=[]
. (#2979)Fixed a bug where printing
qml.Hamiltonian
with complex coefficients raisesTypeError
in some cases. (#3004)Added a more descriptive error message when measuring non-commuting observables at the end of a circuit with
probs
,samples
,counts
andallcounts
. (#3065)
Contributors
This release contains contributions from (in alphabetical order):
Juan Miguel Arrazola, Utkarsh Azad, Tom Bromley, Olivia Di Matteo, Isaac De Vlugt, Yiheng Duan, Lillian Marie Austin Frederiksen, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Meenu Kumari, Christina Lee, Albert Mitjans Coma, Romain Moyard, Rashid N H M, Zeyue Niu, Mudit Pandey, Matthew Silverman, Jay Soni, Antal Száva, Cody Wang, David Wierichs.
- orphan
Release 0.25.1¶
Bug fixes
Fixed Torch device discrepencies for certain parametrized operations by updating
qml.math.array
andqml.math.eye
to preserve the Torch device used. (#2967)
Contributors
This release contains contributions from (in alphabetical order):
Romain Moyard, Rashid N H M, Lee James O’Riordan, Antal Száva
- orphan
Release 0.25.0¶
New features since last release
Estimate computational resource requirements 🧠
Functionality for estimating molecular simulation computations has been added with
qml.resource
. (#2646) (#2653) (#2665) (#2694) (#2720) (#2723) (#2746) (#2796) (#2797) (#2874) (#2944) (#2644)The new resource module allows you to estimate the number of non-Clifford gates and logical qubits needed to implement quantum phase estimation algorithms for simulating materials and molecules. This includes support for quantum algorithms using first and second quantization with specific bases:
First quantization using a plane-wave basis via the
FirstQuantization
class:>>> n = 100000 # number of plane waves >>> eta = 156 # number of electrons >>> omega = 1145.166 # unit cell volume in atomic units >>> algo = FirstQuantization(n, eta, omega) >>> print(algo.gates, algo.qubits) 1.10e+13, 4416
Second quantization with a double-factorized Hamiltonian via the
DoubleFactorization
class:symbols = ["O", "H", "H"] geometry = np.array( [ [0.00000000, 0.00000000, 0.28377432], [0.00000000, 1.45278171, -1.00662237], [0.00000000, -1.45278171, -1.00662237], ], requires_grad=False, ) mol = qml.qchem.Molecule(symbols, geometry, basis_name="sto-3g") core, one, two = qml.qchem.electron_integrals(mol)() algo = DoubleFactorization(one, two)
>>> print(algo.gates, algo.qubits) 103969925, 290
The methods of the
FirstQuantization
and theDoubleFactorization
classes, such asqubit_cost
(number of logical qubits) andgate_cost
(number of non-Clifford gates), can be also accessed as static methods:>>> qml.resource.FirstQuantization.qubit_cost(100000, 156, 169.69608, 0.01) 4377 >>> qml.resource.FirstQuantization.gate_cost(100000, 156, 169.69608, 0.01) 3676557345574
Differentiable error mitigation ⚙️
Differentiable zero-noise-extrapolation (ZNE) error mitigation is now available. (#2757)
Elevate any variational quantum algorithm to a mitigated algorithm with improved results on noisy hardware while maintaining differentiability throughout.
In order to do so, use the
qml.transforms.mitigate_with_zne
transform on your QNode and provide the PennyLane proprietaryqml.transforms.fold_global
folding function andqml.transforms.poly_extrapolate
extrapolation function. Here is an example for a noisy simulation device where we mitigate a QNode and are still able to compute the gradient:# Describe noise noise_gate = qml.DepolarizingChannel noise_strength = 0.1 # Load devices dev_ideal = qml.device("default.mixed", wires=1) dev_noisy = qml.transforms.insert(noise_gate, noise_strength)(dev_ideal) scale_factors = [1, 2, 3] @mitigate_with_zne( scale_factors, qml.transforms.fold_global, qml.transforms.poly_extrapolate, extrapolate_kwargs={'order': 2} ) @qml.qnode(dev_noisy) def qnode_mitigated(theta): qml.RY(theta, wires=0) return qml.expval(qml.PauliX(0))
>>> theta = np.array(0.5, requires_grad=True) >>> qml.grad(qnode_mitigated)(theta) 0.5712737447327619
More native support for parameter broadcasting 📡
default.qubit
now natively supports parameter broadcasting, providing increased performance when executing the same circuit at various parameter positions compared to manually looping over parameters, or directly using theqml.transforms.broadcast_expand
transform. (#2627)dev = qml.device("default.qubit", wires=1) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) return qml.expval(qml.PauliZ(0))
>>> circuit(np.array([0.1, 0.3, 0.2])) tensor([0.99500417, 0.95533649, 0.98006658], requires_grad=True)
Currently, not all templates have been updated to support broadcasting.
Parameter-shift gradients now allow for parameter broadcasting internally, which can result in a significant speedup when computing gradients of circuits with many parameters. (#2749)
The gradient transform
qml.gradients.param_shift
now accepts the keyword argumentbroadcast
. If set toTrue
, broadcasting is used to compute the derivative:dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x, y): qml.RX(x, wires=0) qml.RY(y, wires=1) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
>>> x = np.array([np.pi/3, np.pi/2], requires_grad=True) >>> y = np.array([np.pi/6, np.pi/5], requires_grad=True) >>> qml.gradients.param_shift(circuit, broadcast=True)(x, y) (tensor([[-0.7795085, 0. ], [ 0. , -0.7795085]], requires_grad=True), tensor([[-0.125, 0. ], [0. , -0.125]], requires_grad=True))
The following example highlights how to make use of broadcasting gradients at the QNode level. Internally, broadcasting is used to compute the parameter-shift rule when required, which may result in performance improvements.
@qml.qnode(dev, diff_method="parameter-shift", broadcast=True) def circuit(x, y): qml.RX(x, wires=0) qml.RY(y, wires=1) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
>>> x = np.array(0.1, requires_grad=True) >>> y = np.array(0.4, requires_grad=True) >>> qml.grad(circuit)(x, y) (array(-0.09195267), array(-0.38747287))
Here, only 2 circuits are created internally, rather than 4 with
broadcast=False
.To illustrate the speedup, for a constant-depth circuit with Pauli rotations and controlled Pauli rotations, the time required to compute
qml.gradients.param_shift(circuit, broadcast=False)(params)
(“No broadcasting”) andqml.gradients.param_shift(circuit, broadcast=True)(params)
(“Broadcasting”) as a function of the number of qubits is given here.Operations for quantum chemistry now support parameter broadcasting. (#2726)
>>> op = qml.SingleExcitation(np.array([0.3, 1.2, -0.7]), wires=[0, 1]) >>> op.matrix().shape (3, 4, 4)
Intuitive operator arithmetic 🧮
New functionality for representing the sum, product, and scalar-product of operators is available. (#2475) (#2625) (#2622) (#2721)
The following functionalities have been added to facilitate creating new operators whose matrix, terms, and eigenvalues can be accessed as per usual, while maintaining differentiability. Operators created from these new features can be used within QNodes as operations or as observables (where physically applicable).
Summing any number of operators via
qml.op_sum
results in a “summed” operator:>>> ops_to_sum = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(0)] >>> summed_ops = qml.op_sum(*ops_to_sum) >>> summed_ops PauliX(wires=[0]) + PauliY(wires=[1]) + PauliZ(wires=[0]) >>> qml.matrix(summed_ops) array([[ 1.+0.j, 0.-1.j, 1.+0.j, 0.+0.j], [ 0.+1.j, 1.+0.j, 0.+0.j, 1.+0.j], [ 1.+0.j, 0.+0.j, -1.+0.j, 0.-1.j], [ 0.+0.j, 1.+0.j, 0.+1.j, -1.+0.j]]) >>> summed_ops.terms() ([1.0, 1.0, 1.0], (PauliX(wires=[0]), PauliY(wires=[1]), PauliZ(wires=[0])))
Multiplying any number of operators via
qml.prod
results in a “product” operator, where the matrix product or tensor product is used correspondingly:>>> theta = 1.23 >>> prod_op = qml.prod(qml.PauliZ(0), qml.RX(theta, 1)) >>> prod_op PauliZ(wires=[0]) @ RX(1.23, wires=[1]) >>> qml.eigvals(prod_op) [-1.39373197 -0.23981492 0.23981492 1.39373197]
Taking the product of a coefficient and an operator via
qml.s_prod
produces a “scalar-product” operator:>>> sprod_op = qml.s_prod(2.0, qml.PauliX(0)) >>> sprod_op 2.0*(PauliX(wires=[0])) >>> sprod_op.matrix() array([[ 0., 2.], [ 2., 0.]]) >>> sprod_op.terms() ([2.0], [PauliX(wires=[0])])
Each of these new functionalities can be used within QNodes as operators or observables, where applicable, while also maintaining differentiability. For example:
dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(angles): qml.prod(qml.PauliZ(0), qml.RY(angles[0], 1)) qml.op_sum(qml.PauliX(1), qml.RY(angles[1], 0)) return qml.expval(qml.op_sum(qml.PauliX(0), qml.PauliZ(1)))
>>> angles = np.array([1.23, 4.56], requires_grad=True) >>> circuit(angles) tensor(0.33423773, requires_grad=True) >>> qml.grad(circuit)(angles) array([-0.9424888, 0. ])
All PennyLane operators can now be added, subtracted, multiplied, scaled, and raised to powers using
+
,-
,@
,*
,**
, respectively. (#2849) (#2825) (#2891)You can now add scalars to operators, where the interpretation is that the scalar is a properly-sized identity matrix;
>>> sum_op = 5 + qml.PauliX(0) >>> sum_op.matrix() array([[5., 1.], [1., 5.]])
The
+
and-
operators can be used to combine all Pennylane operators:>>> sum_op = qml.RX(phi=1.23, wires=0) + qml.RZ(phi=3.14, wires=0) - qml.RY(phi=0.12, wires=0) >>> sum_op RX(1.23, wires=[0]) + RZ(3.14, wires=[0]) + -1*(RY(0.12, wires=[0])) >>> qml.matrix(sum_op) array([[-0.18063077-0.99999968j, 0.05996401-0.57695852j], [-0.05996401-0.57695852j, -0.18063077+0.99999968j]])
Note that the behavior of
+
and-
with observables is different; it still creates a Hamiltonian.The
*
and@
operators can be used to scale and compose all PennyLane operators.>>> prod_op = 2*qml.RX(1, wires=0) @ qml.RY(2, wires=0) >>> prod_op 2*(RX(1, wires=[0])) @ RY(2, wires=[0]) >>> qml.matrix(prod_op) array([[ 0.94831976-0.80684536j, -1.47692053-0.51806945j], [ 1.47692053-0.51806945j, 0.94831976+0.80684536j]])
The
**
operator can be used to raise PennyLane operators to a power.>>> exp_op = qml.RZ(1.0, wires=0) ** 2 >>> exp_op RZ**2(1.0, wires=[0]) >>> qml.matrix(exp_op) array([[0.54030231-0.84147098j, 0. +0.j ], [0. +0.j , 0.54030231+0.84147098j]])
A new class called
Controlled
is available inqml.ops.op_math
to represent a controlled version of any operator. This will eventually be integrated intoqml.ctrl
to provide a performance increase and more feature coverage. (#2634)Arithmetic operations can now be simplified using
qml.simplify
. (#2835) (#2854)>>> op = qml.adjoint(qml.adjoint(qml.RX(x, wires=0))) >>> op Adjoint(Adjoint(RX))(tensor([1.04719755, 1.57079633], requires_grad=True), wires=[0]) >>> qml.simplify(op) RX(tensor([1.04719755, 1.57079633], requires_grad=True), wires=[0])
A new function called
qml.equal
can be used to compare the equality of parametric operators. (#2651)>>> qml.equal(qml.RX(1.23, 0), qml.RX(1.23, 0)) True >>> qml.equal(qml.RY(4.56, 0), qml.RY(7.89, 0)) False
Marvelous mixed state features 🙌
The
default.mixed
device now supports backpropagation with the"jax"
interface, which can result in significant speedups. (#2754) (#2776)dev = qml.device("default.mixed", wires=2) @qml.qnode(dev, diff_method="backprop", interface="jax") def circuit(angles): qml.RX(angles[0], wires=0) qml.RY(angles[1], wires=1) return qml.expval(qml.PauliZ(0) + qml.PauliZ(1))
>>> angles = np.array([np.pi/6, np.pi/5], requires_grad=True) >>> qml.grad(circuit)(angles) array([-0.8660254 , -0.25881905])
Additionally, quantum channels now support Jax and TensorFlow tensors. This allows quantum channels to be used inside QNodes decorated by
tf.function
,jax.jit
, orjax.vmap
.The
default.mixed
device now supports readout error. (#2786)A new keyword argument called
readout_prob
can be specified when creating adefault.mixed
device. Any circuits running on adefault.mixed
device with a finitereadout_prob
(upper-bounded by 1) will alter the measurements performed at the end of the circuit similarly to how aqml.BitFlip
channel would affect circuit measurements:>>> dev = qml.device("default.mixed", wires=2, readout_prob=0.1) >>> @qml.qnode(dev) ... def circuit(): ... return qml.expval(qml.PauliZ(0)) >>> circuit() array(0.8)
Relative entropy is now available in qml.qinfo 💥
The quantum information module now supports computation of relative entropy. (#2772)
We’ve enabled two cases for calculating the relative entropy:
A QNode transform via
qml.qinfo.relative_entropy
:dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(param): qml.RY(param, wires=0) qml.CNOT(wires=[0, 1]) return qml.state()
>>> relative_entropy_circuit = qml.qinfo.relative_entropy(circuit, circuit, wires0=[0], wires1=[0]) >>> x, y = np.array(0.4), np.array(0.6) >>> relative_entropy_circuit((x,), (y,)) 0.017750012490703237
Support in
qml.math
for flexible post-processing:>>> rho = np.array([[0.3, 0], [0, 0.7]]) >>> sigma = np.array([[0.5, 0], [0, 0.5]]) >>> qml.math.relative_entropy(rho, sigma) tensor(0.08228288, requires_grad=True)
New measurements, operators, and more! ✨
A new measurement called
qml.counts
is available. (#2686) (#2839) (#2876)QNodes with
shots != None
that returnqml.counts
will yield a dictionary whose keys are bitstrings representing computational basis states that were measured, and whose values are the corresponding counts (i.e., how many times that computational basis state was measured):dev = qml.device("default.qubit", wires=2, shots=1000) @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) return qml.counts()
>>> circuit() {'00': 495, '11': 505}
qml.counts
can also accept observables, where the resulting dictionary is ordered by the eigenvalues of the observable.dev = qml.device("default.qubit", wires=2, shots=1000) @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) return qml.counts(qml.PauliZ(0)), qml.counts(qml.PauliZ(1))
>>> circuit() ({-1: 470, 1: 530}, {-1: 470, 1: 530})
A new experimental return type for QNodes with multiple measurements has been added. (#2814) (#2815) (#2860)
QNodes returning a list or tuple of different measurements return an intuitive data structure via
qml.enable_return()
, where the individual measurements are separated into their own tensors:qml.enable_return() dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x): qml.Hadamard(wires=[0]) qml.CRX(x, wires=[0, 1]) return (qml.probs(wires=[0]), qml.vn_entropy(wires=[0]), qml.probs(wires=0), qml.expval(wires=1))
>>> circuit(0.5) (tensor([0.5, 0.5], requires_grad=True), tensor(0.08014815, requires_grad=True), tensor([0.5, 0.5], requires_grad=True), tensor(0.93879128, requires_grad=True))
In addition, QNodes that utilize this new return type support backpropagation. This new return type can be disabled thereafter via
qml.disable_return()
.An operator called
qml.FlipSign
is now available. (#2780)Mathematically,
qml.FlipSign
functions as follows: \(\text{FlipSign}(n) \vert m \rangle = (-1)^\delta_{n,m} \vert m \rangle\), where \(\vert m \rangle\) is an arbitrary qubit state and $n$ is a qubit configuration:basis_state = [0, 1] dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(): for wire in list(range(2)): qml.Hadamard(wires = wire) qml.FlipSign(basis_state, wires = list(range(2))) return qml.state()
>>> circuit() tensor([ 0.5+0.j -0.5+0.j 0.5+0.j 0.5+0.j], requires_grad=True)
The simultaneous perturbation stochastic approximation (SPSA) optimizer is available via
qml.SPSAOptimizer
. (#2661)The SPSA optimizer is suitable for cost functions whose evaluation may involve noise. Use the SPSA optimizer like you would any other optimizer:
max_iterations = 50 opt = qml.SPSAOptimizer(maxiter=max_iterations) for _ in range(max_iterations): params, cost = opt.step_and_cost(cost, params)
More drawing styles 🎨
New PennyLane-inspired
sketch
andsketch_dark
styles are now available for drawing circuit diagram graphics. (#2709)
Improvements 📈
default.qubit
now natively executes any operation that defines a matrix except for trainablePow
operations. (#2836)Added
expm
to theqml.math
module for matrix exponentiation. (#2890)When adjoint differentiation is requested, circuits are now decomposed so that all trainable operations have a generator. (#2836)
A warning is now emitted for
qml.state
,qml.density_matrix
,qml.vn_entropy
, andqml.mutual_info
when using a device with finite shots or a shot list since these measurements are always analytic. (#2918)The efficiency of the Hartree-Fock workflow has been improved by removing repetitive steps. (#2850)
The coefficients of the non-differentiable molecular Hamiltonians generated with openfermion now have
requires_grad = False
by default. (#2865)Upgraded performance of the
compute_matrix
method of broadcastable parametric operations. (#2759)Jacobians are now cached with the Autograd interface when using the parameter-shift rule. (#2645)
The
qml.state
andqml.density_matrix
measurements now support custom wire labels. (#2779)Add trivial behaviour logic to
qml.operation.expand_matrix
. (#2785)Added an
are_pauli_words_qwc
function which checks if certain Pauli words are pairwise qubit-wise commuting. This new function improves performance when measuring hamiltonians with many commuting terms. (#2789)Adjoint differentiation now uses the adjoint symbolic wrapper instead of in-place inversion. (#2855)
Breaking changes 💔
The deprecated
qml.hf
module is removed. Users with code that callsqml.hf
can simply replaceqml.hf
withqml.qchem
in most cases, or refer to the qchem documentation and demos for more information. (#2795)default.qubit
now usesstopping_condition
to specify support for anything with a matrix. To override this behavior in inheriting devices and to support only a specific subset of operations, developers need to overridestopping_condition
. (#2836)Custom devices inheriting from
DefaultQubit
orQubitDevice
can break due to the introduction of parameter broadcasting. (#2627)A custom device should only break if all three following statements hold simultaneously:
The custom device inherits from
DefaultQubit
, notQubitDevice
.The device implements custom methods in the simulation pipeline that are incompatible with broadcasting (for example
expval
,apply_operation
oranalytic_probability
).The custom device maintains the flag
"supports_broadcasting": True
in itscapabilities
dictionary or it overwritesDevice.batch_transform
without applyingbroadcast_expand
(or both).
The
capabilities["supports_broadcasting"]
is set toTrue
forDefaultQubit
. Typically, the easiest fix will be to change thecapabilities["supports_broadcasting"]
flag toFalse
for the child device and/or to include a call tobroadcast_expand
inCustomDevice.batch_transform
, similar to howDevice.batch_transform
calls it.Separately from the above, custom devices that inherit from
QubitDevice
and implement a custom_gather
method need to allow for the kwargaxis
to be passed to this_gather
method.The argument
argnum
of the functionqml.batch_input
has been redefined: now it indicates the indices of the batched parameters, which need to be non-trainable, in the quantum tape. Consequently, its default value (set to 0) has been removed. (#2873)Before this breaking change, one could call
qml.batch_input
without any arguments when using batched inputs as the first argument of the quantum circuit.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): # argument `inputs` is batched qml.RY(weights[0], wires=0) qml.AngleEmbedding(inputs, wires=range(2), rotation="Y") qml.RY(weights[1], wires=1) return qml.expval(qml.PauliZ(1))
With this breaking change, users must set a value to
argnum
specifying the index of the batched inputs with respect to all quantum tape parameters. In this example the quantum tape parameters are[ weights[0], inputs, weights[1] ]
, thusargnum
should be set to 1, specifying thatinputs
is batched:dev = qml.device("default.qubit", wires=2, shots=None) @qml.batch_input(argnum=1) @qml.qnode(dev, diff_method="parameter-shift", interface="tf") def circuit(inputs, weights): qml.RY(weights[0], wires=0) qml.AngleEmbedding(inputs, wires=range(2), rotation="Y") qml.RY(weights[1], wires=1) return qml.expval(qml.PauliZ(1))
PennyLane now depends on newer versions (>=2.7) of the
semantic_version
package, which provides an updated API that is incompatible which versions of the package prior to 2.7. If you run into issues relating to this package, please reinstall PennyLane. (#2744) (#2767)
Documentation 📕
Added a dedicated docstring for the
QubitDevice.sample
method. (#2812)Optimization examples of using JAXopt and Optax with the JAX interface have been added. (#2769)
Updated IsingXY gate docstring. (#2858)
Bug fixes 🐞
Fixes
qml.equal
so that operators with different inverse properties are not equal. (#2947)Cleans up interactions between operator arithmetic and batching by testing supported cases and adding errors when batching is not supported. (#2900)
Fixed a bug where the parameter-shift rule wasn’t defined for
qml.kUpCCGSD
. (#2913)Reworked the Hermiticity check in
qml.Hermitian
by usingqml.math
calls because calling.conj()
on anEagerTensor
from TensorFlow raised an error. (#2895)Fixed a bug where the parameter-shift gradient breaks when using both custom
grad_recipe
s that contain unshifted terms and recipes that do not contain any unshifted terms. (#2834)Fixed mixed CPU-GPU data-locality issues for the Torch interface. (#2830)
Fixed a bug where the parameter-shift Hessian of circuits with untrainable parameters might be computed with respect to the wrong parameters or might raise an error. (#2822)
Fixed a bug where the custom implementation of the
states_to_binary
device method was not used. (#2809)qml.grouping.group_observables
now works when individual wire labels are iterable. (#2752)The adjoint of an adjoint now has a correct
expand
result. (#2766)Fixed the ability to return custom objects as the expectation value of a QNode with the Autograd interface. (#2808)
The WireCut operator now raises an error when instantiating it with an empty list. (#2826)
Hamiltonians with grouped observables are now allowed to be measured on devices which were transformed using
qml.transform.insert()
. (#2857)Fixed a bug where
qml.batch_input
raised an error when using a batched operator that was not located at the beginning of the circuit. In addition, nowqml.batch_input
raises an error when using trainable batched inputs, which avoids an unwanted behaviour with duplicated parameters. (#2873)Calling
qml.equal
with nested operators now raises aNotImplementedError
. (#2877)Fixed a bug where a non-sensible error message was raised when using
qml.counts
withshots=False
. (#2928)Fixed a bug where no error was raised and a wrong value was returned when using
qml.counts
with another non-commuting observable. (#2928)Operator Arithmetic now allows
Hamiltonian
objects to be used and produces correct matrices. (#2957)
Contributors
This release contains contributions from (in alphabetical order):
Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Prajwal Borkar, Isaac De Vlugt, Olivia Di Matteo, Kristiyan Dilov, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Meenu Kumari, Christina Lee, Sergio Martínez-Losa, Albert Mitjans Coma, Ixchel Meza Chavez, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Bogdan Reznychenko, Shuli Shu, Jay Soni, Modjtaba Shokrian-Zini, Antal Száva, David Wierichs, Moritz Willmann
- orphan
Release 0.24.0¶
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) Array([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) (Array(-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, Ali Asadi, Utkarsh Azad, Samuel Banning, Avani Bhardwaj, Thomas Bromley, Albert Mitjans Coma, Isaac De Vlugt, Amintor Dusko, Trent Fridey, Christian Gogolin, Qi Hu, Katharine Hyatt, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Nathan Killoran, Korbinian Kottmann, Ankit Khandelwal, Christina Lee, Chae-Yeun Park, Mason Moreland, Romain Moyard, Maria Schuld, Jay Soni, Antal Száva, tal66, David Wierichs, Roeland Wiersema, 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 aArray
. (#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─────────┤ ╭<[email protected]> 1: ────╰RX(2.30)──Rot(1.20,3.20,0.70)─╰RX(-2.30)─┤ ╰<[email protected]>
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
Array
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) Array([0.8397495 , 0.16025047], dtype=float32) >>> jax.jacobian(circuit, argnums=[0, 1])(x, y) (Array([-0.2050439, 0.2050439], dtype=float32, weak_type=True), Array([ 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