# What happens when I use ZNE?#

In Mitiq, ZNE is clearly divided into two steps, noise scaling and extrapolation. They are shown in the Figure below.
The corresponding sub-modules in the codebase are `mitiq.zne.scaling.folding`

and `mitiq.zne.inference`

.

**The first step** involves generating and executing noise-scaled quantum circuits.

The user provides a

`QPROGRAM`

, i.e., a quantum circuit defined via any of the supported frontends.Mitiq generates a set of noise-scaled circuits by applying unitary folding with different scale factors.

The noise-scaled circuits are executed on the noisy backend obtaining a set of noise-scaled expectation values.

**The second step** involves inferring the zero-noise value from the measured results.

A parametrized curve is fit to the noise-scaled expectation values.

The curve is extrapolated to the zero-noise limit, obtaining an error mitigated expectation value.

As demonstrated in How do I use ZNE ?, the function `execute_with_zne()`

applies both steps behind the scenes.
In the next sections instead, we show how one can apply ZNE at a lower level, i.e., by applying each step independently.

Moreover, we will also show how the user can customize noise scaling methods and factory objects.

## First step: generating and executing noise-scaled circuits#

### Problem setup#

We define a circuit and an executor, as shown in How do I use ZNE?.

```
from mitiq import benchmarks
from cirq import DensityMatrixSimulator, depolarize
circuit = benchmarks.generate_rb_circuits(
n_qubits=1, num_cliffords=2, return_type="cirq",
)[0]
def execute(circuit, noise_level=0.01):
"""Returns Tr[ρ |0⟩⟨0|] where ρ is the state prepared by the circuit
executed with depolarizing noise.
"""
noisy_circuit = circuit.with_noise(depolarize(p=noise_level))
rho = DensityMatrixSimulator().simulate(noisy_circuit).final_density_matrix
return rho[0, 0].real
```

### Evaluate noise-scaled expectation values#

We first apply local unitary folding to generate a sequence of noise-scaled circuits.

```
from mitiq import zne
# Choose a list of scale factors
scale_factors = [1.0, 3.0, 5.0]
# Generate a list of folded circuits
noise_scaled_circuits = [zne.scaling.fold_gates_at_random(circuit, s) for s in scale_factors]
```

For each noise-scaled circuit we evaluate the associated expectation value.

```
expectation_values = [execute(circ) for circ in noise_scaled_circuits]
```

## Second step: extrapolating the zero-noise limit#

The simplest way to extrapolate the zero-noise limit of an expectation value is to use the static method `Factory.extrapolate()`

of a `Factory`

object. For example, an exponential extrapolation (assuming an infinite noise limit of \(0.5\)) can be obtained as follows.

```
zne.ExpFactory.extrapolate(scale_factors, expectation_values, asymptote=0.5)
```

```
1.0000001849508315
```

Alternatively, one can also instantiate a `Factory`

object, which can be useful for additional analysis and visualization of the measured data.

```
# Initialize a factory
fac = zne.inference.ExpFactory(scale_factors, asymptote=0.5)
# Load data:
for s, e in zip(scale_factors, expectation_values):
fac.push({"scale_factor": s}, e)
# Evaluate the extrapolation
fac.reduce()
```

```
1.0000001849508315
```

```
# Plot the extrapolation fit
_ = fac.plot_fit()
```

## Custom noise-scaling methods#

Custom folding methods can be defined and used with Mitiq, e.g., with `execute_with_zne()`

. The signature of this function must be as follows:

```
import cirq
from mitiq.interface.conversions import atomic_converter
@atomic_converter
def my_custom_folding_function(circuit: cirq.Circuit, scale_factor: float) -> cirq.Circuit:
# Insert custom folding method here
folded_circuit = circuit # Trivial example for testing
return folded_circuit
```

Note

The `atomic_converter()`

decorator makes it so `my_custom_folding_function`

can be used with any supported circuit type, not just Cirq circuits.
The body of the `my_custom_folding_function`

should assume the input
circuit is a Cirq circuit, however.

This function can then be used with `.zne.execute_with_zne`

as an option to scale the noise:

```
zne.execute_with_zne(circuit, execute, scale_noise=my_custom_folding_function)
```

```
0.9551591277122505
```

## Custom extrapolation methods#

If necessary, the user can modify existing extrapolation methods by subclassing one of the built-in factories.

Alternatively, a custom adaptive extrapolation method can be derived from the abstract class `AdaptiveFactory`

.
In this case its core methods must be implemented:
`AdaptiveFactory.__init__()`

, `AdaptiveFactory.next()`

, `AdaptiveFactory.is_converged()`

, `AdaptiveFactory.reduce()`

