Skip to content

Commit

Permalink
Adding layerwise folding as tutorial to mitiq. (#1894)
Browse files Browse the repository at this point in the history
* Adding layerwise folding as tutorial to mitiq.

* fixing typing issue for python3.10.

* Update docs/source/examples/layerwise-folding.md

Co-authored-by: Nathan Shammah <[email protected]>

* Fixing doc warnings. Adding thumbnail.

* Update docs/source/examples/layerwise-folding.md

Co-authored-by: Andrea Mari <[email protected]>

* Responding to diff comments.

* Update docs/source/examples/layerwise-folding.md

Co-authored-by: nate stemen <[email protected]>

* Update docs/source/examples/layerwise-folding.md

Co-authored-by: nate stemen <[email protected]>

* Moving motivational material to top.

* Removing duplicated content from tutorial.

---------

Co-authored-by: vrusso <[email protected]>
Co-authored-by: Nathan Shammah <[email protected]>
Co-authored-by: Andrea Mari <[email protected]>
Co-authored-by: nate stemen <[email protected]>
  • Loading branch information
5 people authored Jul 18, 2023
1 parent bfbeb1c commit fc92819
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 0 deletions.
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 Qiskit: Quantum simulation of quantum many body scars <quantum_simulation_scars_ibmq.md>
ZNE with Cirq: Solving MaxCut with QAOA <maxcut-demo.md>
ZNE with Cirq: Hamiltonian simulation with Pauli gates<hamiltonians.md>
Expand Down
291 changes: 291 additions & 0 deletions docs/source/examples/layerwise-folding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
---
jupytext:
text_representation:
extension: .md
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 using
layerwise folding in contrast with global folding.

One may ask why folding by layer is potentially beneficial to consider. One
reason is that applying global folding will increase the length of the entire
circuit while layerwise folding on a subset of only the noisiest layers will
increase the circuit by a smaller factor.

If running a circuit on hardware is bottle-necked 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.

More information on the layerwise folding technique can be found in
*Calderon et al. Quantum (2023)* {cite}`Calderon_2023_Quantum`.


- [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
# Default to a simulator.
backend = qiskit.Aer.get_backend("qasm_simulator")
noise_model = initialized_depolarizing_noise(noise_level=0.02)
shots = 10_000
```

## Helper functions

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))],
)
print(circuit)
```

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
print(folded_circuits[0])
```

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

```{code-cell} ipython3
print(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)))
print(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 over measurement outcomes at the output of a
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 POVM element 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
*Calderon et al. Quantum (2023)* {cite}`Calderon_2023_Quantum` the authors used the total variational 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.
```

0 comments on commit fc92819

Please sign in to comment.