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 termqml.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 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. -
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. #3864 -
Ensured 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.