Releases: PennyLaneAI/pennylane
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.
- The qutrit shift operator,
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 operat...
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.
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 applica...
-
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 at...
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.
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=[...
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 inv0.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.
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.
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....
-
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
or- multiple
qml.expval
/qml.var
measurements.
Consider a QNode that returns basis-state probabilities:
dev = qml.device('default.qubit', wires=2) x = jnp.array(0.543) y = jnp.array(-0.654) @qml.qnode(dev, diff_method="parameter-shift", interface="jax") def circuit(x, y): qml.RX(x, wires=[0]) qml.RY(y, wires=[1]) qml.CNOT(wires=[0, 1]) return qml.probs(wires=[1])
The QNode can be evaluated and its jacobian can be computed:
>>> circuit(x, y) DeviceArray([0.8397495 , 0.16025047], dtype=float32) >>> jax.jacobian(circuit, argnums=[0, 1])(x, y) (DeviceArray([-0.2050439, 0.2050439], dtype=float32, weak_type=True), DeviceArray([ 0.26043, -0.26043], dtype=float32, weak_type=True))
Note that
jax.jit
is not yet supported for vector-valued QNodes.
Speedier quantum natural gradient ⚡
-
A new function for computing the metric tensor on simulators,
qml.adjoint_metric_tensor
, has been added, that uses classically efficient methods to massively improve performance. (#1992)This method, detailed in Jones (2020), computes the metric tensor using four copies of the state vector and a number of operations that scales quadratically in the number of trainable parameters.
Note that as it makes use of state cloning, it is inherently classical and can only be used with statevector simulators and
shots=None
.It is particularly useful for larger circuits for which backpropagation requires inconvenient or even unfeasible amounts of storage, but is slower. Furthermore, the adjoint method is only available for analytic computation, not for measurements simulation with
shots!=None
.dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circuit(x, y): qml.Rot(*x[0], wires=0) qml.Rot(*x[1], wires=1) qml.Rot(*x[2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) qml.RY(y[0], wires=0) qml.RY(y[1], wires=1) qml.RY(y[0], wires=2) return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(1)) x = np.array([[0.2, 0.4, -0.1], [-2.1, 0.5, -0.2], [0.1, 0.7, -0.6]], requires_grad=False) y = np.array([1.3, 0.2], requires_grad=True)
>>> qml.adjoint_metric_tensor(circuit)(x, y) tensor([[ 0.25495723, -0.07086695], [-0.07086695, 0.24945606]], requires_grad=True)
Computational cost
The adjoint method uses :math:
2P^2+4P+1
gates ...