diff --git a/docs/tutorials/_toc.json b/docs/tutorials/_toc.json index 6c462de7218..c9f7f1b2324 100644 --- a/docs/tutorials/_toc.json +++ b/docs/tutorials/_toc.json @@ -242,7 +242,9 @@ }, { "title": "Low-overhead error detection with spacetime codes", - "url": "/docs/tutorials/ghz-spacetime-codes" + "url": "/docs/tutorials/ghz-spacetime-codes", + "isNew": true, + "isNewDate": "2026-06-29" } ] } diff --git a/docs/tutorials/ghz-spacetime-codes.ipynb b/docs/tutorials/ghz-spacetime-codes.ipynb index 49382566ecd..6e0d7ef9d1a 100644 --- a/docs/tutorials/ghz-spacetime-codes.ipynb +++ b/docs/tutorials/ghz-spacetime-codes.ipynb @@ -2,250 +2,354 @@ "cells": [ { "cell_type": "markdown", - "id": "59c663c5", + "id": "frontmatter", "metadata": {}, "source": [ "---\n", "title: Low-overhead error detection with spacetime codes\n", - "description: Experiment with error detection on a small-scale random Clifford circuit, then walk through the task of GHZ state preparation.\n", + "description: Automatically insert spacetime Pauli checks with qiskit-paulice and boost sampled Clifford-circuit fidelity by postselecting on check syndromes.\n", "---\n", "\n", - "\n", - "{/* cspell:ignore unflagged Tunables parallelizable uncomputation */}\n", - "\n", "# Low-overhead error detection with spacetime codes\n", "\n", - "*Usage estimate: 10 seconds on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*\n", - "\n", + "*Usage estimate: 1 minute on a Heron r3 processor (NOTE: This is an estimate only. Your runtime may vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "learning-outcomes", + "metadata": {}, + "source": [ "## Learning outcomes\n", "\n", - "After going through this tutorial, users should understand the following:\n", - "- The basics of setting up an error detection technique using spacetime codes\n", - "- How to set up a high-fidelity GHZ state on hardware using error detection\n", + "After completing this tutorial, you can expect to understand the following information:\n", "\n", + "- How spacetime Pauli checks detect logical errors in Clifford circuits, and how postselecting on their syndromes boosts the fidelity of a sampled distribution.\n", + "- How to use the `qiskit-paulice` package to find and insert hardware-efficient checks automatically with `get_check_qubits`, `NoiseModel`, and `add_pauli_checks`.\n", + "- How to estimate the fidelity of a stabilizer state by sampling its stabilizers and postselecting on check syndromes.\n", + "- How to run the full error-detection workflow on IBM Quantum® hardware and compare noisy and postselected fidelities." + ] + }, + { + "cell_type": "markdown", + "id": "prerequisites", + "metadata": {}, + "source": [ "## Prerequisites\n", "\n", - "It is recommended that you familiarize yourself with this topic:\n", - "- [Hardware fundamentals](/learning/courses/utility-scale-quantum-computing/hardware) for utility-scale quantum computing\n", + "It is recommended that you familiarize yourself with these topics:\n", "\n", + "- [Hardware fundamentals](/learning/courses/utility-scale-quantum-computing/hardware) for utility-scale quantum computing.\n", + "- The Clifford and stabilizer formalism, including how a stabilizer group describes a pure stabilizer state." + ] + }, + { + "cell_type": "markdown", + "id": "background", + "metadata": {}, + "source": [ "## Background\n", "\n", - "[Low-overhead error detection with spacetime codes](https://arxiv.org/abs/2504.15725) [[1]](#references) by Simon Martiel and Ali Javadi-Abhari proposes synthesizing low-weight, connectivity-aware spacetime checks for Clifford-dominated circuits, then postselecting on those checks to catch faults with far less overhead than full error correction and fewer shots than standard error mitigation.\n", + "[Low-overhead error detection with spacetime codes](https://arxiv.org/abs/2504.15725) [[1]](#references) by Simon Martiel and Ali Javadi-Abhari introduces a method for detecting logical errors in Clifford-dominated circuits that sits between full error correction and lighter-weight error mitigation. The idea builds on coherent Pauli checks (CPC) from [Single-shot error mitigation by coherent Pauli checks](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.5.033193) [[2]](#references) by van den Berg and others. In both approaches, a Clifford \"payload\" circuit is entangled with ancilla qubits to check certain invariants. Measuring the ancillas produces a syndrome that reports whether an error was detected during execution. Keeping only the samples with no detected error improves the fidelity of the sampled distribution, at the cost of a reduced postselection rate.\n", + "\n", + "The key difference between coherent Pauli checks and spacetime checks is the operators they measure. Coherent Pauli checks measure time-localized, high-weight operators. On qubit topologies with limited connectivity, such as heavy hex, those checks need many SWAP gates and often make the circuit too deep to run in practice. Implementing the checks as spacetime codes instead distributes each check across the payload circuit in space and time. This yields a hardware-efficient encoding that stays effective at detecting logical errors while keeping the qubit and depth overhead low.\n", + "\n", + "### What the qiskit-paulice package does\n", + "\n", + "The `qiskit-paulice` package automates the construction of these checks so that you do not have to build them by hand. Its primary role is to find and insert valid spacetime Pauli checks at the locations in a circuit that maximize error detection while minimizing qubit overhead. A check is *valid* when its operators leave the logical action of the payload circuit unchanged, *low-weight* when it uses few entangling gates, and *effective* when it detects a large portion of the errors, relative to the noise the check itself introduces. The package scores candidate checks against a noise model and commits the best ones to the circuit. This tutorial uses three API methods:\n", "\n", - "This paper proposes a novel method for error detection in quantum circuits (specifically Clifford circuits) that strikes a balance between full error correction and lighter-weight mitigation techniques. The key idea is to use spacetime codes to generate \"checks\" across the circuit that are capable of catching errors, with significantly lower qubit and gate overhead than full fault-tolerant error correction. The authors design efficient algorithms to select checks that are low-weight (involving few qubits), compatible with the physical connectivity of the device, and cover large temporal and spatial regions of the circuit. They demonstrate the approach on circuits with up to 50 logical qubits and ~2450 CZ gates, achieving physical-to-logical fidelity gains of up to 236x. Also note that as circuits include more non-Clifford operations, the number of valid checks shrinks exponentially, indicating the method works best for Clifford-dominated circuits. Overall, in the near term, error detection via spacetime codes may offer a practical, lower-overhead route to improving reliability in quantum hardware.\n", + "- `get_check_qubits` inspects a backend coupling map and returns target and ancilla qubit pairs. A check on `target_qubits[i]` uses `ancilla_qubits[i]`.\n", + "- `NoiseModel.from_backend` builds a rough noise model from backend benchmark data. The model scores candidate checks, so an exact, learned noise model is not required. For a learned Pauli-Lindblad model, see `NoiseModel.from_pauli_lindblad_maps`.\n", + "- `add_pauli_checks` finds and inserts checks into a circuit. It returns a sequence of `CheckedCircuit` objects with an increasing number of checks, and each object provides a `get_postselection_method` that maps a measured bitstring to a syndrome vector. The `cost` argument selects the function that scores a check (`gamma`, the sampling overhead of the postselected inverse noise channel, or `LER`, the logical error rate). The `method` argument selects the search strategy (`windowed`, `genetic`, or `windowed_genetic`). This tutorial uses `cost=\"gamma\"` and `method=\"windowed\"`, which together give deterministic, reproducible check selection.\n", "\n", - "This error detection technique relies on the notion of coherent Pauli checks and is based on the work [Single-shot error mitigation by coherent Pauli checks](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.5.033193) [[2]](#references) by van den Berg et al.\n", + "### Estimate fidelity from stabilizer sampling\n", "\n", - "More recently, the paper [Big cats: entanglement in 120 qubits and beyond](https://arxiv.org/abs/2510.09520) [[3]](#references) by Javadi-Abhari et al., reports the creation of a 120-qubit Greenberger-Horne-Zeilinger (GHZ) state, the largest multipartite entangled state achieved to date on a superconducting-qubit platform. Using a hardware-aware compiler, low-overhead error detection, and a \"temporary uncomputation\" technique to reduce noise, the researchers achieved a fidelity of 0.56 ± 0.03 with about 28% post-selection efficiency. The work demonstrates genuine entanglement across all 120 qubits, validating multiple fidelity-certification methods, and marks a major benchmark for scalable quantum hardware.\n", + "To measure how well the error detection works, you can estimate the fidelity of the stabilizer state $|\\psi\\rangle = U|0\\rangle^{\\otimes n}$ that the circuit ideally prepares against the noisy state $\\rho$ that the hardware actually outputs. The projector onto a pure stabilizer state $|\\psi\\rangle$ equals the uniform average over the $2^n$ elements of its stabilizer group $\\mathcal{S}$:\n", "\n", - "This tutorial builds on these ideas, guiding you through implementing the error detection algorithm first on a small-scale random Clifford circuit and then through the task of GHZ state preparation, to help you experiment with error detection on your own quantum circuits." + "$$|\\psi\\rangle\\langle\\psi| = \\frac{1}{2^n}\\sum_{G \\in \\mathcal{S}} G.$$\n", + "\n", + "Substituting this into the fidelity gives the fidelity of $\\rho$ as the average expectation value of every stabilizer $G \\in \\mathcal{S}$ with respect to $\\rho$:\n", + "\n", + "$$F = \\mathrm{Tr}(\\rho|\\psi\\rangle\\langle\\psi|) = \\frac{1}{2^n} \\sum_{G \\in \\mathcal{S}} \\mathrm{Tr}(\\rho G) = \\frac{1}{2^n} \\sum_{G \\in \\mathcal{S}} \\langle G \\rangle_\\rho.$$\n", + "\n", + "For larger problems, enumerating all $2^n$ stabilizers is infeasible, so you can estimate the fidelity from a random sample. Drawing $M$ stabilizers $G_1, \\ldots, G_M$ uniformly at random from $\\mathcal{S}$ gives an unbiased estimation:\n", + "\n", + "$$\\hat F_M = \\frac{1}{M} \\sum_{i=1}^M \\langle G_i \\rangle_\\rho.$$\n", + "\n", + "Because a Clifford circuit prepares a stabilizer state, you can estimate its fidelity directly from sampled expectation values of its stabilizers. This tutorial first walks through the workflow on a simulator with a small circuit, then runs the same workflow on hardware with a deeper circuit. As circuits include more non-Clifford operations, the number of valid checks shrinks quickly, so the method works best for Clifford-dominated circuits." ] }, { "cell_type": "markdown", - "id": "fce0de6c", + "id": "requirements", "metadata": {}, "source": [ "## Requirements\n", + "\n", "Before starting this tutorial, be sure you have the following installed:\n", "\n", "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", "- Qiskit Runtime v0.40 or later (`pip install qiskit-ibm-runtime`)\n", - "- Qiskit Aer v0.17.2 (`pip install qiskit-aer`)" + "- Qiskit Aer v0.17 or later (`pip install qiskit-aer`)\n", + "- Qiskit Paulice (`pip install qiskit-paulice`)\n", + "- tqdm (`pip install tqdm`)" ] }, { "cell_type": "markdown", - "id": "4286334d", + "id": "setup", "metadata": {}, "source": [ - "## Setup" + "## Setup\n", + "\n", + "Import the required libraries and define the helper functions that are not available as imports. The `random_clifford_circuit` function builds a brickwork random Clifford payload, `append_basis_rotation` rotates a circuit so that a stabilizer is measured in the computational basis, `expectation` computes a stabilizer expectation value from sampled counts, and `cum_mean_sem` tracks the running fidelity estimate." ] }, { "cell_type": "code", - "execution_count": null, - "id": "8fdec072", + "execution_count": 1, + "id": "setup-imports", "metadata": {}, "outputs": [], "source": [ "# Standard library imports\n", - "from collections import defaultdict, deque\n", - "from functools import partial\n", + "import random\n", + "import time\n", "\n", "# External libraries\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", + "from tqdm import tqdm\n", "\n", "# Qiskit\n", - "from qiskit import ClassicalRegister, QuantumCircuit\n", - "from qiskit.circuit import Delay\n", - "from qiskit.circuit.library import RZGate, XGate\n", - "from qiskit.converters import circuit_to_dag, dag_to_circuit\n", - "from qiskit.quantum_info import Pauli, random_clifford\n", - "from qiskit.transpiler import AnalysisPass, PassManager\n", - "from qiskit.transpiler.passes import (\n", - " ALAPScheduleAnalysis,\n", - " CollectAndCollapse,\n", - " PadDelay,\n", - " PadDynamicalDecoupling,\n", - " RemoveBarriers,\n", - ")\n", - "from qiskit.transpiler.passes.optimization.collect_and_collapse import (\n", - " collect_using_filter_function,\n", - " collapse_to_operation,\n", - ")\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.visualization import plot_gate_map, plot_histogram\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.quantum_info import Clifford, Pauli, PauliList\n", + "from qiskit.result import sampled_expectation_value\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit.visualization import plot_coupling_map\n", "\n", "# Qiskit Aer\n", "from qiskit_aer import AerSimulator\n", - "from qiskit_aer.noise import NoiseModel, ReadoutError, depolarizing_error\n", + "from qiskit_aer.noise import NoiseModel as AerNoiseModel\n", + "from qiskit_aer.noise import ReadoutError, depolarizing_error\n", "\n", "# Qiskit IBM Runtime\n", "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler" + "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", + "\n", + "# Qiskit Paulice\n", + "from qiskit_paulice import add_pauli_checks\n", + "from qiskit_paulice.layout import get_check_qubits\n", + "from qiskit_paulice.noise_models import NoiseModel" ] }, { - "cell_type": "markdown", - "id": "3888d8aa", + "cell_type": "code", + "execution_count": 2, + "id": "setup-helpers", "metadata": {}, + "outputs": [], "source": [ - "## Small-scale simulator example" + "def random_clifford_circuit(\n", + " num_qubits: int, depth: int, rng: np.random.Generator\n", + ") -> QuantumCircuit:\n", + " \"\"\"Brickwork random Clifford on `num_qubits`, with `depth` CZ layers.\"\"\"\n", + " qc = QuantumCircuit(num_qubits)\n", + " qc.h(range(num_qubits))\n", + " for d in range(depth):\n", + " for i in range(d % 2, num_qubits - 1, 2):\n", + " qc.cz(i, i + 1)\n", + " for q in range(num_qubits):\n", + " if rng.integers(0, 2):\n", + " qc.sx(q)\n", + " if rng.integers(0, 2):\n", + " qc.s(q)\n", + " if rng.integers(0, 2):\n", + " qc.sx(q)\n", + " return qc\n", + "\n", + "\n", + "def append_basis_rotation(\n", + " circuit: QuantumCircuit, pauli: Pauli\n", + ") -> QuantumCircuit:\n", + " \"\"\"Strip measurements, append basis rotations for `pauli`, and re-measure.\"\"\"\n", + " out = circuit.remove_final_measurements(inplace=False)\n", + " for q in range(pauli.num_qubits):\n", + " if pauli.x[q]:\n", + " if pauli.z[q]:\n", + " out.sdg(q)\n", + " out.h(q)\n", + " out.measure_all()\n", + " return out\n", + "\n", + "\n", + "def expectation(counts: dict, pauli: Pauli) -> float:\n", + " \"\"\"Expectation value of `pauli` from counts measured in the Z basis.\n", + "\n", + " Pads with identity on any qubits beyond the support of `pauli`, such as the\n", + " check ancillas that appear in the postselected counts.\n", + " \"\"\"\n", + " if not counts:\n", + " return float(\"nan\")\n", + " n = pauli.num_qubits\n", + " sign = -1 if int(pauli.phase) % 4 == 2 else 1\n", + " total = len(next(iter(counts)))\n", + " label = \"\".join(\n", + " \"Z\" if q < n and (pauli.x[q] or pauli.z[q]) else \"I\"\n", + " for q in range(total - 1, -1, -1)\n", + " )\n", + " return sign * sampled_expectation_value(counts, label)\n", + "\n", + "\n", + "def cum_mean_sem(values: np.ndarray):\n", + " \"\"\"Cumulative mean and standard error of the mean, ignoring NaNs.\"\"\"\n", + " valid = ~np.isnan(values)\n", + " total = np.cumsum(np.where(valid, values, 0.0))\n", + " total_sq = np.cumsum(np.where(valid, values**2, 0.0))\n", + " count = np.maximum(np.cumsum(valid).astype(float), 1)\n", + " mean = total / count\n", + " sem = np.sqrt(np.maximum(total_sq / count - mean**2, 0) / count)\n", + " return np.where(np.cumsum(valid) > 0, mean, np.nan), sem" ] }, { "cell_type": "markdown", - "id": "31bd3de5", + "id": "small-scale-header", "metadata": {}, "source": [ - "### Step 1: Map classical inputs to a quantum problem" + "## Small-scale simulator example\n", + "\n", + "This section walks through the full workflow on a noisy simulator. It uses backend benchmark data to choose a qubit layout and a noise model, finds checks automatically, and uses postselection on the sampled distribution to show the fidelity improvement." ] }, { "cell_type": "markdown", - "id": "704eb75f", + "id": "step1-header", "metadata": {}, "source": [ - "To demonstrate this method, we start by constructing a simple Clifford circuit. Our goal is to be able to detect when certain types of error occur in this circuit, so that we can discard erroneous measurement results. In error detection terminology, this is also known as our payload circuit." + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "The payload circuit is a shallow one-dimensional brickwork random Clifford circuit. Because the circuit is Clifford, it prepares a stabilizer state whose fidelity you can estimate directly from sampled stabilizer expectation values. Start with a shallow circuit so that the checks are easy to visualize in the next step." ] }, { "cell_type": "code", - "execution_count": null, - "id": "f8384a7d", + "execution_count": 3, + "id": "step1-code", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "circ = random_clifford(num_qubits=2, seed=11).to_circuit()\n", - "circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "cf14cbd6", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution" + "num_qubits = 12\n", + "depth = 4\n", + "seed = 1764\n", + "rng = np.random.default_rng(seed)\n", + "np.random.seed(seed)\n", + "\n", + "circuit = random_clifford_circuit(num_qubits, depth, rng)\n", + "circuit.measure_all()\n", + "circuit.draw(\"mpl\", fold=-1, scale=0.6)" ] }, { "cell_type": "markdown", - "id": "aa41f799", + "id": "step2-header", "metadata": {}, "source": [ - "Our goal is to insert a coherent Pauli check into this payload circuit. But before we do that, we separate this circuit into layers. This will be useful later when inserting Pauli gates in between." + "### Step 2: Optimize for quantum hardware execution\n", + "\n", + "Mapping the circuit to hardware sets the physical qubit layout, the noise model that scores candidate checks, and the checks themselves.\n", + "\n", + "First, choose a one-dimensional qubit layout on the Heron r3 QPU `ibm_boston`. The layout zig-zags diagonally through the coupling graph to maximize the number of available ancilla qubits. The `get_check_qubits` function returns target and ancilla pairs, where a check on `target_qubits[i]` uses `ancilla_qubits[i]`.\n", + "\n", + "In the coupling graph that follows, the green qubits are payload qubits and the orange qubits are the ancillas that implement the checks. Qubits with an adjacent ancilla are used as target qubits for checks." ] }, { "cell_type": "code", - "execution_count": 3, - "id": "04bb6ea7", + "execution_count": 4, + "id": "step2-layout", "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target qubits: [68, 69, 89, 91, 111, 113, 133]\n", + "Ancilla qubits: [67, 70, 88, 92, 110, 114, 132]\n" + ] + }, { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, + "execution_count": 4, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "# Separate circuit into layers\n", - "dag = circuit_to_dag(circ)\n", - "circ_layers = []\n", - "for layer in dag.layers():\n", - " layer_as_circuit = dag_to_circuit(layer[\"graph\"])\n", - " circ_layers.append(layer_as_circuit)\n", - "\n", - "# Create subplots\n", - "fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(1, 5, figsize=(10, 4))\n", - "\n", - "# Draw circuits on respective axes\n", - "circ_layers[0].draw(output=\"mpl\", ax=ax1)\n", - "circ_layers[1].draw(output=\"mpl\", ax=ax2)\n", - "circ_layers[2].draw(output=\"mpl\", ax=ax3)\n", - "circ_layers[3].draw(output=\"mpl\", ax=ax4)\n", - "circ_layers[4].draw(output=\"mpl\", ax=ax5)\n", - "\n", - "# Adjust layout to prevent overlap\n", - "plt.tight_layout()\n", - "plt.show()" + "service = QiskitRuntimeService()\n", + "backend = service.backend(\"ibm_marrakesh\")\n", + "\n", + "# Choose a layout and pair each available target qubit with a neighboring ancilla\n", + "layout = [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133]\n", + "target_qubits, ancilla_qubits = get_check_qubits(backend, layout)\n", + "num_checks = len(target_qubits)\n", + "\n", + "print(f\"Target qubits: {target_qubits}\")\n", + "print(f\"Ancilla qubits: {ancilla_qubits}\")\n", + "plot_coupling_map(\n", + " num_qubits=backend.num_qubits,\n", + " qubit_coordinates=getattr(\n", + " backend.configuration(), \"qubit_coordinates\", None\n", + " ),\n", + " coupling_map=backend.configuration().coupling_map,\n", + " figsize=(12, 12),\n", + " qubit_color=[\n", + " \"#4CAF50\"\n", + " if i in set(layout)\n", + " else \"#FF9800\"\n", + " if i in set(ancilla_qubits)\n", + " else \"#DDDDDD\"\n", + " for i in backend.coupling_map.graph.node_indices()\n", + " ],\n", + " qubit_size=220,\n", + " line_width=2,\n", + " font_size=90,\n", + ")" ] }, { "cell_type": "markdown", - "id": "dda1b961", - "metadata": {}, - "source": [ - "Now we are ready to add coherent Pauli checks into the payload circuit. To do this, we need to construct a \"valid check\" and insert it into the circuit. A \"check\" in this case is an operator that is capable of signaling whether an error occurred in the circuit by making a measurement on an ancilla qubit. It is considered a valid check when the additional operators inserted into the quantum circuit don't logically change the original circuit.\n", - "\n", - "This check is capable of detecting types of errors that anticommute with it, and the check will trigger a measurement of $\\ket{1}$ state in the ancilla qubit instead of $\\ket{0}$ through phase kickback. Therefore, we will be able to discard measurements where an error was signaled.\n", - "\n", - "In general, coherent Pauli checks are controlled-Pauli operators inserted into \"wires\" - spacetime locations between gates. The ancilla qubit responsible for signaling the error is the control qubit.\n", - "\n", - "Below we construct a valid check for the Clifford circuit we created above. We can demonstrate that this check doesn't change the circuit operation by showing that when these Pauli checks are propagated to the front of the circuit, they cancel each other out. This is easily shown because a Pauli operator through a Clifford gate is another Pauli operator.\n", - "\n", - "In general, one can use a decoding heuristic as outlined in [[1]](https://arxiv.org/abs/2504.15725) to identify valid checks. For the purposes of our initial example, we can also construct valid checks by using analytical Pauli and Clifford gate multiplication conditions." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "ca5727c1", + "id": "step2-transpile-md", "metadata": {}, - "outputs": [], "source": [ - "# Define a valid check\n", - "pauli_1 = Pauli(\"ZI\")\n", - "pauli_2 = Pauli(\"XZ\")" + "With the backend and layout chosen, transpile the payload into an instruction set architecture (ISA) circuit. Setting the layout and translating the gates into the native gate set of `ibm_boston` is enough here." ] }, { "cell_type": "code", "execution_count": 5, - "id": "e1c1e16d", + "id": "step2-transpile", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, "execution_count": 5, @@ -254,1275 +358,646 @@ } ], "source": [ - "circ_1 = circ_layers[0].compose(circ_layers[1])\n", - "circ_1.draw(\"mpl\")" + "pm = generate_preset_pass_manager(\n", + " optimization_level=0, backend=backend, initial_layout=layout\n", + ")\n", + "circuit_isa = pm.run(circuit)\n", + "circuit_isa.draw(\"mpl\", fold=-1, scale=0.6)" ] }, { - "cell_type": "code", - "execution_count": 6, - "id": "bebdd7df", + "cell_type": "markdown", + "id": "step2-noise-md", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Pauli('-ZI')" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "pauli_1_ev = pauli_1.evolve(circ_1, frame=\"h\")\n", - "pauli_1_ev" + "Next, model how the gate and readout noise on the backend affects execution. The noise model determines where in the circuit a check captures the most error. A more accurate model improves the detection, but it is usually not necessary to learn one by sampling the QPU. The model that follows infers a uniform depolarizing channel for gate and readout noise from `qiskit-ibm-runtime` benchmark data." ] }, { "cell_type": "code", - "execution_count": 7, - "id": "246a42b8", + "execution_count": 6, + "id": "step2-noise", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "NoiseModel(gate_noise=0.19074977552838765, readout_noise=0.1016845703125, idling_noise=None)\n" + ] } ], "source": [ - "circ_2 = circ.copy()\n", - "circ_2.draw(\"mpl\")" + "noise_model = NoiseModel.from_backend(\n", + " backend, layout, uniform_gate_noise=True\n", + ")\n", + "print(noise_model)" ] }, { - "cell_type": "code", - "execution_count": 8, - "id": "415c9471", + "cell_type": "markdown", + "id": "step2-checks-md", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Pauli('-ZI')" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "pauli_2_ev = pauli_2.evolve(circ_2, frame=\"h\")\n", - "pauli_2_ev" + "Now add checks to the circuit. The `add_pauli_checks` function takes the Clifford payload, the list of target qubits, and the noise model. The `ancilla_qubits` argument tells the function which physical ancilla to pair with each target. Checks are added in the order the target qubits appear, so the final layout of the checked circuit is `layout + ancilla_qubits`. To run an output circuit with fewer (`i`) checks, the final layout is `layout + ancilla_qubits[:i]`.\n", + "\n", + "The output of `add_pauli_checks` is a sequence of circuits with an increasing number of checks, from no checks up to one check on every target qubit. The visualization confirms that the checks use the specified target and ancilla pairs. For details on finding good checks, see Sections II to IV of the supplementary information in reference [[1]](#references)." ] }, { "cell_type": "code", - "execution_count": 9, - "id": "f7368e97", + "execution_count": 7, + "id": "step2-checks", "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Physical layout of payload and ancillas: [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133, 67, 70, 88, 92, 110, 114, 132]\n", + "Checked circuit:\n" + ] + }, { "data": { "text/plain": [ - "Pauli('II')" + "\"Output" ] }, - "execution_count": 9, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pauli_1_ev.dot(pauli_2_ev)" + "checked = add_pauli_checks(\n", + " circuit_isa,\n", + " target_qubits,\n", + " noise_model,\n", + " ancilla_qubits=ancilla_qubits,\n", + " cost=\"gamma\",\n", + " method=\"windowed\",\n", + " seed=seed,\n", + ")\n", + "\n", + "print(f\"Physical layout of payload and ancillas: {layout + ancilla_qubits}\")\n", + "print(\"Checked circuit:\")\n", + "checked[-1].circuit.draw(\"mpl\", fold=-1, idle_wires=False)" ] }, { "cell_type": "markdown", - "id": "dba1bc80", + "id": "step3-header", "metadata": {}, "source": [ - "As we can see, we have a valid check, since the inserted Pauli operators simply have the same effect as an identity operator on the circuit. We can now insert these checks into the circuit with an ancilla qubit. This ancilla qubit, or the check qubit, starts in the $\\ket{+}$ state. It includes the controlled versions of the Pauli operations outlined above and is finally measured in the $X$ basis. This check qubit is now capable of capturing errors in the payload circuit without logically altering it. This is because certain types of noise in the payload circuit will modify the state of the check qubit, and it will be measured \"1\" instead of \"0\" in case such an error occurs." + "### Step 3: Execute using Qiskit primitives\n", + "\n", + "To make the effect of gate noise visible, increase the depth of the payload and sample a subset of its stabilizers. Each stabilizer is generally not qubit-wise commuting with the others, so a single set of checks is not valid for two different stabilizers. Rather than group stabilizers into commuting sets, find a good set of checks for each stabilizer independently. Sampling stabilizers uniformly at random gives an unbiased fidelity estimate.\n", + "\n", + "Build the deeper circuit and draw a random sample of its stabilizers." ] }, { "cell_type": "code", - "execution_count": 10, - "id": "52add690", + "execution_count": 8, + "id": "step3-stabilizers", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {-XYXZIZXYIIXZ, -XZYYIIXZXYYY, ...}\n" + ] } ], "source": [ - "# New circuit with 3 qubits (2 payload + 1 ancilla for check)\n", - "circ_meas = QuantumCircuit(3)\n", - "circ_meas.h(0)\n", - "circ_meas.compose(circ_layers[0], [1, 2], inplace=True)\n", - "circ_meas.compose(circ_layers[1], [1, 2], inplace=True)\n", - "circ_meas.cz(0, 2)\n", - "circ_meas.compose(circ_layers[2], [1, 2], inplace=True)\n", - "circ_meas.compose(circ_layers[3], [1, 2], inplace=True)\n", - "circ_meas.compose(circ_layers[4], [1, 2], inplace=True)\n", - "circ_meas.cz(0, 1)\n", - "circ_meas.cx(0, 2)\n", - "circ_meas.h(0)\n", - "\n", - "# Add measurement to payload qubits\n", - "c0 = ClassicalRegister(2, name=\"c0\")\n", - "circ_meas.add_register(c0)\n", - "circ_meas.measure(1, c0[0])\n", - "circ_meas.measure(2, c0[1])\n", - "\n", - "# Add measurement to check qubit\n", - "c1 = ClassicalRegister(1, name=\"c1\")\n", - "circ_meas.add_register(c1)\n", - "circ_meas.measure(0, c1[0])\n", - "\n", - "# Visualize the final circuit with the inserted checks\n", - "circ_meas.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "8ccacbe5", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives" + "depth = 24\n", + "num_stabilizers = 20\n", + "num_shots = 1_000\n", + "\n", + "circuit = random_clifford_circuit(num_qubits, depth, rng)\n", + "\n", + "# Build the full stabilizer group, then sample from it uniformly at random\n", + "circ_no_meas = circuit.remove_final_measurements(inplace=False)\n", + "stabilizer_group = PauliList([Pauli(\"I\" * num_qubits)])\n", + "for generator in (\n", + " Pauli(label) for label in Clifford(circ_no_meas).to_labels(mode=\"S\")\n", + "):\n", + " stabilizer_group = stabilizer_group + stabilizer_group.compose(generator)\n", + "\n", + "keep = np.where(\n", + " stabilizer_group.x.any(axis=1) | stabilizer_group.z.any(axis=1)\n", + ")[0]\n", + "chosen = np.random.default_rng(seed).choice(\n", + " keep, size=min(num_stabilizers, len(keep)), replace=False\n", + ")\n", + "stabilizers = [stabilizer_group[int(i)] for i in chosen]\n", + "\n", + "two_qubit_depth = circuit.depth(lambda x: x.operation.num_qubits == 2)\n", + "print(\n", + " f\"Sampled {len(stabilizers)} stabilizers of a {circuit.num_qubits}-qubit \"\n", + " f\"circuit with two-qubit depth {two_qubit_depth}: \"\n", + " f\"{{{stabilizers[0]}, {stabilizers[1]}, ...}}\"\n", + ")" ] }, { "cell_type": "markdown", - "id": "ece387e6", + "id": "step3-findchecks-md", "metadata": {}, "source": [ - "If the check qubit is measured in \"0\", we keep that measurement. If it's measured in \"1\", then this means that an error occurred in the payload circuit, and we discard that measurement." + "For each sampled stabilizer, rotate the circuit so that the stabilizer is measured in the computational basis, transpile it onto the backend, and find a good set of checks. The target and ancilla pairs are shuffled together for each stabilizer so that each target keeps its ancilla. Remember that checks are committed sequentially in the order the target qubits are given, and a committed check is not changed as more checks are added." ] }, { "cell_type": "code", - "execution_count": 11, - "id": "78679785", + "execution_count": 9, + "id": "step3-findchecks", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 20/20 [00:14<00:00, 1.37it/s]" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "Stabilizer simulation result: {'0 11': 523, '0 01': 477}\n" + "Added 7 checks to 20 circuits in 15s.\n", + "On average, two-qubit depth increased from 24 to 33 when adding 7 checks.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" ] } ], "source": [ - "# Noiseless simulation using stabilizer method\n", - "sim_stab = AerSimulator(method=\"stabilizer\")\n", - "res = sim_stab.run(circ_meas, shots=1000).result()\n", - "counts_noiseless = res.get_counts()\n", - "print(f\"Stabilizer simulation result: {counts_noiseless}\")" + "noisy_circuits = []\n", + "checked_circuits = []\n", + "depths_2q = []\n", + "t0 = time.time()\n", + "for i, pauli in enumerate(tqdm(stabilizers)):\n", + " noisy_circuits.append(pm.run(append_basis_rotation(circuit, pauli)))\n", + " # Shuffle target and ancilla pairs together so each target keeps its ancilla\n", + " targets, ancillas = zip(\n", + " *random.sample(\n", + " list(zip(target_qubits, ancilla_qubits, strict=True)),\n", + " k=len(target_qubits),\n", + " ),\n", + " strict=True,\n", + " )\n", + " checked_circuits.append(\n", + " add_pauli_checks(\n", + " noisy_circuits[-1],\n", + " list(targets),\n", + " noise_model,\n", + " ancilla_qubits=list(ancillas),\n", + " cost=\"gamma\",\n", + " method=\"windowed\",\n", + " seed=seed + 1 + i,\n", + " )\n", + " )\n", + " depths_2q.append(\n", + " checked_circuits[-1][-1].circuit.depth(lambda x: len(x.qubits) == 2)\n", + " )\n", + "\n", + "print(\n", + " f\"Added {num_checks} checks to {len(stabilizers)} circuits \"\n", + " f\"in {(time.time() - t0):.0f}s.\"\n", + ")\n", + "print(\n", + " f\"On average, two-qubit depth increased from \"\n", + " f\"{circuit.depth(lambda x: len(x.qubits) == 2)} to {int(np.mean(depths_2q))} \"\n", + " f\"when adding {num_checks} checks.\"\n", + ")" ] }, { "cell_type": "markdown", - "id": "2bbbdbc4", + "id": "step3-sample-md", "metadata": {}, "source": [ - "Note that with an ideal simulator, the check qubit won't detect any errors as we will show in the subsequent post-processing section. We now introduce a noise model to the simulation and see how the check qubit captures errors." + "Sample the bare payload and the checked circuits with Qiskit Aer. The simulator uses the same depolarizing model that scored the checks, so the noise that the checks target is the noise that the simulator applies." ] }, { "cell_type": "code", - "execution_count": null, - "id": "407c2a8a", + "execution_count": 10, + "id": "step3-sample", "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "Noise model simulation result: {'1 01': 5, '0 11': 478, '1 11': 6, '1 00': 2, '1 10': 1, '0 01': 500, '0 00': 5, '0 10': 3}\n" + "100%|██████████| 20/20 [00:19<00:00, 1.00it/s]\n" ] } ], "source": [ - "# Qiskit Aer noise model\n", - "noise = NoiseModel()\n", - "p2 = 0.003 # two-qubit depolarizing per CZ\n", - "p1 = 0.001 # one-qubit depolarizing per 1q Clifford\n", - "pr = 0.01 # readout bit-flip probability\n", - "\n", - "# 1q depolarizing on common 1q gates\n", - "e1 = depolarizing_error(p1, 1)\n", - "for g1 in [\"id\", \"rz\", \"sx\", \"x\", \"h\", \"s\"]:\n", - " noise.add_all_qubit_quantum_error(e1, g1)\n", - "\n", - "# 2q depolarizing on CZ\n", - "e2 = depolarizing_error(p2, 2)\n", - "noise.add_all_qubit_quantum_error(e2, \"cz\")\n", - "\n", - "# Readout error on measure\n", - "ro = ReadoutError([[1 - pr, pr], [pr, 1 - pr]])\n", - "noise.add_all_qubit_readout_error(ro)\n", - "\n", - "# Qiskit Aer simulation with noise model\n", - "aer = AerSimulator(method=\"automatic\", seed_simulator=43210)\n", - "job = aer.run(circ_meas, shots=1000, noise_model=noise)\n", - "result = job.result()\n", - "counts_noisy = result.get_counts()\n", - "print(f\"Noise model simulation result: {counts_noisy}\")" + "aer_nm = AerNoiseModel()\n", + "aer_nm.add_all_qubit_quantum_error(\n", + " depolarizing_error(noise_model.gate_noise, 2), [\"cz\"]\n", + ")\n", + "p = noise_model.readout_noise\n", + "aer_nm.add_all_qubit_readout_error(ReadoutError([[1 - p, p], [p, 1 - p]]))\n", + "noisy_sim = AerSimulator(method=\"stabilizer\", noise_model=aer_nm)\n", + "\n", + "counts = []\n", + "for i, checked_circ_result in enumerate(tqdm(checked_circuits)):\n", + " noisy_counts = (\n", + " noisy_sim.run(\n", + " noisy_circuits[i], shots=num_shots, seed_simulator=seed * i + 1\n", + " )\n", + " .result()\n", + " .get_counts()\n", + " )\n", + " checked_counts_per_variant = []\n", + " for k, ck in enumerate(checked_circ_result):\n", + " variant_counts = (\n", + " noisy_sim.run(\n", + " ck.circuit, shots=num_shots, seed_simulator=seed * i + 2 + k\n", + " )\n", + " .result()\n", + " .get_counts()\n", + " )\n", + " checked_counts_per_variant.append(variant_counts)\n", + " counts.append((noisy_counts, checked_counts_per_variant))" ] }, { "cell_type": "markdown", - "id": "800154ac", + "id": "step4-header", "metadata": {}, "source": [ "### Step 4: Post-process and return result in desired classical format\n", - "We can now retrieve and analyze the results from the Sampler job. We start by plotting results from the noiseless simulation." + "\n", + "Each check uses entangling gates between one ancilla and one target. The ancilla starts in $|0\\rangle$, so $Z_\\text{anc}$ stabilizes its input. Propagating $Z_\\text{anc}$ forward through the checked circuit yields a Pauli operator on the output whose non-identity terms define the check's support. A check passes when the bits in its support have even parity. A sample is kept only when every check passes.\n", + "\n", + "The `get_postselection_method` of each `CheckedCircuit` returns a function that maps a measured bitstring to a syndrome vector. Keep the samples whose syndrome is zero for every check, and discard the rest. The chart that follows shows that adding more checks lowers the postselection rate. A lower postselection rate needs more shots to reach a target accuracy, so there is a tradeoff between detection capability and sampling cost. The rate appears to converge, which indicates that additional checks contribute less detection capability." ] }, { "cell_type": "code", - "execution_count": 12, - "id": "4741910b", + "execution_count": 11, + "id": "step4-psr", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, - "execution_count": 12, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "# Plot the noiseless results\n", - "# Note that the first bit in the key corresponds to the check qubit\n", - "plot_histogram(counts_noiseless)" + "rate_per_variant = []\n", + "kept_per_stab = []\n", + "for i, (_, checked_counts_per_variant) in enumerate(counts):\n", + " rates = []\n", + " kept_at_num_checks = None\n", + " for k, variant_counts in enumerate(checked_counts_per_variant):\n", + " ps_fn = checked_circuits[i][k].get_postselection_method()\n", + " kept = {\n", + " bs: n for bs, n in variant_counts.items() if not ps_fn(bs).any()\n", + " }\n", + " rates.append(sum(kept.values()) / num_shots)\n", + " if k == num_checks:\n", + " kept_at_num_checks = kept\n", + " rate_per_variant.append(rates)\n", + " kept_per_stab.append(kept_at_num_checks)\n", + "\n", + "max_len = max(len(s) for s in rate_per_variant)\n", + "rates_arr = np.full((len(rate_per_variant), max_len), np.nan)\n", + "for i, s in enumerate(rate_per_variant):\n", + " rates_arr[i, : len(s)] = s\n", + "ks = np.arange(max_len)\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.plot(ks, rates_arr.T, color=\"#ff8c00\", alpha=0.15, linewidth=1)\n", + "ax.plot(\n", + " ks,\n", + " np.nanmedian(rates_arr, axis=0),\n", + " color=\"black\",\n", + " linewidth=1,\n", + " linestyle=\"--\",\n", + " label=\"median\",\n", + ")\n", + "ax.set_xlabel(\"Checks committed\")\n", + "ax.set_ylabel(\"Postselection rate\")\n", + "ax.set_ylim((0, 1.05))\n", + "ax.set_title(\n", + " f\"Per-stabilizer postselection rate ({len(rates_arr)} stabilizers)\"\n", + ")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "55d7c308", + "id": "step4-fidelity-md", "metadata": {}, "source": [ - "As expected, the check qubit doesn't detect any errors. Next, we plot results from the noisy simulation." + "Now compare the fidelity of the bare noisy state with the postselected state. Postselecting only the samples with no detected error raises the expectation value of every stabilizer, and therefore the estimated fidelity. The postselected values use fewer samples than the raw values, yet the expectation values are more accurate and the sampled variance is lower." ] }, { "cell_type": "code", - "execution_count": null, - "id": "51a8f3d9", + "execution_count": 12, + "id": "step4-fidelity", "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ideal fidelity: 1.0\n", + "noisy fidelity: -0.0020\n", + "postselected fidelity: -0.0790\n", + "mean postselection rate: 0.007\n" + ] + }, { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, - "execution_count": 14, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ - "# Plot results with noise model\n", - "plot_histogram(counts_noisy)" - ] - }, - { - "cell_type": "markdown", - "id": "e7d863b1", - "metadata": {}, - "source": [ - "As we can see, some measurements captured the error by flagging the check qubit as \"1\", which are visible in the last four columns. These shots are discarded." + "results = []\n", + "for i, ((noisy_counts, _), kept) in enumerate(\n", + " zip(counts, kept_per_stab, strict=True)\n", + "):\n", + " results.append(\n", + " (\n", + " expectation(noisy_counts, stabilizers[i]),\n", + " expectation(kept, stabilizers[i]),\n", + " sum(kept.values()) / num_shots,\n", + " )\n", + " )\n", + "\n", + "fidelity_noisy = float(np.nanmean([r[0] for r in results]))\n", + "fidelity_postsel = float(np.nanmean([r[1] for r in results]))\n", + "psr = float(np.mean([r[2] for r in results]))\n", + "print(\n", + " f\"ideal fidelity: 1.0\\n\"\n", + " f\"noisy fidelity: {fidelity_noisy:.4f}\\n\"\n", + " f\"postselected fidelity: {fidelity_postsel:.4f}\\n\"\n", + " f\"mean postselection rate: {psr:.3f}\"\n", + ")\n", + "\n", + "evs_ideal = np.ones(len(results))\n", + "evs_noisy = np.array([r[0] for r in results])\n", + "evs_post = np.array([r[1] for r in results])\n", + "idx = np.arange(len(results))\n", + "\n", + "\n", + "def strip(ax, ys, color, label):\n", + " m, s = np.nanmean(ys), np.nanstd(ys)\n", + " ax.axhspan(\n", + " m - s, m + s, color=color, alpha=0.15, label=f\"{label} mean and std\"\n", + " )\n", + " ax.axhline(m, color=color, linewidth=1, linestyle=\"--\")\n", + "\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.axhline(np.nanmean(evs_ideal), color=\"black\", linewidth=1.5, label=\"ideal\")\n", + "strip(ax, evs_noisy, \"red\", \"noisy\")\n", + "strip(ax, evs_post, \"green\", \"postselected\")\n", + "ax.scatter(idx, evs_noisy, color=\"red\", s=22, alpha=0.7, label=\"noisy\")\n", + "ax.scatter(\n", + " idx,\n", + " evs_post,\n", + " color=\"green\",\n", + " s=22,\n", + " alpha=0.7,\n", + " label=\"postselected\",\n", + ")\n", + "ax.set_xlabel(\"stabilizer index\")\n", + "ax.set_ylabel(r\"$\\langle G \\rangle$\")\n", + "ax.set_ylim((-0.1, 1.1))\n", + "ax.set_title(\"Per-stabilizer expectation values\")\n", + "ax.legend(loc=\"lower left\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "M = np.arange(1, len(results) + 1)\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "for ys, color, label in [\n", + " (evs_ideal, \"black\", \"ideal\"),\n", + " (evs_noisy, \"red\", \"noisy\"),\n", + " (evs_post, \"green\", \"postselected\"),\n", + "]:\n", + " cm, sem = cum_mean_sem(ys)\n", + " ax.plot(M, cm, color=color, linewidth=1.5, label=label)\n", + " ax.fill_between(M, cm - sem, cm + sem, color=color, alpha=0.15)\n", + "ax.set_xlabel(\"number of stabilizers averaged\")\n", + "ax.set_ylabel(\"running fidelity estimate\")\n", + "ax.set_title(\"Fidelity convergence versus number of stabilizers\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "dd4baf08", + "id": "step4-gamma-md", "metadata": {}, "source": [ - "Note: The ancilla qubit can also introduce new errors to the circuit. To reduce the effect of this, we can insert nested checks with additional ancilla qubits to the quantum circuit." + "The gamma score reports how much of the modeled noise channel remains undetected by the checks. Plotting the gamma score against the number of committed checks shows how the detection capability improves as each check is added. A gamma curve that converges toward `1.0` indicates that additional checks detect little extra error, which matches the convergence seen in the postselection rate." ] }, { - "cell_type": "markdown", - "id": "7027e13d", + "cell_type": "code", + "execution_count": 13, + "id": "step4-gamma", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "## Large-scale hardware example" + "stab_scores = [\n", + " [variant.cost for variant in checked_circ_result]\n", + " for checked_circ_result in checked_circuits\n", + "]\n", + "max_len = max(len(s) for s in stab_scores)\n", + "scores = np.full((len(stab_scores), max_len), np.nan)\n", + "for i, s in enumerate(stab_scores):\n", + " scores[i, : len(s)] = s\n", + "ks = np.arange(max_len)\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.plot(ks, scores.T, color=\"#4682b4\", alpha=0.15, linewidth=1)\n", + "ax.plot(\n", + " ks,\n", + " np.nanmedian(scores, axis=0),\n", + " color=\"black\",\n", + " linewidth=1,\n", + " linestyle=\"--\",\n", + " label=\"median\",\n", + ")\n", + "ax.set_xlabel(\"Checks committed\")\n", + "ax.set_ylabel(\"Gamma\")\n", + "ax.set_yscale(\"log\")\n", + "ax.set_title(f\"Per-stabilizer gamma curves ({len(scores)} stabilizers)\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3, which=\"both\")\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "c2b7f050", + "id": "hardware-header", "metadata": {}, "source": [ - "### Step 1: Map classical inputs to a quantum problem" + "## Large-scale hardware example\n", + "\n", + "The same workflow runs on hardware with a deeper payload and more stabilizers. This section reuses the backend, layout, target and ancilla pairs, noise model, and pass manager from the simulator example, then submits the sampled circuits to the QPU in a single job. Because the circuit is deeper, gate noise has a larger effect, and the fidelity gain from postselection is more pronounced.\n", + "\n", + "The parameters that follow set the depth, the number of stabilizers, and the number of shots. Increase `hw_num_stabilizers` for a more precise fidelity estimate at the cost of more circuits per job, and adjust the layout to use more qubits if you want a larger payload." ] }, { "cell_type": "markdown", - "id": "96ee4f90", + "id": "hardware-steps-header", "metadata": {}, "source": [ - "Now we demonstrate a significant task for quantum computing algorithms, which is preparing a GHZ state. We will demonstrate how to do this on a real backend using error detection." + "### Steps 1-4 (compressed into a single code block)\n", + "\n", + "The first cell builds the deeper payload, samples its stabilizers, finds the fully checked circuit for each one, and submits one Sampler job that contains both the bare and the checked circuits. Each job carries the tag `TUT_ASPC` so that you can find it later. See [Add job tags](/docs/guides/add-job-tags) for more on tagging jobs." ] }, { "cell_type": "code", - "execution_count": null, - "id": "87317d7d", - "metadata": {}, - "outputs": [], - "source": [ - "# Set optional seed for reproducibility\n", - "SEED = 1\n", - "\n", - "if SEED:\n", - " np.random.seed(SEED)" - ] - }, - { - "cell_type": "markdown", - "id": "0eca54de", - "metadata": {}, - "source": [ - "The error detection algorithm for GHZ state preparation respects the hardware topology. We begin by selecting the desired hardware." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78cafb7c", - "metadata": {}, - "outputs": [], - "source": [ - "# This is used to run on real hardware\n", - "service = QiskitRuntimeService()\n", - "\n", - "# Choose a backend to build GHZ on\n", - "backend_name = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=133\n", - ")\n", - "\n", - "backend = service.backend(backend_name)\n", - "coupling_map = backend.target.build_coupling_map()" - ] - }, - { - "cell_type": "markdown", - "id": "12188865", - "metadata": {}, - "source": [ - "A GHZ state on $n$ qubits is defined as\n", - "$$\\lvert \\mathrm{GHZ}_n\\rangle \\;=\\; \\frac{1}{\\sqrt{2}}\\Big(\\lvert 0\\rangle^{\\otimes n} \\,+\\, \\lvert 1\\rangle^{\\otimes n}\\Big).$$\n", - "\n", - "A very naive approach to prepare the GHZ state would be to choose a root qubit with an initial Hadamard gate, which puts the qubit to an equal superposition state, and then to entangle this qubit with every other qubit. This is not a good approach, since it requires long-range and deep CNOT interactions. In this tutorial, we will use multiple techniques alongside error detection to reliably prepare the GHZ state on real hardware." - ] - }, - { - "cell_type": "markdown", - "id": "9dfcf756", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution" - ] - }, - { - "cell_type": "markdown", - "id": "cbb7cd7f", - "metadata": {}, - "source": [ - "#### Map the GHZ state to hardware" - ] - }, - { - "cell_type": "markdown", - "id": "181ce021", - "metadata": {}, - "source": [ - "First, we search for a root to map the GHZ circuit on hardware. We remove edges/nodes whose CZ errors, measurement errors, and $T_2$ values are worse than the thresholds below. These will not be included in the GHZ circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5033c96c", + "execution_count": 14, + "id": "hardware-submit", "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "17 bad edges: \n", - "[[30, 31], [112, 113], [113, 114], [113, 119], [120, 121], [130, 131], [145, 146], [146, 147], [111, 112], [55, 59], [64, 65], [131, 138], [131, 132], [119, 133], [129, 130], [47, 57], [29, 38]]\n", - "5 bad nodes: \n", - "[1, 113, 131, 146, 120]\n" + "100%|██████████| 50/50 [03:34<00:00, 4.29s/it]\n" ] - } - ], - "source": [ - "def bad_cz(target, threshold=0.01):\n", - " \"\"\"Return list of edges whose CZ error is worse than threshold.\"\"\"\n", - " undirected_edges = []\n", - " for edge in backend.target.build_coupling_map().get_edges():\n", - " if (edge[1], edge[0]) not in undirected_edges:\n", - " undirected_edges.append(edge)\n", - " edges = undirected_edges\n", - " cz_errors = {}\n", - " for edge in edges:\n", - " cz_errors[edge] = target[\"cz\"][edge].error\n", - " worst_edges = sorted(cz_errors.items(), key=lambda x: x[1], reverse=True)\n", - " return [list(edge) for edge, error in worst_edges if error > threshold]\n", - "\n", - "\n", - "def bad_readout(target, threshold=0.01):\n", - " \"\"\"Return list of nodes whose measurement error is worse than threshold.\"\"\"\n", - " meas_errors = {}\n", - " for node in range(backend.num_qubits):\n", - " meas_errors[node] = target[\"measure\"][(node,)].error\n", - " worst_nodes = sorted(\n", - " meas_errors.items(), key=lambda x: x[1], reverse=True\n", - " )\n", - " return [node for node, error in worst_nodes if error > threshold]\n", - "\n", - "\n", - "def bad_coherence(target, threshold=60):\n", - " \"\"\"Return list of nodes whose T2 value is lower than threshold.\"\"\"\n", - " t2s = {}\n", - " for node in range(backend.num_qubits):\n", - " t2 = target.qubit_properties[node].t2\n", - " t2s[node] = t2 * 1e6 if t2 else 0\n", - " worst_nodes = sorted(t2s.items(), key=lambda x: x[1])\n", - " return [node for node, val in worst_nodes if val < threshold]\n", - "\n", - "\n", - "THRESH_CZ = 0.025 # exclude from BFS those edges whose\n", - "# CZ error is worse than this threshold\n", - "THRESH_MEAS = 0.15 # exclude from BFS those nodes whose\n", - "# measurement error is worse than this threshold\n", - "THRESH_T2 = 10 # exclude from BFS those nodes whose\n", - "# T2 value is lower than this threshold\n", - "\n", - "bad_edges = bad_cz(backend.target, threshold=THRESH_CZ)\n", - "bad_nodes_readout = bad_readout(backend.target, threshold=THRESH_MEAS)\n", - "dead_qubits = bad_readout(backend.target, threshold=0.4)\n", - "bad_nodes_coherence = bad_coherence(backend.target, threshold=THRESH_T2)\n", - "bad_nodes = list(set(bad_nodes_readout) | set(bad_nodes_coherence))\n", - "print(f\"{len(bad_edges)} bad edges: \\n{bad_edges}\")\n", - "print(f\"{len(bad_nodes)} bad nodes: \\n{bad_nodes}\")" - ] - }, - { - "cell_type": "markdown", - "id": "637c4584", - "metadata": {}, - "source": [ - "Using the function below, we construct the GHZ circuit on the chosen hardware starting from the root and using breadth-first search (BFS)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5046c8ac", - "metadata": {}, - "outputs": [], - "source": [ - "def parallel_ghz(root, num_qubits, backend, bad_edges, skip):\n", - " \"\"\"\n", - " Build a GHZ state of size `num_qubits` on the given `backend`,\n", - " starting from `root`, expanding in BFS order.\n", - "\n", - " At each BFS layer, every active qubit adds at most one new neighbor\n", - " (so that two-qubit operations can run in parallel with no qubit conflicts).\n", - "\n", - " It grows the entanglement tree outward layer-by-layer.\n", - " \"\"\"\n", - "\n", - " # -------------------------------------------------------------\n", - " # (1) Filter usable connections from the backend coupling map\n", - " # -------------------------------------------------------------\n", - " # The coupling map lists all directed hardware connections as (control, target).\n", - " # We remove edges that are:\n", - " # - listed in `bad_edges` (or their reversed form)\n", - " # - involve a qubit in the `skip` list\n", - " cmap = [list(edge) for edge in backend.coupling_map.get_edges()]\n", - " edges = [\n", - " e\n", - " for e in cmap\n", - " if e not in bad_edges\n", - " and [e[1], e[0]] not in bad_edges\n", - " and e[0] not in skip\n", - " and e[1] not in skip\n", - " ]\n", - "\n", - " # -------------------------------------------------------------\n", - " # (2) Build an undirected adjacency list for traversal\n", - " # -------------------------------------------------------------\n", - " # Even though coupling_map edges are directed, BFS expansion just needs\n", - " # connectivity information (so we treat edges as undirected for search).\n", - " adj = defaultdict(list)\n", - " for u, v in edges:\n", - " adj[u].append(v)\n", - " adj[v].append(u)\n", - "\n", - " # -------------------------------------------------------------\n", - " # (3) Initialize the quantum circuit and BFS state\n", - " # -------------------------------------------------------------\n", - " n = backend.num_qubits\n", - " qc = QuantumCircuit(\n", - " n\n", - " ) # create a circuit with same number of qubits as hardware\n", - " visited = [\n", - " root\n", - " ] # record the order qubits are added to the GHZ chain/tree\n", - " queue = deque([root]) # BFS queue (start from root)\n", - " explored = defaultdict(\n", - " set\n", - " ) # to track which neighbors each node has already explored\n", - " layers = [] # list of per-layer (control, target) gate tuples\n", - " qc.h(root) # GHZ states start with a Hadamard on the root qubit\n", - "\n", - " # -------------------------------------------------------------\n", - " # (4) BFS expansion: build the GHZ tree one layer at a time\n", - " # -------------------------------------------------------------\n", - " # Loop until we've added the desired number of qubits to the GHZ\n", - " while queue and len(visited) < num_qubits:\n", - " layer = [] # collect new (control, target) pairs for this layer\n", - " current = list(\n", - " queue\n", - " ) # snapshot current frontier (so queue mutations don't affect iteration)\n", - " busy = (\n", - " set()\n", - " ) # track qubits already used in this layer (to avoid conflicts)\n", - "\n", - " for node in current:\n", - " queue.popleft()\n", - "\n", - " # find one unvisited neighbor of this node not already explored\n", - " unvisited_neighbors = [\n", - " nb\n", - " for nb in adj[node]\n", - " if nb not in visited and nb not in explored[node]\n", - " ]\n", - "\n", - " if unvisited_neighbors:\n", - " nb = unvisited_neighbors[\n", - " 0\n", - " ] # pick the first available neighbor\n", - " visited.append(nb) # mark it as part of the GHZ structure\n", - " queue.append(\n", - " node\n", - " ) # re-enqueue current node (can keep growing)\n", - " queue.append(nb) # enqueue the newly added qubit\n", - " explored[node].add(nb) # mark that edge as explored\n", - " layer.append(\n", - " (node, nb)\n", - " ) # schedule a CNOT between node and neighbor\n", - " busy.update([node, nb]) # reserve both qubits for this layer\n", - "\n", - " # stop early if we've reached the desired number of qubits\n", - " if len(visited) == num_qubits:\n", - " break\n", - " # else: node has no unused unvisited neighbors left → skip\n", - "\n", - " if layer:\n", - " # add all pairs (node, nb) scheduled this round to layers\n", - " layers.append(layer)\n", - " else:\n", - " # nothing new discovered this pass → done\n", - " break\n", - "\n", - " # -------------------------------------------------------------\n", - " # (5) Emit all layers into the quantum circuit\n", - " # -------------------------------------------------------------\n", - " # For each layer:\n", - " # - apply a CX gate for every (control, target) pair\n", - " # - insert a barrier so transpiler keeps layer structure\n", - " for layer in layers:\n", - " for q1, q2 in layer:\n", - " qc.cx(q1, q2)\n", - " qc.barrier()\n", - "\n", - " # -------------------------------------------------------------\n", - " # (6) Return outputs\n", - " # -------------------------------------------------------------\n", - " # qc: the built quantum circuit\n", - " # visited: order of qubits added\n", - " # layers: list of parallelizable two-qubit operations per step\n", - " return qc, visited, layers" - ] - }, - { - "cell_type": "markdown", - "id": "0167c0f2", - "metadata": {}, - "source": [ - "We now repeatedly search for the best root, where the GHZ circuit will originate from." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "90787353", - "metadata": {}, - "outputs": [], - "source": [ - "ROOT = None # root for BFS search\n", - "GHZ_SIZE = 100 # number of (data) qubits in the GHZ state\n", - "SKIP = [] # nodes to intentionally skip for a better chance of finding checks\n", - "\n", - "# Search for the best root (yielding the shallowest GHZ)\n", - "if ROOT is None:\n", - " best_root = -1\n", - " base_depth = 100\n", - " for root in range(backend.num_qubits):\n", - " qc, ghz_qubits, _ = parallel_ghz(\n", - " root, GHZ_SIZE, backend, bad_edges, SKIP\n", - " )\n", - " if len(ghz_qubits) != GHZ_SIZE:\n", - " continue\n", - " depth = qc.depth(lambda x: x.operation.num_qubits == 2)\n", - " if depth < base_depth:\n", - " best_root = root\n", - " base_depth = depth\n", - " ROOT = best_root" - ] - }, - { - "cell_type": "markdown", - "id": "9b8d8394", - "metadata": {}, - "source": [ - "We now construct the GHZ circuit starting from a specific node - as in, the best root - searching for the shortest depth using breadth-first search." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "a04e921b", - "metadata": {}, - "outputs": [ + }, { "name": "stdout", "output_type": "stream", "text": [ - "base depth: 17, base count: 99\n", - "ROOT: 50\n" + "Submitted job d91ge2vqq29s738nkm00 with 100 circuits\n" ] } ], "source": [ - "# Build a GHZ starting at the best root\n", - "qc, ghz_qubits, _ = parallel_ghz(\n", - " ROOT, GHZ_SIZE, backend, bad_edges, SKIP + bad_nodes\n", + "# -------------------------Step 1: build a deeper payload and sample its stabilizers-------------------------\n", + "hw_depth = 72\n", + "hw_num_stabilizers = 50\n", + "hw_num_shots = 2_000\n", + "\n", + "hw_circuit = random_clifford_circuit(num_qubits, hw_depth, rng)\n", + "hw_no_meas = hw_circuit.remove_final_measurements(inplace=False)\n", + "hw_group = PauliList([Pauli(\"I\" * num_qubits)])\n", + "for generator in (\n", + " Pauli(label) for label in Clifford(hw_no_meas).to_labels(mode=\"S\")\n", + "):\n", + " hw_group = hw_group + hw_group.compose(generator)\n", + "keep = np.where(hw_group.x.any(axis=1) | hw_group.z.any(axis=1))[0]\n", + "chosen = np.random.default_rng(seed).choice(\n", + " keep, size=min(hw_num_stabilizers, len(keep)), replace=False\n", ")\n", - "base_depth = qc.depth(lambda x: x.operation.num_qubits == 2)\n", - "base_count = qc.size(lambda x: x.operation.num_qubits == 2)\n", - "print(f\"base depth: {base_depth}, base count: {base_count}\")\n", - "print(f\"ROOT: {ROOT}\")\n", - "if len(ghz_qubits) != GHZ_SIZE:\n", - " raise Exception(\"No GHZ found. Relax error thresholds.\")" - ] - }, - { - "cell_type": "markdown", - "id": "21bca6bf", - "metadata": {}, - "source": [ - "We need one final consideration before inserting valid checks. This is related to the concept of \"coverage\", which is a measure of how many of the wires in a quantum circuit a check can cover. With a higher coverage, we can detect errors on a wider part of the circuit. With this measure, we can select among the valid checks with the highest circuit coverage. In other words, we will be using the `weighted_coverage` function to score different checks for the GHZ circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a31f2dfb", - "metadata": {}, - "outputs": [], - "source": [ - "def weighted_coverage(layers, parities, w_idle=0.2, w_gate=0.8):\n", - " \"\"\"\n", - " Compute weighted fraction (idle + gate) of wires that are\n", - " covered by at least one parity to all active wires.\n", - " \"\"\"\n", - " wires = active_wires(layers) # defined below\n", - " covered_by_any = {n_layer: set() for n_layer in range(len(layers))}\n", - " for parity in parities:\n", - " trace = z_trace_backward(layers, parity) # defined below\n", - " for n_layer, qs in trace.items():\n", - " covered_by_any[n_layer] |= qs\n", - " covered_weight = 0\n", - " total_weight = 0\n", - " for n_layer in range(len(layers)):\n", - " idle = wires[n_layer][\"idle\"]\n", - " gate = wires[n_layer][\"gate\"]\n", - " total_weight += w_idle * len(idle) + w_gate * len(gate)\n", - " covered_idle = covered_by_any[n_layer] & idle\n", - " covered_gate = covered_by_any[n_layer] & gate\n", - " covered_weight += w_idle * len(covered_idle) + w_gate * len(\n", - " covered_gate\n", - " )\n", - " return covered_weight / total_weight if total_weight > 0 else 0\n", - "\n", - "\n", - "def active_wires(layers):\n", - " \"\"\"\n", - " Returns per-layer dict with two sets:\n", - " - 'idle': activated wires that are idle in this layer\n", - " - 'gate': activated wires that are control/target of a CNOT at this layer\n", - " \"\"\"\n", - " first_activation = {}\n", - " for n_layer, layer in enumerate(layers):\n", - " for c, t in layer:\n", - " first_activation.setdefault(c, n_layer)\n", - " first_activation.setdefault(t, n_layer)\n", - " result = {}\n", - " for n_layer in range(len(layers)):\n", - " active = {\n", - " q\n", - " for q, n_layer0 in first_activation.items()\n", - " if n_layer >= n_layer0\n", - " }\n", - " gate = {q for c, t in layers[n_layer] for q in (c, t)}\n", - " idle = active - gate\n", - " result[n_layer] = {\"idle\": idle, \"gate\": gate}\n", - " return result\n", - "\n", - "\n", - "def z_trace_backward(layers, initial_Zs):\n", - " \"\"\"\n", - " Backward propagate Zs with parity cancellation.\n", - " Returns {layer: set of qubits with odd parity Z at that layer}.\n", - " \"\"\"\n", - " wires = active_wires(layers)\n", - " support = set(initial_Zs)\n", - " trace = {}\n", - " for n_layer in range(len(layers) - 1, -1, -1):\n", - " active = wires[n_layer][\"idle\"] | wires[n_layer][\"gate\"]\n", - " trace[n_layer] = support & active\n", - " # propagate backwards\n", - " new_support = set()\n", - " for q in support:\n", - " hit = False\n", - " for c, t in layers[n_layer]:\n", - " if q == t: # Z on target: copy to control\n", - " new_support ^= {t, c} # toggle both\n", - " hit = True\n", - " break\n", - " elif q == c: # Z on control: passes through\n", - " new_support ^= {c}\n", - " hit = True\n", - " break\n", - " if not hit: # unaffected\n", - " new_support ^= {q}\n", - " support = new_support\n", - " return trace" - ] - }, - { - "cell_type": "markdown", - "id": "631f4797", - "metadata": {}, - "source": [ - "We can now insert checks to the GHZ circuit. Finding valid checks is very convenient for the GHZ state, since any two-qubit Pauli $Z$ operator $Z_i Z_j$ acting on any two qubits $i,j$ of the GHZ circuit is a support and therefore a valid check.\n", - "\n", - "Also note that the checks in this case are controlled-$Z$ operators neighboring Hadamard gates from the left and right on the ancilla qubit. This is equivalent to a CNOT gate applied to the ancilla qubit. The code below inserts the checks into the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b7e798f7", - "metadata": {}, - "outputs": [], - "source": [ - "# --- Tunables controlling the search space / scoring ---\n", - "MAX_SKIPS = 10 # at most how many qubits to skip\n", - "# (in addition to the bad ones and the ones forced to skip above)\n", - "SHUFFLES = 200 # how many times to try removing nodes for checks\n", - "MAX_DEPTH_INCREASE = 10 # how far from the base GHZ depth to go\n", - "# to include checks (increase this for\n", - "# more checks at expense of depth)\n", - "\n", - "W_IDLE = 0.2 # weight of errors to consider during idle timesteps\n", - "W_GATE = 0.8 # weight of errors to consider during gates\n", - "\n", - "# Remove random nodes from the GHZ and build from the root\n", - "# again to increase checks\n", - "degree_two_nodes = [\n", - " i\n", - " for i in ghz_qubits\n", - " if all(n in ghz_qubits for n in coupling_map.neighbors(i))\n", - " and len(coupling_map.neighbors(i)) >= 2\n", - "]\n", - "\n", - "# --- Best-so-far tracking for the randomized search ---\n", - "num_checks = 0\n", - "best_covered_fraction = -1\n", - "best_qc = qc\n", - "best_checks = []\n", - "best_parities = []\n", - "best_layers = []\n", - "\n", - "# Outer loop: vary how many GHZ nodes we try skipping (0..MAX_SKIPS-1)\n", - "for num_skips in range(MAX_SKIPS):\n", - " # Inner loop: try SHUFFLES random choices of 'num_skips' nodes to skip\n", - " for _ in range(SHUFFLES):\n", - " # Construct the skip set:\n", - " # - pre-existing forced SKIP\n", - " # - plus a random sample of 'degree_two_nodes' of size 'num_skips'\n", - " skip = SKIP + list(np.random.choice(degree_two_nodes, num_skips))\n", - "\n", - " # Rebuild the GHZ using the current skip set and bad_nodes\n", - " qc, ghz_qubits, layers = parallel_ghz(\n", - " ROOT, GHZ_SIZE, backend, bad_edges, skip + bad_nodes\n", - " )\n", - "\n", - " # Measure circuit cost as 2-qubit-gate depth only\n", - " depth = qc.depth(lambda x: x.operation.num_qubits == 2)\n", - "\n", - " # If we failed to reach the target GHZ size, discard this attempt\n", - " if len(ghz_qubits) != GHZ_SIZE:\n", - " continue\n", - "\n", - " # --- Build \"checks\" around the GHZ we just constructed ---\n", - " # A check qubit is a non-GHZ, non-dead qubit that has ≥2\n", - " # neighbors inside the GHZ and all those incident\n", - " # edges are usable (i.e., not in bad_edges).\n", - " checks = []\n", - " parities = []\n", - " for i in range(backend.num_qubits):\n", - " neighbors = [\n", - " n for n in coupling_map.neighbors(i) if n in ghz_qubits\n", - " ]\n", - "\n", - " if (\n", - " i not in ghz_qubits\n", - " and i not in dead_qubits\n", - " and len(neighbors) >= 2\n", - " and not any(\n", - " [\n", - " [neighbor, i] in bad_edges\n", - " or [i, neighbor] in bad_edges\n", - " for neighbor in neighbors\n", - " ]\n", - " )\n", - " ):\n", - " # Record this qubit as a check qubit\n", - " checks.append(i)\n", - " parities.append((neighbors[0], neighbors[1]))\n", - " # Physically couple the check qubit 'i' to the two GHZ\n", - " # neighbors via CNOTs\n", - " # (This is the actual \"check\" attachment in the circuit.)\n", - " qc.cx(neighbors[0], i)\n", - " qc.cx(neighbors[1], i)\n", - "\n", - " # Score this design using the weighted coverage\n", - " # metric over the GHZ build layers\n", - " covered_fraction = weighted_coverage(\n", - " layers=layers, parities=parities, w_idle=W_IDLE, w_gate=W_GATE\n", - " )\n", + "hw_stabilizers = [hw_group[int(i)] for i in chosen]\n", + "\n", + "# -------------------------Step 2: transpile and add the fully checked circuit per stabilizer-------------------------\n", + "hw_noisy_circuits = []\n", + "hw_checked_circuits = []\n", + "for i, pauli in enumerate(tqdm(hw_stabilizers)):\n", + " bare = pm.run(append_basis_rotation(hw_circuit, pauli))\n", + " hw_noisy_circuits.append(bare)\n", + " variants = add_pauli_checks(\n", + " bare,\n", + " target_qubits,\n", + " noise_model,\n", + " ancilla_qubits=ancilla_qubits,\n", + " cost=\"gamma\",\n", + " method=\"windowed\",\n", + " seed=seed + 1 + i,\n", + " )\n", + " hw_checked_circuits.append(variants[-1]) # keep the fully checked circuit\n", "\n", - " # Keep it only if:\n", - " # - coverage improves over the best so far, AND\n", - " # - the 2q depth budget isn't blown by more than MAX_DEPTH_INCREASE\n", - " if (\n", - " covered_fraction > best_covered_fraction\n", - " and depth <= base_depth + MAX_DEPTH_INCREASE\n", - " ):\n", - " best_covered_fraction = covered_fraction\n", - " best_qc = qc\n", - " best_ghz_qubits = ghz_qubits\n", - " best_checks = checks\n", - " best_parities = parities\n", - " best_layers = layers" - ] - }, - { - "cell_type": "markdown", - "id": "8da8af69", - "metadata": {}, - "source": [ - "We can now print out the qubits used in the GHZ circuit and the check qubits." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "e11e2392", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "GHZ qubits: [50, 49, 51, 38, 52, 48, 58, 53, 47, 71, 39, 46, 70, 54, 33, 45, 72, 69, 55, 32, 37, 73, 68, 34, 31, 44, 25, 74, 78, 67, 18, 24, 79, 75, 89, 57, 11, 23, 93, 59, 88, 66, 10, 22, 92, 90, 87, 65, 12, 9, 21, 94, 91, 86, 77, 13, 8, 20, 95, 98, 97, 14, 7, 36, 99, 111, 107, 15, 6, 41, 115, 110, 106, 19, 17, 5, 40, 114, 109, 108, 105, 27, 4, 42, 118, 104, 28, 3, 129, 117, 103, 29, 2, 128, 125, 96, 30, 127, 124, 102] 100\n", - "Check qubits: [16, 26, 35, 43, 85, 126] 6\n", - "Covered fraction (no idle): 0.4595959595959596\n" - ] - } - ], - "source": [ - "# --- After search, report the best design found ---\n", - "qc = best_qc\n", - "checks = best_checks\n", - "parities = best_parities\n", - "layers = best_layers\n", - "ghz_qubits = best_ghz_qubits\n", - "if len(ghz_qubits) != GHZ_SIZE:\n", - " raise Exception(\"No GHZ found. Relax error thresholds.\")\n", - "\n", - "print(f\"GHZ qubits: {ghz_qubits} {len(ghz_qubits)}\")\n", - "print(f\"Check qubits: {checks} {len(checks)}\")\n", - "\n", - "covered_fraction = weighted_coverage(\n", - " layers=layers, parities=parities, w_idle=W_IDLE, w_gate=W_GATE\n", - ")\n", - "print(\n", - " \"Covered fraction (no idle): \",\n", - " weighted_coverage(\n", - " layers=layers, parities=parities, w_idle=0.0, w_gate=1.0\n", - " ),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4b0e4e74", - "metadata": {}, - "source": [ - "We can also print out some error statistics." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "040d4a25", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cz errors: \n", - " mean: 0.002, max: 0.012\n", - "meas errors: \n", - " mean: 0.014, max: 0.121\n", - "t1 errors: \n", - " mean: 267.9, min: 23.6\n", - "t2 errors: \n", - " mean: 155.9, min: 13.9\n" - ] - } - ], - "source": [ - "def circuit_errors(target, circ, error_type=\"cz\"):\n", - " \"\"\"\n", - " Pull per-resource error numbers from a Qiskit Target\n", - " for ONLY the qubits/edges actually used by `circ`.\n", - "\n", - " Args:\n", - " target: qiskit.transpiler.Target (e.g., backend.target)\n", - " circ: qiskit.QuantumCircuit\n", - " error_type: one of {\"cz\", \"meas\", \"t1\", \"t2\"}:\n", - " - \"cz\" -> 2q CZ gate error on the circuit's used edges\n", - " - \"meas\" -> measurement error on the circuit's used qubits\n", - " - \"t1\" -> T1 (converted to microseconds) on used qubits\n", - " - \"t2\" -> T2 (converted to microseconds) on used qubits\n", - "\n", - " Returns:\n", - " list[float] of the requested quantity for the active edges/qubits.\n", - " \"\"\"\n", + "# -------------------------Step 3: submit one Sampler job with the bare and checked circuits-------------------------\n", + "sampler = Sampler(mode=backend)\n", + "sampler.options.default_shots = hw_num_shots\n", + "sampler.options.environment.job_tags = [\"TUT_ASPC\"]\n", "\n", - " # Get all 2-qubit edges that appear in the circuit (as undirected pairs).\n", - " active_edges = active_gates(circ) # e.g., {(0,1), (2,3), ...}\n", - "\n", - " # Intersect those with the device coupling map (so we only query valid edges).\n", - " # Note: target.build_coupling_map().get_edges() yields directed pairs.\n", - " edges = [\n", - " edge\n", - " for edge in target.build_coupling_map().get_edges()\n", - " if tuple(sorted(edge)) in active_edges\n", - " ]\n", - "\n", - " # Deduplicate direction: keep only one orientation of each edge.\n", - " undirected_edges = []\n", - " for edge in edges:\n", - " if (edge[1], edge[0]) not in undirected_edges:\n", - " undirected_edges.append(edge)\n", - " edges = undirected_edges # (not used later—see note below)\n", - "\n", - " # Accumulators for different error/physics quantities\n", - " cz_errors, meas_errors, t1_errors, t2_errors = [], [], [], []\n", - "\n", - " # For every active (undirected) edge in the circuit, fetch its CZ error.\n", - " # NOTE: Uses active_gates(circ) again (undirected tuples). This assumes\n", - " # `target['cz']` accepts undirected indexing;\n", - " # many Targets store both directions.\n", - " for edge in active_gates(circ):\n", - " cz_errors.append(target[\"cz\"][edge].error)\n", - "\n", - " # For every active qubit, fetch measure error and T1/T2 (converted to µs).\n", - " for qubit in active_qubits(circ):\n", - " meas_errors.append(target[\"measure\"][(qubit,)].error)\n", - " t1_errors.append(\n", - " target.qubit_properties[qubit].t1 * 1e6\n", - " ) # seconds -> microseconds\n", - " t2_errors.append(\n", - " target.qubit_properties[qubit].t2 * 1e6\n", - " ) # seconds -> microseconds\n", - "\n", - " # Select which set to return.\n", - " if error_type == \"cz\":\n", - " return cz_errors\n", - " elif error_type == \"meas\":\n", - " return meas_errors\n", - " elif error_type == \"t1\":\n", - " return t1_errors\n", - " else:\n", - " return t2_errors\n", - "\n", - "\n", - "def active_qubits(circ):\n", - " \"\"\"\n", - " Return a list of qubit indices that participate in at least one\n", - " non-delay, non-barrier instruction in `circ`.\n", - " \"\"\"\n", - " active_qubits = set()\n", - " for inst in circ.data:\n", - " # Skip scheduling artifacts that don't act on state\n", - " if (\n", - " inst.operation.name != \"delay\"\n", - " and inst.operation.name != \"barrier\"\n", - " ):\n", - " for qubit in inst.qubits:\n", - " q = circ.find_bit(\n", - " qubit\n", - " ).index # map Qubit object -> integer index\n", - " active_qubits.add(q)\n", - " return list(active_qubits)\n", - "\n", - "\n", - "def active_gates(circ):\n", - " \"\"\"\n", - " Return a set of undirected 2-qubit edges (i, j) that appear in `circ`.\n", - " \"\"\"\n", - " used_2q_gates = set()\n", - " for inst in circ:\n", - " if inst.operation.num_qubits == 2:\n", - " qs = inst.qubits\n", - " # map Qubit objects -> indices, then sort to make the edge undirected\n", - " qs = sorted([circ.find_bit(q).index for q in qs])\n", - " used_2q_gates.add(tuple(sorted(qs)))\n", - " return used_2q_gates\n", - "\n", - "\n", - "# ---- Print summary statistics ----\n", - "cz_errors = circuit_errors(backend.target, qc, error_type=\"cz\")\n", - "meas_errors = circuit_errors(backend.target, qc, error_type=\"meas\")\n", - "t1_errors = circuit_errors(backend.target, qc, error_type=\"t1\")\n", - "t2_errors = circuit_errors(backend.target, qc, error_type=\"t2\")\n", - "\n", - "np.set_printoptions(linewidth=np.inf)\n", - "print(\n", - " f\"cz errors: \\n mean: {np.round(np.mean(cz_errors), 3)}, \"\n", - " f\"max: {np.round(np.max(cz_errors), 3)}\"\n", - ")\n", - "print(\n", - " f\"meas errors: \\n mean: {np.round(np.mean(meas_errors), 3)}, \"\n", - " f\"max: {np.round(np.max(meas_errors), 3)}\"\n", - ")\n", - "print(\n", - " f\"t1 errors: \\n mean: {np.round(np.mean(t1_errors), 1)}, \"\n", - " f\"min: {np.round(np.min(t1_errors), 1)}\"\n", - ")\n", - "print(\n", - " f\"t2 errors: \\n mean: {np.round(np.mean(t2_errors), 1)}, \"\n", - " f\"min: {np.round(np.min(t2_errors), 1)}\"\n", - ")" + "pubs = hw_noisy_circuits + [cc.circuit for cc in hw_checked_circuits]\n", + "job = sampler.run(pubs)\n", + "print(f\"Submitted job {job.job_id()} with {len(pubs)} circuits\")" ] }, { "cell_type": "markdown", - "id": "a8f0a54f", + "id": "hardware-retrieve-md", "metadata": {}, "source": [ - "As previously, we can simulate the circuit first in the absence of noise to ensure correctness of the GHZ state preparation circuit." + "After the job completes, retrieve the results and postselect the checked counts. Compute the noisy and postselected fidelities as the average stabilizer expectation value over the sampled stabilizers." ] }, { "cell_type": "code", - "execution_count": null, - "id": "b905e875", + "execution_count": 15, + "id": "hardware-retrieve", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Stabilizer simulation result:\n", - "{'000000 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111': 525, '000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000': 475}\n" + "noisy fidelity: 0.0032\n", + "postselected fidelity: 0.0268\n", + "mean postselection rate: 0.008\n" ] }, { "data": { "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# --- Simulate to ensure correctness ---\n", - "\n", - "qc_meas = qc.copy()\n", - "\n", - "# Add measurements to the GHZ qubits\n", - "c1 = ClassicalRegister(len(ghz_qubits), \"c1\")\n", - "qc_meas.add_register(c1)\n", - "for q, c in zip(ghz_qubits, c1):\n", - " qc_meas.measure(q, c)\n", - "\n", - "# Add measurements to the check qubits\n", - "if len(checks) > 0:\n", - " c2 = ClassicalRegister(len(checks), \"c2\")\n", - " qc_meas.add_register(c2)\n", - " for q, c in zip(checks, c2):\n", - " qc_meas.measure(q, c)\n", - "\n", - "# Simulate the circuit with stabilizer method\n", - "sim_stab = AerSimulator(method=\"stabilizer\")\n", - "res = sim_stab.run(qc_meas, shots=1000).result()\n", - "counts = res.get_counts()\n", - "print(\"Stabilizer simulation result:\")\n", - "print(counts)\n", - "\n", - "# Rename keys to \"0 0\" and \"0 1\" for easier plotting\n", - "# First len(checks) bits are check bits, rest are GHZ bits\n", - "keys = list(counts.keys())\n", - "for key in keys:\n", - " check_bits = key[: len(checks)]\n", - " ghz_bits = key[(len(checks) + 1) :]\n", - " if set(check_bits) == {\"0\"} and set(ghz_bits) == {\"0\"}:\n", - " counts[\"0 0\"] = counts.pop(key)\n", - " elif set(check_bits) == {\"0\"} and set(ghz_bits) == {\"1\"}:\n", - " counts[\"0 1\"] = counts.pop(key)\n", - " else:\n", - " continue\n", - "\n", - "plot_histogram(counts)" - ] - }, - { - "cell_type": "markdown", - "id": "a5db4e3b", - "metadata": {}, - "source": [ - "As expected, the check qubits are measured as all zeros, and we successfully prepared the GHZ state." - ] - }, - { - "cell_type": "markdown", - "id": "14770785", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives" - ] - }, - { - "cell_type": "markdown", - "id": "d7a6e8a8", - "metadata": {}, - "source": [ - "Now we are ready to run the circuit on real hardware and demonstrate how the error detection protocol can capture errors in GHZ state preparation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e2d37555", - "metadata": {}, - "outputs": [], - "source": [ - "SHOTS = 10000 # number of shots" - ] - }, - { - "cell_type": "markdown", - "id": "eef98bd0", - "metadata": {}, - "source": [ - "We define a helper function to add measurements to the GHZ circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5f99ba1f", - "metadata": {}, - "outputs": [], - "source": [ - "def add_measurements(qc, ghz_qubits, checks):\n", - " # --- Measure each set of qubits into different\n", - " # classical registers to facilitate post-processing ---\n", - "\n", - " # Add measurements to the GHZ qubits\n", - " c1 = ClassicalRegister(len(ghz_qubits), \"c1\")\n", - " qc.add_register(c1)\n", - " for q, c in zip(ghz_qubits, c1):\n", - " qc.measure(q, c)\n", - "\n", - " # Add measurements to the check qubits\n", - " c2 = ClassicalRegister(len(checks), \"c2\")\n", - " qc.add_register(c2)\n", - " for q, c in zip(checks, c2):\n", - " qc.measure(q, c)\n", - "\n", - " return qc" - ] - }, - { - "cell_type": "markdown", - "id": "19eae8b4", - "metadata": {}, - "source": [ - "Before execution, we draw the layout of the GHZ qubits and the check qubits on the selected hardware." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d0faf114", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" + "\"Output" ] }, "metadata": {}, @@ -1530,347 +1005,85 @@ } ], "source": [ - "# Plot the layout of GHZ and check qubits on the device\n", - "plot_gate_map(\n", - " backend,\n", - " label_qubits=True,\n", - " line_width=20,\n", - " line_color=[\n", - " \"black\"\n", - " if edge[0] in ghz_qubits + checks and edge[1] in ghz_qubits + checks\n", - " else \"lightgrey\"\n", - " for edge in backend.coupling_map.graph.edge_list()\n", - " ],\n", - " qubit_color=[\n", - " \"blue\"\n", - " if i in ghz_qubits\n", - " else \"salmon\"\n", - " if i in checks\n", - " else \"lightgrey\"\n", - " for i in range(0, backend.num_qubits)\n", - " ],\n", - ")\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "78c8c2b6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "681d4f29", - "metadata": {}, - "source": [ - "We now add the measurements." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "75169ff2", - "metadata": {}, - "outputs": [], - "source": [ - "qc = add_measurements(qc, ghz_qubits, checks)" - ] - }, - { - "cell_type": "markdown", - "id": "9bf787f5", - "metadata": {}, - "source": [ - "The scheduling pipeline below locks in timing, removes barriers, simplifies delays, and injects dynamical decoupling, all while preserving the original operation times." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "673d1a87", - "metadata": {}, - "outputs": [], - "source": [ - "# The scheduling consists of first inserting delays while barriers\n", - "# are still there, then removing the barriers and consolidating the\n", - "# delays, so that the operations do not move in time\n", - "# Lastly we replace delays with dynamical decoupling\n", - "collect_function = partial(\n", - " collect_using_filter_function,\n", - " filter_function=(lambda node: node.op.name == \"delay\"),\n", - " split_blocks=True,\n", - " min_block_size=2,\n", - " split_layers=False,\n", - " collect_from_back=False,\n", - " max_block_width=None,\n", - ")\n", - "\n", - "collapse_function = partial(\n", - " collapse_to_operation,\n", - " collapse_function=(\n", - " lambda circ: Delay(sum(inst.operation.duration for inst in circ))\n", - " ),\n", - ")\n", - "\n", - "\n", - "class Unschedule(AnalysisPass):\n", - " \"\"\"Removes a property from the passmanager property set so that the\n", - " circuit looks unscheduled, so we can schedule it again.\"\"\"\n", - "\n", - " def run(self, dag):\n", - " del self.property_set[\"node_start_time\"]\n", - "\n", - "\n", - "def build_passmanager(backend, dd_qubits=None):\n", - " pm = generate_preset_pass_manager(\n", - " target=backend.target,\n", - " layout_method=\"trivial\",\n", - " optimization_level=2,\n", - " routing_method=\"none\",\n", - " )\n", - "\n", - " pm.scheduling = PassManager(\n", - " [\n", - " ALAPScheduleAnalysis(target=backend.target),\n", - " PadDelay(target=backend.target),\n", - " RemoveBarriers(),\n", - " Unschedule(),\n", - " CollectAndCollapse(\n", - " collect_function=collect_function,\n", - " collapse_function=collapse_function,\n", - " ),\n", - " ALAPScheduleAnalysis(target=backend.target),\n", - " PadDynamicalDecoupling(\n", - " dd_sequence=[XGate(), RZGate(-np.pi), XGate(), RZGate(np.pi)],\n", - " spacing=[1 / 4, 1 / 2, 0, 0, 1 / 4],\n", - " target=backend.target,\n", - " qubits=dd_qubits,\n", - " ),\n", - " ]\n", + "# -------------------------Step 4: postselect and compare fidelity-------------------------\n", + "result = job.result()\n", + "n_stab = len(hw_stabilizers)\n", + "\n", + "hw_results = []\n", + "for i in range(n_stab):\n", + " noisy_counts = result[i].join_data().get_counts()\n", + " checked_counts = result[n_stab + i].join_data().get_counts()\n", + " ps_fn = hw_checked_circuits[i].get_postselection_method()\n", + " kept = {bs: c for bs, c in checked_counts.items() if not ps_fn(bs).any()}\n", + " hw_results.append(\n", + " (\n", + " expectation(noisy_counts, hw_stabilizers[i]),\n", + " expectation(kept, hw_stabilizers[i]),\n", + " sum(kept.values()) / sum(checked_counts.values()),\n", + " )\n", " )\n", "\n", - " return pm" - ] - }, - { - "cell_type": "markdown", - "id": "6b6c4078", - "metadata": {}, - "source": [ - "We can now use the custom pass manager to transpile the circuit for the selected backend." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b8c537b", - "metadata": {}, - "outputs": [], - "source": [ - "# Transpile the circuits for the backend\n", - "pm = build_passmanager(backend, ghz_qubits)\n", - "\n", - "# Instruction set architecture (ISA) level circuit after scheduling and\n", - "# DD insertion\n", - "isa_circuit = pm.run(qc)\n", - "\n", - "# Draw after scheduling and DD insertion\n", - "# timeline_drawer(isa_circuit, show_idle=False, time_range=(0, 1000),\n", - "# target=backend.target)" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "0c4b279b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "isa_circuit.draw(\"mpl\", fold=-1, idle_wires=False)" - ] - }, - { - "cell_type": "markdown", - "id": "4e0712a5", - "metadata": {}, - "source": [ - "We then submit the job using the Qiskit Runtime Sampler primitive." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "803e5790", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "d493f17nmdfs73abf9qg\n" - ] - } - ], - "source": [ - "# Select the Sampler options\n", - "sampler = Sampler(mode=backend)\n", - "sampler.options.default_shots = SHOTS\n", - "sampler.options.dynamical_decoupling.enable = False\n", - "sampler.options.execution.rep_delay = 0.00025\n", - "sampler.options.environment.job_tags = [\"TUT_EDSC\"]\n", - "\n", - "# Submit the job\n", - "print(\"Submitting Sampler job\")\n", - "ghz_job = sampler.run([isa_circuit])\n", - "\n", - "print(ghz_job.job_id())" - ] - }, - { - "cell_type": "markdown", - "id": "c39bfe38", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format\n", - "We can now retrieve and analyze the results from the Sampler job." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "23495738", - "metadata": {}, - "outputs": [], - "source": [ - "# Retrieve the job results\n", - "job_result = ghz_job.result()" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "55e07409", - "metadata": {}, - "outputs": [], - "source": [ - "# Get the counts from GHZ and check qubit measurements\n", - "ghz_counts = job_result[0].data.c1.get_counts()\n", - "checks_counts = job_result[0].data.c2.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "d7162ed7", - "metadata": {}, - "outputs": [], - "source": [ - "# Post-process to get unflagged GHZ counts (i.e., check bits are all '0')\n", - "joined_counts = job_result[0].join_data().get_counts()\n", - "unflagged_counts = {}\n", - "for key, count in joined_counts.items():\n", - " check_bits = key[: len(checks)]\n", - " ghz_bits = key[len(checks) :]\n", - " if set(check_bits) == {\"0\"}:\n", - " unflagged_counts[ghz_bits] = count" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "d7902975", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get top 20 outcomes by frequency from the unflagged counts\n", - "top_counts = dict(\n", - " sorted(unflagged_counts.items(), key=lambda x: x[1], reverse=True)[:20]\n", + "hw_fidelity_noisy = float(np.nanmean([r[0] for r in hw_results]))\n", + "hw_fidelity_postsel = float(np.nanmean([r[1] for r in hw_results]))\n", + "hw_psr = float(np.mean([r[2] for r in hw_results]))\n", + "print(\n", + " f\"noisy fidelity: {hw_fidelity_noisy:.4f}\\n\"\n", + " f\"postselected fidelity: {hw_fidelity_postsel:.4f}\\n\"\n", + " f\"mean postselection rate: {hw_psr:.3f}\"\n", ")\n", "\n", - "# Rename keys for better visualization\n", - "top_counts_renamed = {}\n", - "i = 0\n", - "for key, count in top_counts.items():\n", - " if set(key) == {\"0\"}:\n", - " top_counts_renamed[\"all 0s\"] = count\n", - " elif set(key) == {\"1\"}:\n", - " top_counts_renamed[\"all 1s\"] = count\n", - " else:\n", - " top_counts_renamed[f\"other_{i}\"] = count\n", - " i += 1\n", - "\n", - "plot_histogram(top_counts_renamed, figsize=(12, 7))" - ] - }, - { - "cell_type": "markdown", - "id": "90505f1c", - "metadata": {}, - "source": [ - "In the histogram above, we plotted 20 bitstring measurements from the GHZ qubits that were not flagged by the check qubits. As expected, all-0 and all-1 bitstrings have the highest counts. Note that some erroneous bitstrings with low error weights were not captured by the error detection. The highest counts are still found in the expected bitstrings." + "hw_noisy = np.array([r[0] for r in hw_results])\n", + "hw_post = np.array([r[1] for r in hw_results])\n", + "idx = np.arange(n_stab)\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.axhline(1.0, color=\"black\", linewidth=1.5, label=\"ideal\")\n", + "strip(ax, hw_noisy, \"red\", \"noisy\")\n", + "strip(ax, hw_post, \"green\", \"postselected\")\n", + "ax.scatter(idx, hw_noisy, color=\"red\", s=22, alpha=0.7, label=\"noisy\")\n", + "ax.scatter(\n", + " idx,\n", + " hw_post,\n", + " color=\"green\",\n", + " s=22,\n", + " alpha=0.7,\n", + " label=\"postselected\",\n", + ")\n", + "ax.set_xlabel(\"stabilizer index\")\n", + "ax.set_ylabel(r\"$\\langle G \\rangle$\")\n", + "ax.set_ylim((-0.1, 1.1))\n", + "ax.set_title(\"Per-stabilizer expectation values on hardware\")\n", + "ax.legend(loc=\"lower left\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "5423c559", + "id": "next-steps", "metadata": {}, "source": [ "## Next steps\n", "\n", "\n", "If you found this work interesting, you might be interested in the following material:\n", - "- To further explore technical details of GHZ state preparation, see [[3]](https://arxiv.org/abs/2510.09520). In addition to error detection, authors use readout error mitigation with M3 and TREX and perform temporary uncomputation techniques to prepare high-fidelity GHZ states.\n", - "- Tutorial on [repetition codes](/docs/tutorials/repetition-codes).\n", + "\n", + "- The tutorial on [repetition codes](/docs/tutorials/repetition-codes) for an introduction to quantum error correction.\n", + "- The [`qiskit-paulice` package](https://github.com/Qiskit/qiskit-paulice) for the full check-finding API.\n", + "- The paper [Low-overhead error detection with spacetime codes](https://arxiv.org/abs/2504.15725) for the theory behind the checks.\n", "" ] }, { "cell_type": "markdown", - "id": "7bbdd161", + "id": "references", "metadata": {}, "source": [ "## References\n", - "- [1] Martiel, S., & Javadi-Abhari, A. (2025). Low-overhead error detection with spacetime codes. *arXiv preprint arXiv:2504.15725.*\n", - "- [2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., & Temme, K. (2023). Single-shot error mitigation by coherent Pauli checks. *Physical Review Research*, 5(3), 033193.\n", - "- [3] Javadi-Abhari, A., Martiel, S., Seif, A., Takita, M., & Wei, K. X. (2025). Big cats: entanglement in 120 qubits and beyond. *arXiv preprint arXiv:2510.09520*." + "\n", + "- [1] Martiel, S., & Javadi-Abhari, A. (2025). Low-overhead error detection with spacetime codes. *arXiv preprint* [arXiv:2504.15725](https://arxiv.org/abs/2504.15725).\n", + "- [2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., & Temme, K. (2023). Single-shot error mitigation by coherent Pauli checks. *Physical Review Research*, 5(3), 033193." ] } ], diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/04bb6ea7-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/04bb6ea7-0.avif deleted file mode 100644 index 635d3f8335b..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/04bb6ea7-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/0c4b279b-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/0c4b279b-0.avif deleted file mode 100644 index 50227cd1e0c..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/0c4b279b-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/246a42b8-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/246a42b8-0.avif deleted file mode 100644 index 6394b21f75a..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/246a42b8-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/4741910b-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/4741910b-0.avif deleted file mode 100644 index f11a4a1360b..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/4741910b-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/51a8f3d9-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/51a8f3d9-0.avif deleted file mode 100644 index a75a4e65e41..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/51a8f3d9-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/52add690-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/52add690-0.avif deleted file mode 100644 index 55cde84b4a7..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/52add690-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/78c8c2b6-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/78c8c2b6-0.avif deleted file mode 100644 index aace76fd2e4..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/78c8c2b6-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/b905e875-1.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/b905e875-1.avif deleted file mode 100644 index 044569e4b7d..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/b905e875-1.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/d0faf114-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/d0faf114-0.avif deleted file mode 100644 index 1138f8c0737..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/d0faf114-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/d7902975-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/d7902975-0.avif deleted file mode 100644 index aa7fe689d00..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/d7902975-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/e1c1e16d-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/e1c1e16d-0.avif deleted file mode 100644 index 2005515ad57..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/e1c1e16d-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/error-detection/extracted-outputs/f8384a7d-0.avif b/public/docs/images/tutorials/error-detection/extracted-outputs/f8384a7d-0.avif deleted file mode 100644 index 6394b21f75a..00000000000 Binary files a/public/docs/images/tutorials/error-detection/extracted-outputs/f8384a7d-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/hardware-retrieve-1.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/hardware-retrieve-1.avif new file mode 100644 index 00000000000..196275076fb Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/hardware-retrieve-1.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step1-code-0.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step1-code-0.avif new file mode 100644 index 00000000000..ba80214bde6 Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step1-code-0.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-checks-1.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-checks-1.avif new file mode 100644 index 00000000000..260a5954109 Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-checks-1.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-layout-1.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-layout-1.avif new file mode 100644 index 00000000000..210f0c22775 Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-layout-1.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-transpile-0.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-transpile-0.avif new file mode 100644 index 00000000000..38aa4c1584d Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step2-transpile-0.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-fidelity-1.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-fidelity-1.avif new file mode 100644 index 00000000000..bdd272b0925 Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-fidelity-1.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-fidelity-2.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-fidelity-2.avif new file mode 100644 index 00000000000..d8c18de3d24 Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-fidelity-2.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-gamma-0.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-gamma-0.avif new file mode 100644 index 00000000000..9bf8c1a1de5 Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-gamma-0.avif differ diff --git a/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-psr-0.avif b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-psr-0.avif new file mode 100644 index 00000000000..dae72a8d41e Binary files /dev/null and b/public/docs/images/tutorials/ghz-spacetime-codes/extracted-outputs/step4-psr-0.avif differ diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 01639ed9f01..660c2a1f9ba 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -675,6 +675,7 @@ notifications: "docs/tutorials/ghz-spacetime-codes": - "`@nathanearnestnoble`" - "`@MeltemTolunay`" + - "`@henryzou50`" "docs/tutorials/spin-chain-vqe": - "`@nathanearnestnoble`" - "`@mrvee-qC-bee`"