- Demos/
- Algorithms/
How to track algorithmic error using PennyLane
How to track algorithmic error using PennyLane
Published: May 02, 2024. Last updated: October 06, 2024.
In order to accurately determine the resources required to run a given quantum workflow, one must carefully track and propagate the sources of error within the many algorithms that make up the workflow. Furthermore, there are a variety of different errors to keep track of:
Input / Encoding Error: The error from embedding classical data into the quantum circuit (e.g. initial state prep).
Algorithm-specific Error: The error caused by the structure of the algorithm itself (e.g. QPE with limited readout qubits).
Approximate Decomposition Error: The error caused by decomposing gates approximately (e.g. Clifford + T decomposition).
Hardware Noise Error: The error introduced by noisy quantum channels (e.g.
BitFlip
,PhaseFlip
).Measurement Uncertainty: The error from the probabilistic nature of quantum measurement (e.g. multiple samples required for state tomography).
We refer to the first three of these as “Algorithmic Error”. Typically, these types of error computations are performed by hand due to the variety of error metrics and the specific handling of such errors for each subroutine. In this demo, we present the latest tools in PennyLane which automatically track algorithmic error.

Quantify Error using the Spectral Norm
Before we can track the error in our quantum workflow, we need to quantify it. A common method for quantifying the error
between operators is to compute the “distance” between them; specifically, the spectral norm of the difference between
the operators. We can use the new SpectralNormError
class to compute and represent this error.
Consider for example, that instead of applying qml.RX(1.234)
we incur some rounding error in the rotation angle;
how much error would the resulting operators have?
We can compute this easily with PennyLane:
import pennylane as qml
from pennylane.resource import SpectralNormError
exact_op = qml.RX(1.234, wires=0)
thetas = [1.23, 1.2, 1.0]
ops = [qml.RX(theta, wires=0) for theta in thetas]
for approx_op, theta in zip(ops, thetas):
error = SpectralNormError.get_error(exact_op, approx_op)
print(f"Spectral Norm error (theta = {theta:.2f}): {error:.5f}")
Spectral Norm error (theta = 1.23): 0.00200
Spectral Norm error (theta = 1.20): 0.01700
Spectral Norm error (theta = 1.00): 0.11693
The error in the operator increases as we round the rotation angle to fewer decimal places as expected. Now that we can quantify the error, let’s track the error for one of the most common workflows in quantum computing: time evolving a quantum state under a given Hamiltonian!
Tracking Errors in Hamiltonian Simulation
Time evolving a quantum state under a Hamiltonian requires generating the unitary \(\hat{U} = \exp(iHt).\) In general it is difficult to prepare this operator exactly, so it is instead prepared approximately. The most common method to accomplish this is the Suzuki-Trotter product formula 1. This subroutine introduces algorithm-specific error as it produces an approximation to the matrix exponential operator.
Let’s explicitly compute the error from this algorithm for a simple Hamiltonian:
time = 0.1
Hamiltonian = qml.X(0) + qml.Y(0)
exact_op = qml.exp(Hamiltonian, 1j * time) # U = e^iHt ~ TrotterProduct(..., order=2)
approx_op = qml.TrotterProduct( # eg: e^iHt ~ e^iXt/2 * e^iYt * e^iXt/2
Hamiltonian,
time,
order=2,
)
error = SpectralNormError.get_error(exact_op, approx_op) # Expensive to compute
print(f"Error from Suzuki-Trotter algorithm: {error:.5f}")
Error from Suzuki-Trotter algorithm: 0.00037
In general, exactly computing the spectral norm is computationally expensive for larger systems as it requires diagonalizing the operators. For this reason, we typically use upper bounds on the spectral norm error in the product formulas.
We provide two common methods for bounding the error from literature 1.
They can be accessed by using op.error()
and specifying the method
keyword argument:
op = qml.TrotterProduct(Hamiltonian, time, order=2)
one_norm_error_bound = op.error(method="one-norm-bound")
commutator_error_bound = op.error(method="commutator-bound")
print("one-norm bound: ", one_norm_error_bound)
print("commutator bound: ", commutator_error_bound)
one-norm bound: SpectralNormError(0.012000000000000004)
commutator bound: SpectralNormError(0.01066666666666667)
Custom Error Operations
With the new SpectralNormError
and ErrorOperation
classes it’s easy for anyone to define their own custom operations with error. All we need to do is to specify
how the error is computed. Once the error function is defined, PennyLane tracks and propagates the error
through the circuit. This makes it easy for us to add and combine multiple error operations together in a
quantum circuit. In the following example we define a custom operation with error to act as an approximate
decomposition.
Suppose, for example, that our quantum
hardware does not natively support rotation gates (RX
,
RY
, RZ
). How could we decompose the RX
gate?
Notice that \(\hat{R_{x}}(\frac{\pi}{4}) = \hat{H} \cdot \hat{T} \cdot \hat{H}\) up to a global phase \(e^{i \frac{\pi}{8}}.\)
from pennylane import numpy as np
op1 = qml.RX(np.pi / 4, 0)
op2 = qml.GlobalPhase(np.pi / 8) @ qml.Hadamard(0) @ qml.T(0) @ qml.Hadamard(0)
np.allclose(qml.matrix(op1), qml.matrix(op2))
True
We can approximate the RX
gate by rounding the rotation angle to the lowest multiple
of \(\frac{\pi}{4},\) then using multiple iterations of the sequence above.
The approximation error we incur from this decomposition is given by the expression:
where \(\theta = \frac{\pi \ - \ \Delta_{\phi}}{2}\) and \(\Delta_{\phi}\) is the absolute difference between the true rotation angle and the next lowest multiple of \(\frac{\pi}{4}.\)
We can take this approximate decomposition and turn it into a PennyLane operation simply by inheriting
from the ErrorOperation
class, and defining the error method:
from pennylane.resource.error import ErrorOperation
class Approximate_RX(ErrorOperation):
def __init__(self, phi, wires):
"""Approximate decomposition for RX gate"""
return super().__init__(phi, wires)
@staticmethod
def compute_decomposition(phi, wires):
"""Defining the gate decomposition"""
num_iterations = int(phi // (np.pi / 4)) # how many rotations of pi/4 to apply
global_phase = num_iterations * np.pi / 8
decomposition = [qml.GlobalPhase(global_phase)]
for _ in range(num_iterations):
decomposition += [qml.Hadamard(wires), qml.T(wires), qml.Hadamard(wires)]
return decomposition
def error(self):
"""The error in our approximation"""
phi = self.parameters[0] # The error depends on the true rotation angle
delta_phi = phi % (np.pi / 4)
theta = (np.pi - delta_phi) / 2
error = np.sqrt(2 - 2 * np.sin(theta))
return SpectralNormError(error)
We can verify that evaluating the expression for the approximation error gives us the same result as
explicitly computing the error. Notice that we can access the error of our new operator in the same way
we did for Hamiltonian simulation, using op.error()
.
phi = 1.23
true_op = qml.RX(phi, wires=0)
approx_op = Approximate_RX(phi, wires=0)
error_from_theory = approx_op.error()
explicit_comp = SpectralNormError.get_error(true_op, approx_op)
print("Explicit computation: ", explicit_comp)
print("Error from function: ", error_from_theory.error)
Explicit computation: 0.22184346764856389
Error from function: 0.22184346764856405
Bringing it All Together
Tracking the error for each component individually is great, but we ultimately want to put these pieces together in a quantum circuit. PennyLane now automatically tracks and propagates these errors through the circuit. This means we can write our circuits as usual and get all the benefits of error tracking for free.
dev = qml.device("default.qubit")
@qml.qnode(dev)
def circ(H, t, phi1, phi2):
qml.Hadamard(0)
qml.Hadamard(1)
# Approx decomposition
Approximate_RX(phi1, 0)
Approximate_RX(phi2, 1)
qml.CNOT([0, 1])
# Approx time evolution:
qml.TrotterProduct(H, t, order=2)
# Measurement:
return qml.state()
Along with executing the circuit, we can also compute the error in the circuit through specs()
:
phi1, phi2 = (0.12, 3.45)
print("State:")
print(circ(Hamiltonian, time, phi1, phi2), "\n")
errors_dict = qml.specs(circ)(Hamiltonian, time, phi1, phi2)["errors"]
error = errors_dict["SpectralNormError"]
print("Error:")
print(error)
State:
[0.04966733-0.54493335j 0.04966733-0.54493335j 0.04966733-0.44509994j
0.04966733-0.44509994j]
Error:
SpectralNormError(0.22470860342773674)
Conclusion
In this demo, we showcased the new SpectralNormError
and
ErrorOperation
classes in PennyLane. We also highlighted the new functionality
in TrotterProduct
class to compute error bounds in product formulas.
We explained how to construct a custom error operation and used it in a simple workflow to
propagate the error through the circuit. Accurately tracking the error in our workflows allows us to
make more resource-efficient algorithms, ultimately unlocking new applications. We hope that you can make
use of these tools in your cutting-edge research workflows.
References
- 1(1,2)
Andrew M. Childs, Yuan Su, Minh C. Tran, Nathan Wiebe, and Shuchen Zhu, “Theory of Trotter Error with Commutator Scaling”. Phys. Rev. X 11, 011020 (2021)
About the author
Jay Soni
Jay completed his BSc. in Mathematical Physics from the University of Waterloo and currently works as a Quantum Software Developer at Xanadu. Fun fact, you will often find him sipping on a Tim Horton's IceCapp while he is working.
Total running time of the script: (0 minutes 0.029 seconds)