Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding layerwise folding as tutorial to mitiq. #1894

Merged
merged 11 commits into from
Jul 18, 2023
Binary file added docs/source/_thumbnails/layerwise.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ def get_incollection_template(self, e):
"examples/hamiltonians": "_static/vqe-cirq-pauli-sum-mitigation-plot.png",
"examples/braket_mirror_circuit": "_static/mirror-circuits.png",
"examples/maxcut-demo": "_static/max-cut.png",
"examples/layerwise-folding": "_static/layerwise.png",
"examples/cirq-ibmq-backends": "_static/cirq-mitiq-ibmq.png",
"examples/pennylane-ibmq-backends": "_static/zne-pennylane.png",
"examples/ibmq-backends": "_static/ibmq-gate-map.png",
Expand Down
1 change: 1 addition & 0 deletions docs/source/examples/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ZNE with PyQuil: Improving VQE <vqe-pyquil-demo.ipynb>
ZNE with Cirq: Energy landscape of a variational circuit <simple-landscape-cirq.md>
ZNE with Braket: Energy landscape of a variational circuit <simple-landscape-braket.md>
ZNE with Qiskit: Energy landscape of a variational circuit <simple-landscape-qiskit.md>
ZNE with Qiskit: Layerwise folding <layerwise-folding.md>
ZNE with Cirq: Solving MaxCut with QAOA <maxcut-demo.md>
ZNE with Cirq: Hamiltonian simulation with Pauli gates<hamiltonians.md>
ZNE with Cirq: Energy of molecular Hydrogen <molecular_hydrogen.md>
Expand Down
305 changes: 305 additions & 0 deletions docs/source/examples/layerwise-folding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
---
jupytext:
text_representation:
extension: .myst
vprusso marked this conversation as resolved.
Show resolved Hide resolved
format_name: myst
format_version: 0.13
jupytext_version: 1.11.1
kernelspec:
display_name: Python 3
language: python
name: python3
---

# ZNE with Qiskit: Layerwise folding


This tutorial shows an example of how to mitigate noise on IBMQ backends with
the Cirq frontend using layerwise folding in contrast with global folding.

vprusso marked this conversation as resolved.
Show resolved Hide resolved

