Toric Code Ground State

View on QuantumAI Run in Google Colab View source on GitHub Download notebook

Before beginning, we will import the necessary modules into the colab.

try:
    import recirq
except ImportError:
    !pip install --quiet git+https://github.com/quantumlib/ReCirq
    import recirq

try:
    import qsimcirq
except ImportError:
    !pip install qsimcirq --quiet
    import qsimcirq

import cirq
import matplotlib.pyplot as plt

import recirq.toric_code.toric_code_plaquettes as tcp
import recirq.toric_code.toric_code_plotter as tcplot
import recirq.toric_code.toric_code_rectangle as tcr
import recirq.toric_code.toric_code_state_prep as tcsp

plt.rcParams['figure.dpi'] = 144

Toric code Hamiltonian

The toric code Hamiltonian

\begin{equation} H = -\sum_s A_s - \sum_p B_p \end{equation}

involves local four-qubit parity operators, where each qubit lives on an edge in a square lattice. Here, the "star" operators \(A_s\) are products of Pauli \(Z\) operators around a vertex, while the "plaquette" operators \(B_p\) are products of \(X\) operators around a square, for example,

\begin{equation} A_s = Z_i \otimes Z_j \otimes Z_k \otimes Z_l \end{equation} \begin{equation} B_p = X_a \otimes X_b \otimes X_c \otimes X_d. \end{equation}

These local parity operators all commute with each other: all \(A_s\) commute, all \(B_p\) commute, and \(A_s\) and \(B_p\) commute with each other because they overlap on an even number of qubits. They can thus all be simultaneously diagonalized, and those shared eigenstates are also the eigenstates of \(H\).

Toric Code Example

In our paper, we mostly work with the 31-qubit lattice above. With these boundary conditions, there is a unique ground state that has a \(+1\) eigenvalue for all \(A_s\) and \(B_p\). Note for different boundary conditions, we can have degeneracies that are locally-indistinguishable (for example on a torus, or with the "surface code" logical qubits we explore in Figure 4 of our paper).

In this module, we will primarily work with the smaller 22-qubit to avoid time and memory constraints associated with the larger rectangle.

Understanding the ground state

In this example, we focus on reproducing our first figure, where we create this unique ground state \(|G\rangle\) using a shallow unitary circuit. The general idea is to start out with \(|0\rangle^{\otimes 22}\), so all \(\langle A_s \rangle = +1\). We then apply projection operators \(\mathbb{I} + B_p\) which project the state into a \(+1\) eigenstate of \(B_p\), after which all the local parities are \(+1\):

\begin{equation} |G\rangle \propto \prod_p (\mathbb{I} + B_p)|0\rangle^{\otimes 22}. \end{equation}

To create this state, we assign a "team captain" qubit to each plaquette \(B_p\). Starting from \(|0\rangle^{\otimes n}\), we perform a Hadamard on each team captain, and then each team captain is responsible for performing a CNOT to each of its team mates. We have to be careful with the ordering to keep things efficient and avoid the captains stepping on each other's toes. This is easier to visualize for a smaller system, for example the 12-qubit version in Figure S2, reproduced below. Note the superposition of \(2^4\) states, as there are four plaquettes \(B_p\).

12 qubit toric code example

Creating \(|G\rangle\) with ReCirq

Basics: 22-qubit circuit

First, we can create a example 22-qubit grid by instantiate it using a ToricCodeRectangle object and then plot a visualization using a ToricCodePlotter object, both found in the ReCirq repository.

short_rectangle = tcr.ToricCodeRectangle(
    origin_qubit=cirq.GridQubit(3, 0), row_vector=(1, 1), rows=2, cols=4
)
plotter = tcplot.ToricCodePlotter()
plotter.plot_code(short_rectangle)
<Axes: >

png

We can also see the full circuit of how to create this code (using CNOT gates) using these objects as well. By printing out the circuit moment by moment, we can see the gates lined up in a visual manner.

full_circuit = tcsp.toric_code_cnot_circuit(short_rectangle)
for idx, moment in enumerate(full_circuit):
    print(f'moment {idx}\n{moment}\n')
moment 0
  ╷ 0 1 2 3 4
╶─┼───────────
0 │       H
  │
1 │     H   H
  │
2 │   H   H
  │
3 │ H   H
  │
4 │   H
  │

moment 1
  ╷ 1 2 3
