diff --git a/docs/_index_included.yaml b/docs/_index_included.yaml index 29f97aef..0583fcb5 100644 --- a/docs/_index_included.yaml +++ b/docs/_index_included.yaml @@ -124,6 +124,15 @@ included: description: Analyze published experimental wavefunctions. path: /cirq/experiments/qcqmc/experimental_wavefunctions +- heading: Quantum Error Correction + description: Surface Code Memory Experiment + options: + - cards + items: + - heading: Quantum Engine tutorial + path: /cirq/experiments/qec_memory/surface_code_lambda + - heading: Code Layout + - heading: Lattice Gauge Theory description: Simulations for Charges and Strings in (2+1)D Lattice Gauge Theories options: diff --git a/docs/_toc.yaml b/docs/_toc.yaml index f9b8ebdc..ad39e8f3 100644 --- a/docs/_toc.yaml +++ b/docs/_toc.yaml @@ -84,6 +84,10 @@ toc: - title: "Experimental Wavefunctions" path: /cirq/experiments/qcqmc/experimental_wavefunctions +- heading: "Quantum Error Correction" +- title: "Surface Code Memory Experiment" + path: /cirq/experiments/qec_memory/surface_code_lambda + - heading: "Lattice Gauge Theory" - title: "Simulation of Charges and Strings" path: /cirq/experiments/lattice_gauge/lattice_gauge diff --git a/docs/qec_memory/index.md b/docs/qec_memory/index.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/qec_memory/surface_code_lambda.ipynb b/docs/qec_memory/surface_code_lambda.ipynb new file mode 100644 index 00000000..5449f928 --- /dev/null +++ b/docs/qec_memory/surface_code_lambda.ipynb @@ -0,0 +1,521 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tC-E2pe2p2i4" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zEvb2aKbqlOU" + }, + "source": [ + "# Surface code memory experiment\n", + "\n", + "The goal of this ReCirq tutorial is to reproduce Figure 1c from the Nature paper, \"Quantum error correction below the surface code threshold,\" *Nature* **638** 920-926 (2025). https://www.nature.com/articles/s41586-024-08449-y. We run a surface code memory experiment for various code distances and hopefully see the logical error rate decrease as the code distance increases, quantified by the parameter $\\Lambda$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0BS-Zpfiqqt3" + }, + "source": [ + "## Imports\n", + "\n", + "Before beginning, we install and import the necessary modules." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yZRWmZ8Sqi9a" + }, + "outputs": [], + "source": [ + "try:\n", + " import cirq\n", + " assert hasattr(cirq.transformers, 'apply_lazy_args_on_circuit_operation')\n", + "except:\n", + " !pip install --upgrade cirq~=1.0.dev" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "v9Up1M5KoyFa" + }, + "outputs": [], + "source": [ + "try:\n", + " import recirq.qec_memory.decoding as qec_decoding\n", + " import wget\n", + "except:\n", + " !pip install \"recirq[qec_memory] @ git+https://github.com/quantumlib/ReCirq.git\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BF5PB0z4rCBd" + }, + "outputs": [], + "source": [ + "import copy\n", + "import time\n", + "import zipfile\n", + "from typing import Literal\n", + "\n", + "import cirq_google\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import stim\n", + "import wget\n", + "from google.colab import auth\n", + "from IPython.display import display\n", + "\n", + "import recirq.qec_memory.analysis as qec_analysis\n", + "import recirq.qec_memory.circuits as qec_circuits\n", + "import recirq.qec_memory.decoding as qec_decoding" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UJTPJe2Lsa-E" + }, + "source": [ + "## Authentication\n", + "\n", + "Next, we authenticate with Google Cloud in order to use Quantum Engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nlphOZ7osk2S" + }, + "outputs": [], + "source": [ + "auth.authenticate_user(clear_output=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vFexVDNxtE32" + }, + "source": [ + "## Download circuits\n", + "\n", + "Next, download the stim circuits from Zenodo." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "H7KV6B1-s47o" + }, + "outputs": [], + "source": [ + "# download the circuits\n", + "wget.download('https://zenodo.org/records/23004813/files/ZXXZ circuits.zip')\n", + "\n", + "# unzip them:\n", + "with zipfile.ZipFile('ZXXZ circuits.zip', 'r') as zip_ref:\n", + " zip_ref.extractall()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "abjf6lnOulZX" + }, + "source": [ + "There are two sets of circuits: one for distance 3 and distance 5 surface code and the other for distance 3, 5, and 7 surface code. Specify `config_name` below to pick between the two options. Fill in `processor_id` to match the device that you will use in either case. `distance_to_shifts` hardcodes the allowed placements of the circuits onto the device for each code distance. We will average over these placements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6O0X7paDtxYZ" + }, + "outputs": [], + "source": [ + "config_name = 'd5' # choose between d5 or d7\n", + "\n", + "if config_name == 'd5':\n", + " processor_id = '' # fill this in\n", + " distance_to_shifts = {\n", + " 3: [(0,0), (0,4), (2,2), (-2,2)],\n", + " 5: [(0,0)]\n", + " }\n", + " directory = 'd3_d5/'\n", + "elif config_name == 'd7':\n", + " processor_id = '' # fill this in\n", + " distance_to_shifts = {\n", + " 3: [(0, 0),\n", + " (-2, 2),\n", + " (-4, 4),\n", + " (2, 2),\n", + " (0, 4),\n", + " (-2, 6),\n", + " (4, 4),\n", + " (2, 6),\n", + " (0, 8)],\n", + " 5: [(0, 0), (-2, 2), (2, 2), (0, 4)],\n", + " 7: [(0, 0)]\n", + " }\n", + " directory = 'd3_d5_d7/'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NC1gdu26vKnp" + }, + "source": [ + "Now we can load the circuits. The Zenodo circuits are all for 10 cycles of error correction; to achieve other cycle numbers, we will modify the looped element within these circuits. When we run these circuits, we will average over the two choices of observable and two various choices of the placement of the circuit onto the device parameterized by `shift`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3RmXS-autVXY" + }, + "outputs": [], + "source": [ + "def get_zxxz_circuit(\n", + " distance: int, observable: Literal[\"H\", \"V\"], shift: tuple[int, int]\n", + ") -> stim.Circuit:\n", + " \"\"\"Load a XZZX surface code circuit from a file.\n", + "\n", + " Args:\n", + " distance: The code distance.\n", + " observable: Which basis to measure in.\n", + " shift: Determines which qubits are used.\n", + "\n", + " Returns:\n", + " A stim circuit for the memory experiment.\n", + " \"\"\"\n", + " circuit = stim.Circuit.from_file(\n", + " directory\n", + " + f\"distance_{distance}_cycles_10_observable_{observable}_shift_{shift}.stim\"\n", + " )\n", + " # remove the sweep bits and subsequent TICK\n", + " nq = 2 * distance**2 - 1\n", + " return circuit[:nq] + circuit[(nq + 2) :]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4UH-rVC_wpdI" + }, + "source": [ + "Let's inspect what oen of these circuits looks like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GRAXIquEtk33" + }, + "outputs": [], + "source": [ + "circuit = get_zxxz_circuit(3, 'H', (0,0))\n", + "print(circuit.diagram())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Y-Hoh7P0xFOi" + }, + "source": [ + "A distance-$d$ surface code should have $d^2$ data qubits and $d^2-1$ measure qubits to measure each of the $d^2-1$ stabilizers. Here $d=3$, and you can see that, indeed, there are $9+8=17$ qubits, of which 8 (the measure qubits) are measured mid-circuit. You can step through the circuit interactively using Crumble:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HYJa6F45wwlJ" + }, + "outputs": [], + "source": [ + "print(circuit.to_crumble_url())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n87zSXj8ytyY" + }, + "source": [ + "Now, let's look at the other observable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "or1nAYctyJC9" + }, + "outputs": [], + "source": [ + "circuit = get_zxxz_circuit(3, 'V', (0,0))\n", + "print(circuit.diagram())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kvehBC0Ezlv8" + }, + "source": [ + "You can see that this differs by Hadamards on the data qubits from the other observable/basis." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HYJpw1SSz1YI" + }, + "source": [ + "## Load the Engine and Sampler objects for data taking" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ClmBAWoHyzuB" + }, + "outputs": [], + "source": [ + "engine = cirq_google.get_engine(\"\") ## fill in your GCP project\n", + "sampler = engine.get_sampler(\n", + " processor_id, circuits_per_job=200, device_config_name=config_name\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DxCmqbvO0DBG" + }, + "source": [ + "## Specify data taking parameters\n", + "\n", + "`qec_recipe` specifies which hardware techniques we will add to the circuit. See, for example, https://www.nature.com/articles/s41567-023-02226-w for a description of DQLR.\n", + "\n", + "We will prepare the data qubits in random computational basis states at the beginning of the circuit. The number of choices is specified by `num_sweep_bit_choices`.\n", + "\n", + "`repetitions` is the number of times we will repeat each circuit.\n", + "\n", + "`cycles_list` specifies the number of error correction cycles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6TegLiDh0B2-" + }, + "outputs": [], + "source": [ + "qec_recipe = [\n", + " \"ADEPT\",\n", + " \"ECHO\",\n", + " \"DQLR_MSMLR\",\n", + " \"DD\",\n", + " \"RESET_AFTER_MEAS\",\n", + " \"READOUT_RESET_TAG\",\n", + "]\n", + "num_sweep_bit_choices = 10\n", + "repetitions = 1000\n", + "cycles_list = np.arange(10, 251, 20)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xUwk_j6q1Vkc" + }, + "source": [ + "## Take the data!\n", + "\n", + "The following cell takes the data by first iterating the choices of `SurfaceCodeParams` (code distance, shift, and observable) in a random order. For each such choice, we perform the following steps:\n", + "1. The hardware techniques are added to the circuit using `engine.compile_circuit`.\n", + "2. The ADEPT phases (single-qubit Z rotations added to cancel a certain type of coherent errors) are calibrated using `engine.calibrate_for_circuit`. See [arXiv:1603.03082](https://arxiv.org/abs/1603.03082).\n", + "3. The circuits are generated for all of the desired cycle numbers and sweep bit choices in a random order.\n", + "4. The data is taken using `sampler.run_batch`.\n", + "5. Decoding is performed using `qec_decoding.get_logical_error_probability`, which uses pymatching and a detector error model built from `stimflow.NoiseModel.si1000`.\n", + "6. In the command `lambda_results.plot(ax)`, the logical error probability vs cycle number is plotted, and an exponential is fitted to extract the logical error rate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Eg4EvLO8089h" + }, + "outputs": [], + "source": [ + "# repeat the cycle numbers num_sweep_bit_choices times.\n", + "# For each one we will add random sweep bits.\n", + "cycles_times_sweep_bits = np.repeat(cycles_list, num_sweep_bit_choices)\n", + "\n", + "# shuffle the order of cycle numbers\n", + "rng = np.random.default_rng()\n", + "rng.shuffle(cycles_times_sweep_bits)\n", + "\n", + "# set up some variables that we will use to keep track of the iterations\n", + "bases = [\"H\", \"V\"]\n", + "total_param_choices = len(bases) * sum(\n", + " len(shifts) for shifts in distance_to_shifts.values()\n", + ")\n", + "done_params = set()\n", + "\n", + "# we will store the results in here:\n", + "lambda_results = qec_analysis.LambdaExperimentResults(\n", + " cycles_list, repetitions, num_sweep_bit_choices\n", + ")\n", + "\n", + "# and plot them in here:\n", + "fig, ax = plt.subplots(dpi=150, facecolor=\"white\")\n", + "\n", + "# we will print out how long it is taking:\n", + "start_time = time.time()\n", + "time_string = (\n", + " lambda: f\"{(time.time()-start_time)//60:.0f}:{(time.time()-start_time)%60:02.0f}\"\n", + ")\n", + "\n", + "# start data taking\n", + "while len(done_params) < total_param_choices:\n", + " # first choose the code distance\n", + " distance = rng.choice(list(distance_to_shifts.keys()))\n", + "\n", + " # the following if block helps us get a lambda estimate faster but can be\n", + " # deleted to take data in a random order\n", + " if len(done_params) == 1:\n", + " if next(iter(done_params)).distance == 3:\n", + " distance = 5\n", + " elif next(iter(done_params)).distance == 5:\n", + " distance = 3\n", + " elif next(iter(done_params)).distance == 7:\n", + " distance = 5\n", + "\n", + " # next choose from the allowed placements on the device (shifts)\n", + " shift_idx = rng.integers(0, len(distance_to_shifts[distance]))\n", + " shift = distance_to_shifts[distance][shift_idx]\n", + "\n", + " # next choose the observable\n", + " observable = rng.choice(bases)\n", + "\n", + " # skip if we have already taken data for these params\n", + " params = qec_analysis.SurfaceCodeParams(distance, observable, shift)\n", + " if params in done_params:\n", + " continue\n", + " print(f\"{time_string()}: Starting d={distance}, shift={shift}, basis={observable}\")\n", + "\n", + " # get the Zenodo stim circuit\n", + " stim_circuit = get_zxxz_circuit(distance, observable, shift)\n", + "\n", + " # add the hardware techniques\n", + " print(f\"{time_string()}: Compiling hardware circuit\")\n", + " hardware_circuit = engine.compile_circuit(\n", + " stim_circuit,\n", + " qec_recipe,\n", + " processor_id=processor_id,\n", + " config_name=config_name,\n", + " )\n", + "\n", + " # calibrate the ADEPT phases. See arXiv:1603.03082.\n", + " print(f\"{time_string()}: Calibrating ADEPT\")\n", + " resolver = engine.calibrate_for_circuit(\n", + " hardware_circuit, processor_id, config_name=config_name\n", + " )\n", + "\n", + " # Generate the circuits for the various cycle numbers and sweep bits.\n", + " # Uncomment the following line to use a different random order of cycles for\n", + " # each choice of params; otherwise the same order is reused.\n", + " # rng.shuffle(cycles_times_sweep_bits) # uncomment to\n", + " print(f\"Order of cycles: {cycles_times_sweep_bits}\")\n", + " circuits = [\n", + " qec_circuits.add_sweep_bits(\n", + " qec_circuits.replace_loop_repetitions(\n", + " hardware_circuit, cycles, original_cycles=10\n", + " ),\n", + " rng,\n", + " )\n", + " for cycles in cycles_times_sweep_bits\n", + " ]\n", + "\n", + " # Take the data.\n", + " print(f\"{time_string()}: Taking data\")\n", + " result = sampler.run_batch(\n", + " circuits,\n", + " repetitions=repetitions,\n", + " params_list=[resolver] * len(cycles_times_sweep_bits),\n", + " )\n", + "\n", + " # Extract the logical error probability.\n", + " print(f\"{time_string()}: Analyzing data\")\n", + " lep = np.array(\n", + " [\n", + " qec_decoding.get_logical_error_probability(res_i[0].data, circuit, 0.001)\n", + " for circuit, res_i in zip(circuits, result)\n", + " ]\n", + " )\n", + "\n", + " # Fit the logical error rate and plot the results. Extract Λ.\n", + " lambda_results.add_result(params, lep, copy.deepcopy(cycles_times_sweep_bits))\n", + " ax.clear()\n", + " ax = lambda_results.plot(ax)\n", + " display(fig)\n", + " done_params.add(params)\n" + ] + } + ], + "metadata": { + "colab": { + "name": "surface_code_lambda.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/recirq/qec_memory/extra-requirements.txt b/recirq/qec_memory/extra-requirements.txt index 3d3462b9..cab58657 100644 --- a/recirq/qec_memory/extra-requirements.txt +++ b/recirq/qec_memory/extra-requirements.txt @@ -2,3 +2,4 @@ pymatching stim stimcirq stimflow +wget