- [ZNE with Qiskit: Layerwise folding](#zne-with-qiskit-layerwise-folding)
- [Setup](#setup)
- [Helper functions](#helper-functions)
- [Define circuit to analyze](#define-circuit-to-analyze)
- [Total variational distance metric](#total-variational-distance-metric)
- [Impact of single vs. multiple folding](#impact-of-single-vs-multiple-folding)
- [Executor](#executor)
- [Global folding with linear extrapolation](#global-folding-with-linear-extrapolation)
- [Layerwise folding with linear extrapolation](#layerwise-folding-with-linear-extrapolation)

+++

## Setup

```{code-cell} ipython3
from typing import Dict, List, Optional
import numpy as np
import os
import cirq
import qiskit
import matplotlib.pyplot as plt

from mitiq import zne
from mitiq.zne.scaling.layer_scaling import layer_folding, get_layer_folding
from mitiq.interface.mitiq_qiskit.qiskit_utils import initialized_depolarizing_noise
from mitiq.interface.mitiq_qiskit.conversions import to_qiskit
from mitiq.interface.mitiq_cirq.cirq_utils import sample_bitstrings

from cirq.contrib.svg import SVGCircuit
from qiskit_ibm_provider import IBMProvider

USE_REAL_HARDWARE = False

if USE_REAL_HARDWARE:
# Save account credentials.
#IBMProvider.save_account(token="<IBM_TOKEN_HERE>")

# Load a previously saved account.
provider = IBMProvider()
backend = provider.get_backend("ibmq_qasm_simulator")
noise_model = None
else:
# Default to a simulator.
backend = qiskit.Aer.get_backend("qasm_simulator")
noise_model = initialized_depolarizing_noise(noise_level=0.02)

shots = 10_000
```

**Note:** When `USE_REAL_HARDWARE` is set to `False`, a classically simulated noisy backend is used instead of a real quantum computer.
vprusso marked this conversation as resolved.
Show resolved Hide resolved

## Helper functions

```{code-cell} ipython3
# Display circuit in a more visually appealing way.
draw = lambda circuit: SVGCircuit(circuit)
```

The following function will return a list of circuits where the ith element in
the list is a circuit with layer "i" folded `num_folds` number of times. This
will be useful when analyzing how much folding increases the noise on a given
layer.

```{code-cell} ipython3
def apply_num_folds_to_all_layers(circuit: cirq.Circuit, num_folds: int = 1) -> List[cirq.Circuit]:
"""List of circuits where ith circuit is folded `num_folds` times."""
return [
layer_folding(circuit, [0] * i + [num_folds] + [0] * (len(circuit) - i))
for i in range(len(circuit))
]
```

For instance, consider the following circuit.

```{code-cell} ipython3
# Define a basic circuit for
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
[cirq.ops.H(q0)],
[cirq.ops.CNOT(q0, q1)],
[cirq.measure(cirq.LineQubit(0))],
)
draw(circuit)
vprusso marked this conversation as resolved.
Show resolved Hide resolved
```

Let us invoke the `apply_num_folds_to_all_layers` function as follows.

```{code-cell} ipython3
folded_circuits = apply_num_folds_to_all_layers(circuit, num_folds=2)
```

Note that the first element of the list is the circuit with the first layer of
the circuit folded twice.

```{code-cell} ipython3
draw(folded_circuits[0])
```

Similarly, the second element of the list is the circuit with the second layer
folded.

```{code-cell} ipython3
draw(folded_circuits[1])
```


## Define circuit to analyze

We will use the following circuit to analyze, but of course, you could use
other circuits here as well.

```{code-cell} ipython3
circuit = cirq.Circuit([cirq.X(cirq.LineQubit(0))] * 10, cirq.measure(cirq.LineQubit(0)))
draw(circuit)
```

## Total variational distance metric

An $i$-inversion can be viewed as a local perturbation of the circuit. We want
to define some measure by which we can determine how much such a perturbation
affects the outcome.

Define the quantity:

$$
p(k|C) = \langle \langle k | C | \rho_0 \rangle \rangle
$$

as the probability distribution oveer measurement outcomes at the output of a
vprusso marked this conversation as resolved.
Show resolved Hide resolved
circuit $C$ where $k \in B^n$ with $B^n$ being the set of all $n$-length bit
strings where $\langle \langle k |$ is the vectorized (non-ideal) POVM element
vprusso marked this conversation as resolved.
Show resolved Hide resolved
that corresponds to measuring bit string $k$.

The *impact* of applying an inversion is given by

$$
d \left[p(\cdot|C), p(\cdot|C^{(i)})\right]
$$

where $d$ is some distance measure. In
[arXiv:2204.06056](https://arxiv.org/abs/2204.06056), the authors used the
vprusso marked this conversation as resolved.
Show resolved Hide resolved
total variatonal distance (TVD) measure where

$$
\eta^{(i)} := \frac{1}{2} \sum_{k} |p(k|C) - p(k|C^{(i)})|.
$$

```{code-cell} ipython3
def tvd(circuit: cirq.Circuit, num_folds: int = 1, shots: int = 10_000) -> List[float]:
"""Compute the total variational distance (TVD) between ideal circuit and folded circuit(s)."""
circuit_dist = sample_bitstrings(circuit=circuit, shots=shots).prob_distribution()

folded_circuits = apply_num_folds_to_all_layers(circuit, num_folds)

distances: Dict[int, float] = {}
for i, folded_circuit in enumerate(folded_circuits):
folded_circuit_dist = sample_bitstrings(circuit=folded_circuit, shots=shots).prob_distribution()

res: float = 0.0
for bitstring in circuit_dist.keys():
res += np.abs(circuit_dist[bitstring] - folded_circuit_dist[bitstring])
distances[i] = res / 2

return distances
```

## Impact of single vs. multiple folding

We can plot the impact of applying layer inversions to the circuit.

```{code-cell} ipython3
def plot_single_vs_multiple_folding(circuit: cirq.Circuit) -> None:
"""Plot how single vs. multiple folding impact the error at a given layer."""
single_tvd = tvd(circuit, num_folds=1).values()
multiple_tvd = tvd(circuit, num_folds=5).values()

labels = [f"L{i}" for i in range(len(circuit))]
x = np.arange(len(labels)) # the label locations
width = 0.35 # the width of the bars

fig, ax = plt.subplots()
rects1 = ax.bar(x - width/2, single_tvd, width, label="single")
rects2 = ax.bar(x + width/2, multiple_tvd, width, label="multiple")

# Add some text for labels, title and custom x-axis tick labels, etc.
ax.set_xlabel(r"$L_{G_i \theta_i}$")
ax.set_ylabel(r"$\eta^{(i)}$")
ax.set_title("Single vs. multiple folding")
ax.set_xticks(x, labels, rotation=60)

ax.legend()

ax.bar_label(rects1, padding=3)
ax.bar_label(rects2, padding=3)

fig.tight_layout()

plt.show()
```

```{code-cell} ipython3
plot_single_vs_multiple_folding(circuit)
```

As can be seen, the amount of noise on each layer is increased if the number of
folds on that layer are increased.

## Executor

Next, we define an executor function that will allow us to run our experiment

```{code-cell} ipython3
def executor(circuit: cirq.Circuit, shots: int = 10_000) -> float:
"""Returns the expectation value to be mitigated.

Args:
circuit: Circuit to run.
shots: Number of times to execute the circuit to compute the expectation value.
"""
qiskit_circuit = to_qiskit(circuit)
job = qiskit.execute(
experiments=qiskit_circuit,
backend=backend,
noise_model=noise_model,
basis_gates=noise_model.basis_gates if noise_model is not None else None,
optimization_level=0, # Important to preserve folded gates.
shots=shots,
)

# Convert from raw measurement counts to the expectation value
counts = job.result().get_counts()

expectation_value = 0.0 if counts.get("0") is None else counts.get("0") / shots
return expectation_value
```

## Global folding with linear extrapolation

First, for comparison, we apply ZNE with global folding on the entire circuit.
We then compare the mitigated result of applying ZNE with linear extrapolation
against the unmitigated value.


```{code-cell} ipython3
unmitigated = executor(circuit)

linear_factory = zne.inference.LinearFactory(scale_factors=[1.0, 1.5, 2.0, 2.5, 3.0])
mitigated = zne.execute_with_zne(circuit, executor, factory=linear_factory)

print(f"Unmitigated result {unmitigated:.3f}")
print(f"Mitigated result {mitigated:.3f}")
```

## Layerwise folding with linear extrapolation

For contrast, we apply layerwise folding on only the layer with the most noise
and use linear extrapolation. As above, we compare the mitigated and
unmitigated values.

```{code-cell} ipython3
# Calculate the TVDs of each layer in the circuit (with `num_folds=3`):
tvds = tvd(circuit, num_folds=3)

# Fold noisiest layer only.
layer_to_fold = max(tvds, key=tvds.get)
fold_layer_func = zne.scaling.get_layer_folding(layer_to_fold)

mitigated = zne.execute_with_zne(circuit, executor, scale_noise=fold_layer_func, factory=linear_factory)
print(f"Mitigated (layerwise folding) result {mitigated:.3f}")
print(f"Unmitigated result {unmitigated:.3f}")
```

**Note**: While doing layerwise folding on the noisiest layer will, on average,
improve the mitigated value, it still will not eclipse the benefit of doing
global folding.
vprusso marked this conversation as resolved.
Show resolved Hide resolved

So why consider this technique in this context? One reason is that applying
global folding will increase the length of the entire circuit while layerewise
folding on a subset of only the noisiest layers will increase the circuit by a
smaller factor.

If running a circuit on hardware is bottlenecked by the cost of running a long
circuit, this technique could potentially be used to arrive at a better result
(although not as good as global folding) but with less monetary cost.