.

A new non-adaptive method can instead be derived from the abstract `BatchedFactory`

class.
In this case it is usually sufficient to override only the `BatchedFactory.__init__()`

and
the `BatchedFactory.extrapolate()`

methods, which are responsible for the initialization and for the
final zero-noise extrapolation, respectively. An example of a simple custom non-adaptive `Factory`

is given in the next code cell.

```
from mitiq.zne.inference import BatchedFactory, LinearFactory
import numpy as np
class MyFactory(BatchedFactory):
"""Factory object implementing a linear extrapolation taking
into account that the expectation value must be within a given
interval. If the zero-noise limit falls outside the
interval, its value is clipped.
"""
def __init__(self, scale_factors, min_expval, max_expval):
"""
Args:
scale_factors: The noise scale factors at which
expectation values should be measured.
min_expval: The lower bound for the expectation value.
min_expval: The upper bound for the expectation value.
"""
super(MyFactory, self).__init__(scale_factors)
self._options = {"min_expval": min_expval, "max_expval": max_expval}
@staticmethod
def extrapolate(
scale_factors, exp_values, min_expval, max_expval, full_output = False,
):
"""Fit a linear model and clip its zero-noise limit."""
# Perform standard linear extrapolation
result = LinearFactory.extrapolate(scale_factors, exp_values, full_output)
# Return the clipped zero-noise extrapolation.
if not full_output:
return np.clip(result, min_expval, max_expval)
else:
# In this case "result" is a tuple of extrapolation data
zne_limit = np.clip(result[0], min_expval, max_expval)
return (zne_limit, *result[1:])
```

Using `MyFactory`

as an option for `execute_with_zne()`

, the customized extrapolation is applied.

```
# Test MyFactory clips the result as expected
fac = MyFactory([1, 2, 3], min_expval=0.0, max_expval=0.5)
zne_limit_clipped = zne.execute_with_zne(circuit, execute, factory=fac)
assert zne_limit_clipped == 0.5
```

After defining a custom `Factory`

, we suggest to check that all the methods inherited from the parent class run without errors.

```
# Test parent methods run without errors
fac.get_expectation_values()
fac.get_extrapolation_curve()
fac.get_optimal_parameters()
fac.get_parameters_covariance()
fac.get_scale_factors()
fac.get_zero_noise_limit_error()
zne_limit_clipped = fac.get_zero_noise_limit()
```

### Regression tools in `zne.inference`

#

In the body of the previous `MyFactory`

example, we imported and used the `mitiq_polyfit()`

function.
This is simply a wrap of `numpy.polyfit()`

, slightly adapted to the notion and to the error types
of Mitiq. This function can be used to fit a polynomial ansatz to the measured expectation values. This function performs
a least squares minimization which is **linear** (with respect to the coefficients) and therefore admits an algebraic solution.

Similarly, from `mitiq.zne.inference`

one can also import `mitiq_curve_fit()`

,
which is instead a wrap of `scipy.optimize.curve_fit()`

. Differently from `mitiq_polyfit()`

,
`mitiq_curve_fit()`

can be used with a generic (user-defined) ansatz.
Since the fit is based on a numerical **non-linear** least squares minimization, this method may fail to converge
or could be subject to numerical instabilities.

## Low-level usage of a factory#

In this section we present a low-level usage of a `Factory`

. In typical use-cases, the following information is not necessary but it can be useful for understanding how `Factory`

objects work under the hood.

### The `run`

method.#

The `Factory.run()`

method can be used to run all the data acquisition steps associated to a `Factory`

, until enough data is collected for the extrapolation.

```
fac = zne.inference.AdaExpFactory(steps=5, asymptote=0.5)
# Run the factory to collect data
fac.run(circuit, execute)
# Extrapolate
fac.reduce()
```

```
1.0057045799371627
```

### The `run_classical`

method.#

Instead of `Factory.run()`

, the `Factory.run_classical()`

method can be used if we have at disposal a function which directly
maps a noise scale factor to the corresponding expectation value.

```
def noise_to_expval(scale_factor: float) -> float:
"""Function returning an expectation value for a given scale_factor."""
scaled_circuit = zne.scaling.fold_gates_at_random(circuit, scale_factor)
return execute(scaled_circuit)
```

```
# Remove internal data, if present.
fac.reset()
# Run the factory to collect data
fac.run_classical(noise_to_expval)
# Extrapolate
fac.reduce()
```

```
1.0057044799987562
```

The `Factory.run_classical()`

applies ZNE as a fully classical inference problem.
Indeed, all the quantum aspects of the problem (circuit, observable, backend, etc.) are
wrapped into the `noise_to_expval()`

function.