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 :math:k
eigenvalues of a sparse :math:N \times N
matrix ifk
is smaller than :math:N-1
. If a larger :math:k
is requested, the dense matrix representation of the Hamiltonian is constructed and the regularqml.math.linalg.eigvalsh
is applied. (#2333) -
The function
qml.ctrl
was given the optional argumentcontrol_values=None
. If overridden,control_values
takes an integer or a list of integers corresponding to the binary value that each control value should take. The same change is reflected inControlledOperation
. Control values of0
are implemented byqml.PauliX
applied before and after the controlled operation (#2288) -
Operators now have a
has_matrix
property denoting whether or not the operator defines a matrix. (#2331) (#2476) -
Circuit cutting now performs expansion to search for wire cuts in contained operations or tapes. (#2340)
-
The
qml.draw
andqml.draw_mpl
transforms are now located in thedrawer
module. They can still be accessed via the top-levelqml
namespace. (#2396) -
Raise a warning where caching produces identical shot noise on execution results with finite shots. (#2478)
Deprecations
- The
ObservableReturnTypes
Sample
,Variance
,Expectation
,Probability
,State
, andMidMeasure
have been moved tomeasurements
fromoperation
. (#2329) (#2481)
Breaking changes
-
The caching ability of devices has been removed. Using the caching on the QNode level is the recommended alternative going forward. (#2443)
One way for replicating the removed
QubitDevice
caching behaviour is by creating acache
object (e.g., a dictionary) and passing it to theQNode
:n_wires = 4 wires = range(n_wires) dev = qml.device('default.qubit', wires=n_wires) cache = {} @qml.qnode(dev, diff_method='parameter-shift', cache=cache) def expval_circuit(params): qml.templates.BasicEntanglerLayers(params, wires=wires, rotation=qml.RX) return qml.expval(qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliZ(3)) shape = qml.templates.BasicEntanglerLayers.shape(5, n_wires) params = np.random.random(shape)
>>> expval_circuit(params) tensor(0.20598436, requires_grad=True) >>> dev.num_executions 1 >>> expval_circuit(params) tensor(0.20598436, requires_grad=True) >>> dev.num_executions 1
-
The
qml.finite_diff
function has been removed. Please useqml.gradients.finite_diff
to compute the gradient of tapes of QNodes. Otherwise, manual implementation is required. (#2464) -
The
get_unitary_matrix
transform has been removed, please useqml.matrix
instead. (#2457) -
The
update_stepsize
method has been removed fromGradientDescentOptimizer
and its child optimizers. Thestepsize
property can be interacted with directly instead. (#2370) -
Most optimizers no longer flatten and unflatten arguments during computation. Due to this change, user provided gradient functions must return the same shape as
qml.grad
. (#2381) -
The old circuit text drawing infrastructure has been removed. (#2310)
RepresentationResolver
was replaced by theOperator.label
method.qml.drawer.CircuitDrawer
was replaced byqml.drawer.tape_text
.qml.drawer.CHARSETS
was removed because unicode is assumed to be accessible.Grid
andqml.drawer.drawable_grid
were removed because the custom data class was replaced by list of sets of operators or measurements.qml.transforms.draw_old
was replaced byqml.draw
.qml.CircuitGraph.greedy_layers
was deleted, as it was no longer needed by the circuit drawer and did not seem to have uses outside of that situation.qml.CircuitGraph.draw
was deleted, as we draw tapes instead.- The tape method
qml.tape.QuantumTape.draw
now simply callsqml.drawer.tape_text
. - In the new pathway, the
charset
keyword was deleted, themax_length
keyword defaults to100
, and thedecimals
andshow_matrices
keywords were added.
-
The deprecated QNode, available via
qml.qnode_old.QNode
, has been removed. Please transition to using the standardqml.QNode
. (#2336) (#2337) (#2338)In addition, several other components which powered the deprecated QNode have been removed:
-
The deprecated, non-batch compatible interfaces, have been removed.
-
The deprecated tape subclasses
QubitParamShiftTape
,JacobianTape
,CVParamShiftTape
, andReversibleTape
have been removed.
-
-
The deprecated tape execution method
tape.execute(device)
has been removed. Please useqml.execute([tape], device)
instead. (#2339)
Bug fixes
-
Fixed a bug in the
qml.PauliRot
operation, where computing the generator was not taking into account the operation wires. (#2466) -
Fixed a bug where non-trainable arguments were shifted in the
NesterovMomentumOptimizer
if a trainable argument was after it in the argument list. (#2466) -
Fixed a bug with
@jax.jit
for grad whendiff_method="adjoint"
andmode="backward"
. (#2460) -
Fixed a bug where
qml.DiagonalQubitUnitary
did not support@jax.jit
and@tf.function
. (#2445) -
Fixed a bug in the
qml.PauliRot
operation, where computing the generator was not taking into account the operation wires. (#2442) -
Fixed a bug with the padding capability of
AmplitudeEmbedding
where the inputs are on the GPU. (#2431) -
Fixed a bug by adding a comprehensible error message for calling
qml.probs
without passing wires or an observable. (#2438) -
The behaviour of
qml.about()
was modified to avoid warnings being emitted due to legacy behaviour ofpip
. (#2422) -
Fixed a bug where observables were not considered when determining the use of the
jax-jit
interface. (#2427) (#2474) -
Fixed a bug where computing statistics for a relatively few number of shots (e.g.,
shots=10
), an error arose due to indexing into an array using aDeviceArray
. (#2427) -
PennyLane Lightning version in Docker container is pulled from latest wheel-builds. (#2416)
-
Optimizers only consider a variable trainable if they have
requires_grad = True
. (#2381) -
Fixed a bug with
qml.expval
,qml.var
,qml.state
andqml.probs
(whenqml.probs
is the only measurement) where thedtype
specified on the device did not match thedtype
of the QNode output. (#2367) -
Fixed a bug where the output shapes from batch transforms are inconsistent with the QNode output shape. (#2215)
-
Fixed a bug caused by the squeezing in
qml.gradients.param_shift_hessian
. (#2215) -
Fixed a bug in which the
expval
/var
of aTensor(Observable)
would depend on the order in which the observable is defined: (#2276)>>> @qml.qnode(dev) ... def circ(op): ... qml.RX(0.12, wires=0) ... qml.RX(1.34, wires=1) ... qml.RX(3.67, wires=2) ... return qml.expval(op) >>> op1 = qml.Identity(wires=0) @ qml.Identity(wires=1) @ qml.PauliZ(wires=2) >>> op2 = qml.PauliZ(wires=2) @ qml.Identity(wires=0) @ qml.Identity(wires=1) >>> print(circ(op1), circ(op2)) -0.8636111153905662 -0.8636111153905662
-
Fixed a bug where
qml.hf.transform_hf()
would fail due to missing wires in the qubit operator that is prepared for tapering the HF state. (#2441) -
Fixed a bug with custom device defined jacobians not being returned properly. (#2485)
Documentation
-
The sections on adding operator and observable support in the "How to add a plugin" section of the plugins page have been updated. (#2389)
-
The missing arXiv reference in the
LieAlgebra
optimizer has been fixed. (#2325)
Contributors
This release contains contributions from (in alphabetical order):
Karim Alaa El-Din, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Alain Delgado, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Angus Lowe, Romain Moyard, Zeyue Niu, Matthew Silverman, Lee James O'Riordan, Maria Schuld, Jay Soni, Antal Száva, Maurice Weber, David Wierichs.