Source code for pennylane.transforms.mitigate
# Copyright 2021 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides transforms for mitigating quantum circuits."""
from collections.abc import Sequence
from copy import copy
from typing import Any, Optional
import pennylane as qml
from pennylane import adjoint, apply
from pennylane.math import mean, round, shape
from pennylane.queuing import AnnotatedQueue
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.transforms import transform
from pennylane.typing import PostprocessingFn
[docs]@transform
def fold_global(tape: QuantumScript, scale_factor) -> tuple[QuantumScriptBatch, PostprocessingFn]:
r"""Differentiable circuit folding of the global unitary ``circuit``.
For a unitary circuit :math:`U = L_d .. L_1`, where :math:`L_i` can be either a gate or layer, ``fold_global`` constructs
.. math:: \text{fold_global}(U) = U (U^\dagger U)^n (L^\dagger_d L^\dagger_{d-1} .. L^\dagger_s) (L_s .. L_d)
where :math:`n = \lfloor (\lambda - 1)/2 \rfloor` and :math:`s = \lfloor \left(\lambda - 1 \right) (d/2) \rfloor` are determined via the ``scale_factor`` :math:`=\lambda`.
The purpose of folding is to artificially increase the noise for zero noise extrapolation, see :func:`~.pennylane.transforms.mitigate_with_zne`.
Args:
tape (QNode or QuantumTape): the quantum circuit to be folded
scale_factor (float): Scale factor :math:`\lambda` determining :math:`n` and :math:`s`
Returns:
qnode (QNode) or tuple[List[QuantumTape], function]: The folded circuit as described in :func:`qml.transform <pennylane.transform>`.
.. seealso:: :func:`~.pennylane.transforms.mitigate_with_zne`; This function is analogous to the implementation in ``mitiq`` `mitiq.zne.scaling.fold_global <https://mitiq.readthedocs.io/en/v.0.1a2/apidoc.html?highlight=global_folding#mitiq.zne.scaling.fold_global>`_.
.. note::
This method no longer decomposes the circuit as part of the folding procedure. Users are encouraged to use
:func:`~.pennylane.transforms.decompose` to expand the circuit into a target gateset before using this transform.
**Example**
Let us look at the following circuit.
.. code-block:: python
x = np.arange(6)
@qml.qnode(dev)
def circuit(x):
qml.RX(x[0], wires=0)
qml.RY(x[1], wires=1)
qml.RZ(x[2], wires=2)
qml.CNOT(wires=(0,1))
qml.CNOT(wires=(1,2))
qml.RX(x[3], wires=0)
qml.RY(x[4], wires=1)
qml.RZ(x[5], wires=2)
return qml.expval(qml.Z(0) @ qml.Z(1) @ qml.Z(2))
Setting ``scale_factor=1`` does not affect the circuit:
>>> folded = qml.transforms.fold_global(circuit, 1)
>>> print(qml.draw(folded)(x))
0: ──RX(0.0)─╭●──RX(3.0)──────────┤ ╭<Z@Z@Z>
1: ──RY(1.0)─╰X─╭●────────RY(4.0)─┤ ├<Z@Z@Z>
2: ──RZ(2.0)────╰X────────RZ(5.0)─┤ ╰<Z@Z@Z>
Setting ``scale_factor=2`` results in the partially folded circuit :math:`U (L^\dagger_d L^\dagger_{d-1} .. L^\dagger_s) (L_s .. L_d)`
with :math:`s = \lfloor \left(1 \mod 2 \right) d/2 \rfloor = 4` since the circuit is composed of :math:`d=8` gates.
>>> folded = qml.transforms.fold_global(circuit, 2)
>>> print(qml.draw(folded)(x))
0: ──RX(0.0)─╭●──RX(3.0)──RX(3.0)†──RX(3.0)──────────────────┤ ╭<Z@Z@Z>
1: ──RY(1.0)─╰X─╭●────────RY(4.0)───RY(4.0)†─╭●──╭●──RY(4.0)─┤ ├<Z@Z@Z>
2: ──RZ(2.0)────╰X────────RZ(5.0)───RZ(5.0)†─╰X†─╰X──RZ(5.0)─┤ ╰<Z@Z@Z>
Setting ``scale_factor=3`` results in the folded circuit :math:`U (U^\dagger U)`.
>>> folded = qml.transforms.fold_global(circuit, 3)
>>> print(qml.draw(folded)(x))
0: ──RX(0.0)─╭●──RX(3.0)──RX(3.0)†───────────────╭●─────────RX(0.0)†──RX(0.0)─╭●──RX(3.0)──────────┤╭<Z@Z@Z>
1: ──RY(1.0)─╰X─╭●────────RY(4.0)───RY(4.0)†─╭●──╰X†────────RY(1.0)†──RY(1.0)─╰X─╭●────────RY(4.0)─┤├<Z@Z@Z>
2: ──RZ(2.0)────╰X────────RZ(5.0)───RZ(5.0)†─╰X†──RZ(2.0)†──RZ(2.0)──────────────╰X────────RZ(5.0)─┤╰<Z@Z@Z>
.. note::
Circuits are treated as lists of operations. Since the ordering of that list is ambiguous, so is its folding.
This can be seen exemplarily for two equivalent unitaries :math:`U1 = X(0) Y(0) X(1) Y(1)` and :math:`U2 = X(0) X(1) Y(0) Y(1)`.
The folded circuits according to ``scale_factor=2`` would be :math:`U1 (X(0) Y(0) Y(0) X(0))` and :math:`U2 (X(0) X(1) X(1) X(0))`, respectively.
So even though :math:`U1` and :math:`U2` are describing the same quantum circuit, the ambiguity in their ordering as a list yields two differently folded circuits.
.. details::
The main purpose of folding is for zero noise extrapolation (ZNE). PennyLane provides a differentiable transform :func:`~.pennylane.transforms.mitigate_with_zne`
that allows you to perform ZNE as a black box. If you want more control and `see` the extrapolation, you can follow the logic of the following example.
We start by setting up a noisy device using the mixed state simulator and a noise channel.
.. code-block:: python
n_wires = 4
# Describe noise
noise_gate = qml.DepolarizingChannel
noise_strength = 0.05
# Load devices
dev_ideal = qml.device("default.mixed", wires=n_wires)
dev_noisy = qml.transforms.insert(noise_gate, noise_strength)(dev_ideal)
x = np.arange(6)
H = 1.*qml.X(0) @ qml.X(1) + 1.*qml.X(1) @ qml.X(2)
def circuit(x):
qml.RY(x[0], wires=0)
qml.RY(x[1], wires=1)
qml.RY(x[2], wires=2)
qml.CNOT(wires=(0,1))
qml.CNOT(wires=(1,2))
qml.RY(x[3], wires=0)
qml.RY(x[4], wires=1)
qml.RY(x[5], wires=2)
return qml.expval(H)
qnode_ideal = qml.QNode(circuit, dev_ideal)
qnode_noisy = qml.QNode(circuit, dev_noisy)
We can then create folded versions of the noisy qnode and execute them for different scaling factors.
>>> scale_factors = [1., 2., 3.]
>>> folded_res = [qml.transforms.fold_global(qnode_noisy, lambda_)(x) for lambda_ in scale_factors]
We want to later compare the ZNE with the ideal result.
>>> ideal_res = qnode_ideal(x)
ZNE is, as the name suggests, an extrapolation in the noise to zero. The underlyding assumption is that the level of noise is proportional to the scaling factor
by artificially increasing the circuit depth. We can perform a polynomial fit using ``numpy`` functions. Note that internally in :func:`~.pennylane.transforms.mitigate_with_zne`
a differentiable polynomial fit function :func:`~.pennylane.transforms.poly_extrapolate` is used.
>>> # coefficients are ordered like coeffs[0] * x**2 + coeffs[1] * x + coeffs[0]
>>> coeffs = np.polyfit(scale_factors, folded_res, 2)
>>> zne_res = coeffs[-1]
We used a polynomial fit of ``order=2``. Using ``order=len(scale_factors) -1`` is also referred to as Richardson extrapolation and implemented in :func:`~.pennylane.transforms.richardson_extrapolate`.
We can now visualize our fit to see how close we get to the ideal result with this mitigation technique.
.. code-block:: python
x_fit = np.linspace(0, 3, 20)
y_fit = np.poly1d(coeffs)(x_fit)
plt.plot(scale_factors, folded_res, "x--", label="folded")
plt.plot(0, ideal_res, "X", label="ideal res")
plt.plot(0, zne_res, "X", label="ZNE res", color="tab:red")
plt.plot(x_fit, y_fit, label="fit", color="tab:red", alpha=0.5)
plt.legend()
.. figure:: ../../_static/fold_global_zne_by-hand.png
:align: center
:width: 60%
:target: javascript:void(0);
"""
# The main intention for providing ``fold_global`` was for it to be used in combination with ``mitigate_with_zne``, which also works with mitiq functions.
# To preserve the mitiq functionality, ``mitigate_with_zne`` should get a tape transform.
# To make ``fold_global`` also user-facing and work with qnodes, this function is batch_transformed instead, and therefore applicable on qnodes.
return [fold_global_tape(tape, scale_factor)], lambda x: x[0]
def _divmod(a, b):
"""Performs divmod but in an all-interface compatible manner"""
out1 = qml.math.floor(a / b)
out2 = a - out1 * b
return int(out1), out2
def fold_global_tape(circuit, scale_factor):
r"""
This is the internal tape transform to be used with :func:`~.pennylane.transforms.mitigate_with_zne`.
For the user-facing function see :func:`~.pennylane.transforms.fold_global`.
Args:
circuit (QuantumTape): the circuit to be folded
scale_factor (float): Scale factor :math:`\lambda` determining :math:`n` and :math:`s`
Returns:
QuantumTape: Folded circuit
"""
# TODO: simplify queing via qfunc(op) - currently just a workaround, to solve the problem of ownership when tape contains adjoint(op)
# https://github.com/PennyLaneAI/pennylane/pull/2766 already touched on the issue, future work
# in Q3 2022 should make it possible to substantially simplify this.
def qfunc(op):
copy(op).queue()
# Generate base_circuit without measurements
# Treat all circuits as lists of operations, build new tape in the end
base_ops = circuit.operations
if any((isinstance(op, qml.operation.Channel) for op in base_ops)):
raise ValueError(
"Circuits containing quantum channels cannot be folded with mitigate_with_zne. "
"To use zero-noise extrapolation on the circuit with channel noise, "
"please add the noise on the device rather than the circuit."
)
num_global_folds, fraction_scale = _divmod(scale_factor - 1, 2)
n_ops = len(base_ops)
num_to_fold = int(round(fraction_scale * n_ops / 2))
# Create new_circuit from folded list
with AnnotatedQueue() as new_circuit_q:
# Original U
for op in base_ops:
qfunc(op)
# Folding U => U (U^H U)**n.
for _ in range(int(num_global_folds)):
for op in base_ops[::-1]:
adjoint(qfunc)(op)
for op in base_ops:
qfunc(op)
# Remainder folding U => U (U^H U)**n (L_d^H .. L_s^H) (L_s .. L_d)
for i in range(n_ops - 1, n_ops - num_to_fold - 1, -1):
adjoint(qfunc)(base_ops[i])
for i in range(n_ops - num_to_fold, n_ops):
qfunc(base_ops[i])
# Append measurements
for meas in circuit.measurements:
apply(meas)
return QuantumScript.from_queue(new_circuit_q)
# TODO: make this a pennylane.math function
def _polyfit(x, y, order):
"""Brute force implementation of polynomial fit"""
x = qml.math.convert_like(x, y[0])
x = qml.math.cast_like(x, y[0])
X = qml.math.vander(x, order + 1)
y = qml.math.stack(y)
# scale X to improve condition number and solve
scale = qml.math.sum(qml.math.sqrt((X * X)), axis=0)
X = X / scale
# Compute coeffs:
# This part is typically done using a lstq solver, do it with the penrose inverse by hand:
# i.e. coeffs = (X.T @ X)**-1 X.T @ y see https://en.wikipedia.org/wiki/Polynomial_regression
c = qml.math.linalg.pinv(qml.math.transpose(X) @ X)
c = c @ qml.math.transpose(X)
c = qml.math.tensordot(c, y, axes=1)
c = qml.math.transpose(qml.math.transpose(c) / scale)
return c
[docs]def poly_extrapolate(x, y, order):
r"""Extrapolator to :math:`f(0)` for polynomial fit.
The polynomial is defined as ``f(x) = p[0] * x**deg + p[1] * x**(deg-1) + ... + p[deg]`` such that ``deg = order + 1``.
This function is compatible with all interfaces supported by pennylane.
Args:
x (Array): Data in x
y (Array): Data in y = f(x)
order (int): Order of the polynomial fit
Returns:
float: Extrapolated value at f(0).
.. seealso:: :func:`~.pennylane.transforms.richardson_extrapolate`, :func:`~.pennylane.transforms.mitigate_with_zne`
**Example:**
>>> x = np.linspace(1, 10, 5)
>>> y = x**2 + x + 1 + 0.3 * np.random.rand(len(x))
>>> qml.transforms.poly_extrapolate(x, y, 2)
tensor(1.01717601, requires_grad=True)
"""
coeff = _polyfit(x, y, order)
return coeff[-1]
[docs]def richardson_extrapolate(x, y):
r"""Polynomial fit where the degree of the polynomial is fixed to being equal to the length of ``x``.
In a nutshell, this function is calling :func:`~.pennylane.transforms.poly_extrapolate` with ``order = len(x)-1``.
This function is compatible with all interfaces supported by pennylane.
Args:
x (Array): Data in x
y (Array): Data in y = f(x)
Returns:
float: Extrapolated value at f(0).
.. seealso:: :func:`~.pennylane.transforms.poly_extrapolate`, :func:`~.pennylane.transforms.mitigate_with_zne`
**Example:**
>>> x = np.linspace(1, 10, 5)
>>> y = x**2 + x + 1 + 0.3 * np.random.rand(len(x))
>>> qml.transforms.richardson_extrapolate(x, y)
tensor(1.15105156, requires_grad=True)
"""
return poly_extrapolate(x, y, len(x) - 1)
[docs]def exponential_extrapolate(x, y, asymptote=None, eps=1.0e-6):
r"""Extrapolate to the zero-noise limit using an exponential model (:math:`Ae^{Bx} + C`). This
is done by linearizing the data using a logarithm, whereupon a linear fit is performed. Once
the model parameters are found, they are transformed back to exponential parameters.
Args:
x (Array): Data in x axis.
y (Array): Data in y axis such that :math:`y = f(x)`.
asymptote (float): Infinite noise limit expected for your circuit of interest (:math:`C`
in the equation above). Defaults to 0 in the case an asymptote is not supplied.
eps (float): Epsilon to regularize :math:`\log(y - C)` when the argument is to close to
zero or negative.
Returns:
float: Extrapolated value at f(0).
.. seealso:: :func:`~.pennylane.transforms.richardson_extrapolate`, :func:`~.pennylane.transforms.mitigate_with_zne`.
**Example:**
>>> np.random.seed(0)
>>> x = np.linspace(1, 10, 5)
>>> y = np.exp(-x) + np.random.normal(scale=0.1, size=len(x))
>>> qml.transforms.exponential_extrapolate(x, y)
0.23365009000522544
"""
y = qml.math.stack(y)
slope, y_intercept = _polyfit(x, y, 1)
if asymptote is None:
sign = qml.math.sign(-slope)
asymptote = 0.0
else:
sign = qml.math.sign(-(asymptote - y_intercept))
y_shifted = sign * (y - asymptote)
y_shifted = qml.math.where(y_shifted < eps, eps, y_shifted)
y_scaled = qml.math.log(y_shifted)
zne_unscaled = poly_extrapolate(x, y_scaled, 1)
return sign * qml.math.exp(zne_unscaled) + asymptote
# pylint: disable=too-many-arguments, protected-access
[docs]@transform
def mitigate_with_zne(
tape: QuantumScript,
scale_factors: Sequence[float],
folding: callable,
extrapolate: callable,
folding_kwargs: Optional[dict[str, Any]] = None,
extrapolate_kwargs: Optional[dict[str, Any]] = None,
reps_per_factor=1,
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
r"""Mitigate an input circuit using zero-noise extrapolation.
Error mitigation is a precursor to error correction and is compatible with near-term quantum
devices. It aims to lower the impact of noise when evaluating a circuit on a quantum device by
evaluating multiple variations of the circuit and post-processing the results into a
noise-reduced estimate. This transform implements the zero-noise extrapolation (ZNE) method
originally introduced by
`Temme et al. <https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.119.180509>`__ and
`Li et al. <https://journals.aps.org/prx/abstract/10.1103/PhysRevX.7.021050>`__.
Details on the functions passed to the ``folding`` and ``extrapolate`` arguments of this
transform can be found in the usage details. This transform is compatible with functionality
from the `Mitiq <https://mitiq.readthedocs.io/en/stable/>`__ package (version 0.11.0 and above),
see the example and usage details for further information.
Args:
tape (QNode or QuantumTape): the quantum circuit to be error-mitigated
scale_factors (Sequence[float]): the range of noise scale factors used
folding (callable): a function that returns a folded circuit for a specified scale factor
extrapolate (callable): a function that returns an extrapolated result when provided a
range of scale factors and corresponding results
folding_kwargs (dict): optional keyword arguments passed to the ``folding`` function
extrapolate_kwargs (dict): optional keyword arguments passed to the ``extrapolate`` function
reps_per_factor (int): Number of circuits generated for each scale factor. Useful when the
folding function is stochastic.
Returns:
qnode (QNode) or tuple[List[.QuantumTape], function]:
The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit
will provide the mitigated results in the form of a tensor of a tensor, a tuple, or a nested tuple depending
upon the nesting structure of measurements in the original circuit.
**Example:**
We first create a noisy device using ``default.mixed`` by adding :class:`~.AmplitudeDamping` to
each gate of circuits executed on the device using the :func:`~.transforms.add_noise` transform:
.. code-block:: python3
import pennylane as qml
dev = qml.device("default.mixed", wires=2)
fcond = qml.noise.wires_in(dev.wires)
noise = qml.noise.partial_wires(qml.AmplitudeDamping, 0.05)
noise_model = qml.NoiseModel({fcond: noise})
noisy_dev = qml.add_noise(dev, noise_model)
.. note ::
The :func:`~.transforms.add_noise` transform should be used on the device instead of
the circuit if the defined ``noise_model`` contains a :class:`~.operation.Channel`
instance. This is to prevent ``mitigate_with_zne`` from computing the adjoint of
the channel operation during `folding`, which is currently not supported.
We can now set up a mitigated ``QNode`` by first decomposing it into a target gate set via :func:`~.pennylane.transforms.decompose`
and then applying this transform by passing ``folding`` and ``extrapolate`` functions. PennyLane provides native
functions :func:`~.pennylane.transforms.fold_global` and :func:`~.pennylane.transforms.poly_extrapolate`, or
:func:`~.pennylane.transforms.richardson_extrapolate`, that allow for differentiating through them. Custom functions, as well as
functionalities from the `Mitiq <https://mitiq.readthedocs.io/en/stable/>`__ package are supported as well (see usage details below).
.. code-block:: python3
import numpy as np
from functools import partial
from pennylane import qnode
from pennylane.transforms import fold_global, poly_extrapolate
n_wires = 2
n_layers = 2
shapes = qml.SimplifiedTwoDesign.shape(n_layers, n_wires)
np.random.seed(0)
w1, w2 = [np.random.random(s) for s in shapes]
@partial(
qml.transforms.mitigate_with_zne,
scale_factors=[1., 2., 3.],
folding=fold_global,
extrapolate=poly_extrapolate,
extrapolate_kwargs={'order' : 2},
)
@partial(qml.transforms.decompose, gate_set = ["RY", "CZ"])
@qnode(noisy_dev)
def circuit(w1, w2):
qml.SimplifiedTwoDesign(w1, w2, wires=range(2))
return qml.expval(qml.Z(0))
Executions of ``circuit`` will now be mitigated:
>>> circuit(w1, w2)
0.19113067088978522
The unmitigated circuit result is ``0.33652776`` while the ideal circuit result is
``0.23688169`` and we can hence see that mitigation has helped reduce our estimation error.
This mitigated qnode can be differentiated like any other qnode.
>>> qml.grad(circuit)(w1, w2)
(array([-0.89319941, 0.37949841]),
array([[[-7.04121596e-01, 3.00073104e-01]],
[[-6.41155176e-01, 8.32667268e-17]]]))
.. note::
As of PennyLane v0.39, the native function :func:`~.pennylane.transforms.fold_global`
no longer decomposes the circuit as part of the folding procedure. Users are
encouraged to use :func:`~.pennylane.transforms.decompose` to unroll the circuit into a target
gateset before folding, when using this transform.
.. details::
:title: Usage Details
**Theoretical details**
A summary of ZNE can be found in `LaRose et al. <https://arxiv.org/abs/2009.04417>`__. The
method works by assuming that the amount of noise present when a circuit is run on a
noisy device is enumerated by a parameter :math:`\gamma`. Suppose we have an input circuit
that experiences an amount of noise equal to :math:`\gamma = \gamma_{0}` when executed.
Ideally, we would like to evaluate the result of the circuit in the :math:`\gamma = 0`
noise-free setting.
To do this, we create a family of equivalent circuits whose ideal noise-free value is the
same as our input circuit. However, when run on a noisy device, each circuit experiences
a noise equal to :math:`\gamma = s \gamma_{0}` for some scale factor :math:`s`. By
evaluating the noisy outputs of each circuit, we can extrapolate to :math:`s=0` to estimate
the result of running a noise-free circuit.
A key element of ZNE is the ability to run equivalent circuits for a range of scale factors
:math:`s`. When the noise present in a circuit scales with the number of gates, :math:`s`
can be varied using `unitary folding <https://ieeexplore.ieee.org/document/9259940>`__.
Unitary folding works by noticing that a unitary :math:`U` is equivalent to
:math:`U U^{\dagger} U`. This type of transform can be applied to individual gates in the
circuit or to the whole circuit. When no folding occurs, the scale factor is
:math:`s=1` and we are running our input circuit. On the other hand, when each gate has been
folded once, we have tripled the amount of noise in the circuit so that :math:`s=3`. For
:math:`s \geq 3`, each gate in the circuit will be folded more than once. A typical choice
of scale parameters is :math:`(1, 2, 3)`.
**Unitary folding**
This transform applies ZNE to an input circuit using the unitary folding approach. It
requires a callable to be passed as the ``folding`` argument with signature
.. code-block:: python
fn(circuit, scale_factor, **folding_kwargs)
where
- ``circuit`` is a quantum tape,
- ``scale_factor`` is a float, and
- ``folding_kwargs`` are optional keyword arguments.
The output of the function should be the folded circuit as a quantum tape.
Folding functionality is available from the
`Mitiq <https://mitiq.readthedocs.io/en/stable/>`__ package (version 0.11.0 and above)
in the
`zne.scaling.folding <https://mitiq.readthedocs.io/en/stable/apidoc.html#module-mitiq.zne.scaling.folding>`__
module.
.. warning::
Calculating the gradient of mitigated circuits is not supported when using the Mitiq
package as a backend for folding or extrapolation.
**Extrapolation**
This transform also requires a callable to be passed to the ``extrapolate`` argument that
returns the extrapolated value(s). Its function should be
.. code-block:: python
fn(scale_factors, results, **extrapolate_kwargs)
where
- ``scale_factors`` are the ZNE scale factors,
- ``results`` are the execution results of the circuit at the specified scale
factors of shape ``(len(scale_factors), len(qnode_returns))``, and
- ``extrapolate_kwargs`` are optional keyword arguments.
The output of the extrapolate ``fn`` should be a flat array of
length ``len(qnode_returns)``.
Extrapolation functionality is available using ``extrapolate``
methods of the factories in the
`mitiq.zne.inference <https://mitiq.readthedocs.io/en/stable/apidoc.html#module-mitiq.zne.inference>`__
module.
"""
folding_kwargs = folding_kwargs or {}
extrapolate_kwargs = extrapolate_kwargs or {}
tape = tape.expand(stop_at=lambda op: not isinstance(op, QuantumScript))
script_removed = QuantumScript(tape.operations[tape.num_preps :])
tapes = [
[folding(script_removed, s, **folding_kwargs) for _ in range(reps_per_factor)]
for s in scale_factors
]
tapes = [tape_ for tapes_ in tapes for tape_ in tapes_] # flattens nested list
# if folding was a batch transform, ignore the processing function
if isinstance(tapes[0], tuple) and isinstance(tapes[0][0], list) and callable(tapes[0][1]):
tapes = [t[0] for t, _ in tapes]
prep_ops = tape.operations[: tape.num_preps]
out_tapes = [tape.copy(operations=prep_ops + tape_.operations) for tape_ in tapes]
def processing_fn(results):
"""Maps from input tape executions to an error-mitigated estimate"""
# content of `results` must be modified in this post-processing function
results = list(results)
for i, tape in enumerate(out_tapes):
# stack the results if there are multiple measurements
# this will not create ragged arrays since only expval measurements are allowed
if len(tape.observables) > 1:
results[i] = qml.math.stack(results[i])
# Averaging over reps_per_factor repetitions
results_flattened = []
for i in range(0, len(results), reps_per_factor):
# The stacking ensures the right interface is used
# averaging over axis=0 is critical because the qnode may have multiple outputs
results_flattened.append(mean(qml.math.stack(results[i : i + reps_per_factor]), axis=0))
extrapolated = extrapolate(scale_factors, results_flattened, **extrapolate_kwargs)
extrapolated = extrapolated[0] if shape(extrapolated) == (1,) else extrapolated
# unstack the results in the case of multiple measurements
return extrapolated if shape(extrapolated) == () else tuple(qml.math.unstack(extrapolated))
return out_tapes, processing_fn
_modules/pennylane/transforms/mitigate
Download Python script
Download Notebook
View on GitHub