╶─┼───────
1 │   @
  │   │
2 │ @ X @
  │ │   │
3 │ X @ X
  │   │
4 │   X
  │

moment 2
  ╷ 1 2 3 4
╶─┼─────────
1 │   @─X
  │
2 │ @─X @─X
  │
3 │   @─X
  │

moment 3
  ╷ 0 1 2 3 4 5
╶─┼─────────────
0 │       @─X
  │
1 │       @ @─X
  │       │
2 │       X @
  │         │
3 │ @ @─X   X
  │ │
4 │ X @ @─X
  │   │
5 │   X
  │

moment 4
  ╷ 0 1 2 3 4
╶─┼───────────
0 │       @
  │       │
1 │       X @
  │         │
2 │         X
  │
3 │ @─X
  │
4 │   @─X
  │

moment 5
  ╷ 0 1 2 4 5
╶─┼───────────
0 │       @
  │       │
1 │       X @
  │         │
2 │         X
  │
4 │ @─X
  │
5 │   @─X
  │

Simulating the parities

For a given circuit, we can determine all the parity expectation values \(\langle A_s\rangle\) by sampling 22-qubit bitstrings and then computing each expectation value. We do the same thing with for \(\langle B_p \rangle\), but we include a layer of Hadamards before measurement to effectively "measure in \(X\) basis."

def partial_circuit(
    n_moments_to_include: int, *, x_basis: bool
) -> cirq.Circuit:
    """Create the first N moments of a toric in Z or X basis.

    Args:
        n_moments_to_include: number of moments to include
        x_basis: If True, add Hadamards to effectively measure in the X basis.
            If False, measure in the computational (Z) basis.

    Returns: First N moments of a toric code circuit plus an optional
        layer of Hadamard gates to effectively measure in the X basis.
        This circuit also includes measurement gates.
    """
    sliced_circuit = full_circuit[:n_moments_to_include]
    qubits = sorted(short_rectangle.qubits)
    if x_basis:
        sliced_circuit += cirq.Moment(cirq.H.on_each(*qubits))
    return sliced_circuit + cirq.measure(*qubits)


def get_plaquettes(
    n_moment_to_include: int, repetitions: int = 1000,
    sampler: cirq.Sampler = qsimcirq.QSimSimulator()
) -> tcp.ToricCodePlaquettes:
    """Simulates the results in both bases and determine plaquette values.

    Args:
            n_moments_to_include: number of moments to include
            repetitions: number of repetitions (shots) to sample
            sampler: Sampler (simulator) to execute circuits.  Defaults to qsim.
    """
    x_data = sampler.run(
        partial_circuit(n_moment_to_include, x_basis=True), repetitions=repetitions
    )
    z_data = sampler.run(
        partial_circuit(n_moment_to_include, x_basis=False), repetitions=repetitions
    )
    return tcp.ToricCodePlaquettes.from_global_measurements(
        short_rectangle, x_data.data, z_data.data
    )

We can step through the circuit one moment at a time to see how the parities \(A_s\) and \(B_p\) evolve through the circuit. This is similar to Figure 1B in paper (but simulating instead of using experimental data). We begin with \(|0\rangle^{\otimes 22}\), which corresponds to n_moments_to_include=0. There, all \(\langle A_s \rangle = +1\) but \(\langle B_p \rangle = 0\) (see colorbars below). The subsequent moments apply Hadamard and CNOT gates to stitch entanglement across the device and create \(|G\rangle\).

for n in range(len(tcsp.toric_code_cnot_circuit(short_rectangle)) + 1):
    p = get_plaquettes(n)
    ax = plotter.plot_expectation_values(p)
    ax.set_title(f'n_moments_to_include={n}')
    plt.pause(0.001)
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:112: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  parities = data.applymap(
/tmpfs/src/tf_docs_env/lib/python3.10/site-packages/recirq/toric_code/toric_code_plaquettes.py:115: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
  return float(parities.mean())

png

After the final step, all the parities are \(+1\) (see colorbars below), indicating we have successfully created \(|G\rangle\).

ax_z = plotter.make_colorbar(x_basis=False, orientation='horizontal')
ax_z.set_label(r'Z parity, $\langle A\rangle$')

ax_x = plotter.make_colorbar(x_basis=True, orientation='horizontal')
ax_x.set_label(r'X parity, $\langle B\rangle$')

png

png