diff --git a/docs/tutorials/_toc.json b/docs/tutorials/_toc.json index 9dfda2865bb..6d95a5b3e08 100644 --- a/docs/tutorials/_toc.json +++ b/docs/tutorials/_toc.json @@ -93,6 +93,11 @@ { "title": "Workload optimization", "children": [ + { + "title": "Benchmark dynamic circuits with cut Bell pairs", + "url": "/docs/tutorials/edc-cut-bell-pair-benchmarking", + "platform": "cloud" + }, { "title": "Introduction to fractional gates", "url": "/docs/tutorials/fractional-gates" diff --git a/docs/tutorials/edc-cut-bell-pair-benchmarking.ipynb b/docs/tutorials/edc-cut-bell-pair-benchmarking.ipynb new file mode 100644 index 00000000000..dc5629c9786 --- /dev/null +++ b/docs/tutorials/edc-cut-bell-pair-benchmarking.ipynb @@ -0,0 +1,709 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9857bace", + "metadata": {}, + "source": [ + "{/* cspell:ignore LOCC */}\n", + "{/* cspell:ignore bitstr */}\n", + "{/* cspell:ignore textcoords */}\n", + "{/* cspell:ignore xytext */}\n", + "{/* cspell:ignore fontsize */}\n", + "{/* cspell:ignore Carrera */}\n", + "{/* cspell:ignore Ristè */}\n", + "# Benchmark dynamic circuits with cut Bell pairs" + ] + }, + { + "cell_type": "markdown", + "id": "d08a5e51", + "metadata": {}, + "source": [ + "*Estimated QPU usage: 22 seconds (tested on IBM Kingston)*" + ] + }, + { + "cell_type": "markdown", + "id": "77dca78b", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "Quantum hardware is typically limited to local interactions, but many algorithms require entangling distant qubits or even [qubits on separate processors](#references). Dynamic circuits, that is, circuits with mid-circuit measurement and feedforward, provide a way to overcome these limitations by using real-time classical communication to effectively implement non-local quantum operations. In this approach, measurement outcomes from one part of a circuit (or one QPU) can conditionally trigger gates on another, allowing us to teleport entanglement across long distances. This forms the basis of **local operations and classical communication (LOCC)** schemes, where we consume entangled resource states (Bell pairs) and communicate measurement results classically to link distant qubits.\n", + "\n", + "One promising use of LOCC is to realize virtual long-range CNOT gates by teleportation, as shown in the [long-range entanglement tutorial](/docs/tutorials/long-range-entanglement). Instead of a direct long-range CNOT (which hardware connectivity might not permit), we create Bell pairs and perform a teleportation-based gate implementation. However, the fidelity of such operations depends on hardware characteristics. Qubit decoherence during the necessary delay (while waiting for measurement results) and classical communication latency can degrade the entangled state. Also, errors on mid-circuit measurements are harder to correct than errors on final measurements as they propagate to the rest of the circuit through the conditional gates.\n", + "\n", + "In the [reference experiment](#references), the authors introduce a Bell pair fidelity benchmark to identify which parts of a device are best suited for LOCC-based entanglement. The idea is to run a small dynamic circuit on every group of four connected qubits in the processor. This four-qubit circuit first creates a Bell pair on two middle qubits, then uses those as a resource to entangle the two edge qubits by using LOCC. Concretely, qubits 1 and 2 are prepared into an uncut Bell pair locally (using a Hadamard and CNOT), and then a teleportation routine consumes that Bell pair to entangle qubits 0 and 3. Qubits 1 and 2 are measured during execution of the circuit, and based on those outcomes, Pauli corrections (an X on qubit 3 and Z on qubit 0) are applied. Qubits 0 and 3 are then left in a Bell state at the end of the circuit.\n", + "\n", + "To quantify the quality of this final entangled pair, we measure its stabilizers: specifically, the parity in the $Z$ basis ($Z_0Z_3$) and in the $X$ basis ($X_0X_3$). For a perfect Bell pair, both of these expectations equal +1. In practice, hardware noise will reduce these values. We therefore repeat the circuit twice for each qubit-pair: one circuit measures qubits 0 and 3 in the $Z$ basis, and another measures them in the $X$ basis. From the results, we obtain an estimate of $\\langle Z_0Z_3\\rangle$ and $\\langle X_0X_3\\rangle$ for that pair of qubits. We use the mean squared error (MSE) of these stabilizers with respect to the ideal value (1) as a simple metric of entanglement fidelity. A lower MSE means the two qubits achieved a Bell state closer to ideal (higher fidelity), whereas a higher MSE indicates more error. By scanning this experiment across the device, we can benchmark the measurement-and-feedforward capability of different qubit groups and identify the best pairs of qubits for LOCC operations.\n", + "\n", + "This tutorial demonstrates the experiment on an IBM Quantum® device to illustrate how dynamic circuits can be used to generate and evaluate entanglement between distant qubits. We will map out all 4-qubit linear chains on the device, run the teleportation circuit on each, and then visualize the distribution of MSE values. This end-to-end procedure shows how to leverage Qiskit Runtime and dynamic circuit features to inform hardware-aware choices for cutting circuits or distributing quantum algorithms across a modular system." + ] + }, + { + "cell_type": "markdown", + "id": "152c479f", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, ensure that you have the following installed:\n", + "\n", + "* Qiskit SDK 2.0 or later, with visualization support (`pip install 'qiskit[visualization]'`)\n", + "* Qiskit Runtime 0.40 or later (`pip install qiskit-ibm-runtime`)" + ] + }, + { + "cell_type": "markdown", + "id": "e67f9466", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b59e4534", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit import IfElseOp\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "def create_bell_stab(initial_layouts):\n", + " \"\"\"\n", + " Create a circuit for a 1D chain of qubits (number of qubits must be a multiple of 4),\n", + " where a middle Bell pair is consumed to create a Bell at the edge.\n", + " Takes as input a list of lists, where each element of the list is a\n", + " 1D chain of physical qubits that is used as the initial_layout for the transpiled circuit.\n", + " Returns a list of length-2 tuples, each tuple contains a circuit to measure the ZZ stabilizer and\n", + " a circuit to measure the XX stabilizer of the edge Bell state.\n", + " \"\"\"\n", + " bell_circuits = []\n", + " for (\n", + " initial_layout\n", + " ) in initial_layouts: # Iterate over chains of physical qubits\n", + " assert (\n", + " len(initial_layout) % 4 == 0\n", + " ), f\"The length of the chain must be a multiple of 4, len(inital_layout)={len(initial_layout)}\"\n", + " num_pairs = len(initial_layout) // 4\n", + "\n", + " bell_parallel = QuantumCircuit(4 * num_pairs, 4 * num_pairs)\n", + "\n", + " for pair_idx in range(num_pairs):\n", + " (q0, q1, q2, q3) = (\n", + " pair_idx * 4,\n", + " pair_idx * 4 + 1,\n", + " pair_idx * 4 + 2,\n", + " pair_idx * 4 + 3,\n", + " )\n", + " (c0, c1) = pair_idx * 4, pair_idx * 4 + 3 # edge qubits\n", + " (ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2 # middle qubits\n", + "\n", + " bell_parallel.h(q0)\n", + " bell_parallel.h(q1)\n", + " bell_parallel.cx(q1, q2)\n", + " bell_parallel.cx(q0, q1)\n", + " bell_parallel.cx(q2, q3)\n", + " bell_parallel.h(q2)\n", + "\n", + " # add barrier BEFORE measurements and add id in conditional\n", + " bell_parallel.barrier()\n", + " for pair_idx in range(num_pairs):\n", + " (q0, q1, q2, q3) = (\n", + " pair_idx * 4,\n", + " pair_idx * 4 + 1,\n", + " pair_idx * 4 + 2,\n", + " pair_idx * 4 + 3,\n", + " )\n", + " (ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2 # middle qubits\n", + "\n", + " bell_parallel.measure(q1, ca0)\n", + " bell_parallel.measure(q2, ca1)\n", + " # bell_parallel.barrier() #remove barrier after measurement\n", + "\n", + " for pair_idx in range(num_pairs):\n", + " (q0, q1, q2, q3) = (\n", + " pair_idx * 4,\n", + " pair_idx * 4 + 1,\n", + " pair_idx * 4 + 2,\n", + " pair_idx * 4 + 3,\n", + " )\n", + " (ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2 # middle qubits\n", + " with bell_parallel.if_test((ca0, 1)):\n", + " bell_parallel.x(q3)\n", + " with bell_parallel.if_test((ca1, 1)):\n", + " bell_parallel.z(q0)\n", + " bell_parallel.id(q0) # add id here for correct alignment\n", + "\n", + " bell_zz = bell_parallel.copy()\n", + " bell_zz.barrier()\n", + " bell_xx = bell_parallel.copy()\n", + " bell_xx.barrier()\n", + " for pair_idx in range(num_pairs):\n", + " (q0, q1, q2, q3) = (\n", + " pair_idx * 4,\n", + " pair_idx * 4 + 1,\n", + " pair_idx * 4 + 2,\n", + " pair_idx * 4 + 3,\n", + " )\n", + " bell_xx.h(q0)\n", + " bell_xx.h(q3)\n", + " bell_xx.barrier()\n", + " for pair_idx in range(num_pairs):\n", + " (q0, q1, q2, q3) = (\n", + " pair_idx * 4,\n", + " pair_idx * 4 + 1,\n", + " pair_idx * 4 + 2,\n", + " pair_idx * 4 + 3,\n", + " )\n", + " (c0, c1) = pair_idx * 4, pair_idx * 4 + 3 # edge qubits\n", + "\n", + " bell_zz.measure(q0, c0)\n", + " bell_zz.measure(q3, c1)\n", + "\n", + " bell_xx.measure(q0, c0)\n", + " bell_xx.measure(q3, c1)\n", + "\n", + " bell_circuits.append(bell_zz)\n", + " bell_circuits.append(bell_xx)\n", + "\n", + " return bell_circuits\n", + "\n", + "\n", + "def get_mse(result, initial_layouts):\n", + " \"\"\"\n", + " given a result object and the initial layouts, returns a dict of layouts and their mse\n", + " \"\"\"\n", + " layout_mse = {}\n", + " for layout_idx, initial_layout in enumerate(initial_layouts):\n", + " layout_mse[tuple(initial_layout)] = {}\n", + "\n", + " num_pairs = len(initial_layout) // 4\n", + "\n", + " counts_zz = result[2 * layout_idx].data.c.get_counts()\n", + " total_shots = sum(counts_zz.values())\n", + "\n", + " # Get ZZ expectation value\n", + " exp_zz_list = []\n", + " for pair_idx in range(num_pairs):\n", + " exp_zz = 0\n", + " for bitstr, shots in counts_zz.items():\n", + " bitstr = bitstr[::-1] # reverse order to big endian\n", + " b1, b0 = (\n", + " bitstr[pair_idx * 4],\n", + " bitstr[pair_idx * 4 + 3],\n", + " ) # parse bitstring to get edge measurements for each 4-q chain\n", + " z_val0 = 1 if b0 == \"0\" else -1\n", + " z_val1 = 1 if b1 == \"0\" else -1\n", + " exp_zz += z_val0 * z_val1 * shots\n", + " exp_zz /= total_shots\n", + " exp_zz_list.append(exp_zz)\n", + "\n", + " counts_xx = result[2 * layout_idx + 1].data.c.get_counts()\n", + " total_shots = sum(counts_xx.values())\n", + "\n", + " # Get XX expectation value\n", + " exp_xx_list = []\n", + " for pair_idx in range(num_pairs):\n", + " exp_xx = 0\n", + " for bitstr, shots in counts_xx.items():\n", + " bitstr = bitstr[::-1] # reverse order to big endian\n", + " b1, b0 = (\n", + " bitstr[pair_idx * 4],\n", + " bitstr[pair_idx * 4 + 3],\n", + " ) # parse bitstring to get edge measurements for each 4-q chain\n", + " x_val0 = 1 if b0 == \"0\" else -1\n", + " x_val1 = 1 if b1 == \"0\" else -1\n", + " exp_xx += x_val0 * x_val1 * shots\n", + " exp_xx /= total_shots\n", + " exp_xx_list.append(exp_xx)\n", + "\n", + " mse_list = [\n", + " ((exp_zz - 1) ** 2 + (exp_xx - 1) ** 2) / 2\n", + " for exp_zz, exp_xx in zip(exp_zz_list, exp_xx_list)\n", + " ]\n", + "\n", + " print(f\"layout {initial_layout}\")\n", + " for idx in range(num_pairs):\n", + " layout_mse[tuple(initial_layout)][\n", + " tuple(initial_layout[4 * idx : 4 * idx + 4])\n", + " ] = mse_list[idx]\n", + " print(\n", + " f\"qubits: {initial_layout[4*idx:4*idx+4]}, mse:, {round(mse_list[idx],4)}\"\n", + " )\n", + " # print(f'exp_zz: {round(exp_zz_list[idx],4)}, exp_xx: {round(exp_xx_list[idx],4)}')\n", + " print(\" \")\n", + " return layout_mse\n", + "\n", + "\n", + "def plot_mse_ecdfs(layouts_mse, combine_layouts=False):\n", + " \"\"\"\n", + " Plot CDF of MSE data for multiple layouts. Optionally combine all data in a single CDF\n", + " \"\"\"\n", + "\n", + " if not combine_layouts:\n", + " for initial_layout, layouts in layouts_mse.items():\n", + " sorted_layouts = dict(\n", + " sorted(layouts.items(), key=lambda item: item[1])\n", + " ) # sort layouts by mse\n", + "\n", + " # get layouts and mses\n", + " layout_list = list(sorted_layouts.keys())\n", + " mse_list = np.asarray(list(sorted_layouts.values()))\n", + "\n", + " # convert to numpy\n", + " x = np.array(mse_list)\n", + " y = np.arange(1, len(x) + 1) / len(x)\n", + "\n", + " # Prepend (x[0], 0) to start CDF at zero\n", + " x = np.insert(x, 0, x[0])\n", + " y = np.insert(y, 0, 0)\n", + "\n", + " # Create the plot\n", + " plt.plot(\n", + " x,\n", + " y,\n", + " marker=\"x\",\n", + " linestyle=\"-\",\n", + " label=f\"qubits: {initial_layout}\",\n", + " )\n", + "\n", + " # add qubits labels for the edge pairs\n", + " for xi, yi, q in zip(x[1:], y[1:], layout_list):\n", + " plt.annotate(\n", + " [q[0], q[3]],\n", + " (xi, yi),\n", + " textcoords=\"offset points\",\n", + " xytext=(5, -10),\n", + " ha=\"left\",\n", + " fontsize=8,\n", + " )\n", + "\n", + " elif combine_layouts:\n", + " all_layouts = {}\n", + " all_initial_layout = []\n", + " for (\n", + " initial_layout,\n", + " layouts,\n", + " ) in layouts_mse.items(): # puts together all layout information\n", + " all_layouts.update(layouts)\n", + " all_initial_layout += initial_layout\n", + "\n", + " sorted_layouts = dict(\n", + " sorted(all_layouts.items(), key=lambda item: item[1])\n", + " ) # sort layouts by mse\n", + "\n", + " # get layouts and mses\n", + " layout_list = list(sorted_layouts.keys())\n", + " mse_list = np.asarray(list(sorted_layouts.values()))\n", + "\n", + " # convert to numpy\n", + " x = np.array(mse_list)\n", + " y = np.arange(1, len(x) + 1) / len(x)\n", + "\n", + " # Prepend (x[0], 0) to start CDF at zero\n", + " x = np.insert(x, 0, x[0])\n", + " y = np.insert(y, 0, 0)\n", + "\n", + " # Create the plot\n", + " plt.plot(\n", + " x,\n", + " y,\n", + " marker=\"x\",\n", + " linestyle=\"-\",\n", + " label=f\"qubits: {sorted(list(set(all_initial_layout)))}\",\n", + " )\n", + "\n", + " # add qubit labels for the edge pairs\n", + " for xi, yi, q in zip(x[1:], y[1:], layout_list):\n", + " plt.annotate(\n", + " [q[0], q[3]],\n", + " (xi, yi),\n", + " textcoords=\"offset points\",\n", + " xytext=(5, -10),\n", + " ha=\"left\",\n", + " fontsize=8,\n", + " )\n", + "\n", + " plt.xscale(\"log\")\n", + " plt.xlabel(\"Mean squared error of ⟨ZZ⟩ and ⟨XX⟩\")\n", + " plt.ylabel(\"Cumulative distribution function\")\n", + " plt.title(\"CDF for different initial layouts\")\n", + " plt.grid(alpha=0.3)\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "2a16f346", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "The first step is to create a set of quantum circuits to benchmark all candidate Bell-pair links tailored to the device’s topology. We programmatically search the device coupling map for all linearly-connected chains of four qubits. Each such chain (labelled by qubit indices $[q0–q1–q2–q3]$) serves as a test case for the entanglement-swapping circuit. By identifying all possible length-4 paths, we ensure maximum coverage for possible grouping of qubits that could realize the protocol." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55f49481", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(operational=True)\n", + "backend.target.add_instruction(IfElseOp, name=\"if_else\")" + ] + }, + { + "cell_type": "markdown", + "id": "27ee6db2", + "metadata": {}, + "source": [ + "We generate these chains by using a helper function that performs a greedy search on the device graph. It returns “stripes” of four 4-qubit chains bundled into 16-qubit groups (dynamic circuits currently constrain the size of the measurement register to `16` qubits). Bundling allows us to run multiple 4-qubit experiments in parallel on distinct parts of the chip, and make efficient use of the whole device. Each 16-qubit stripe contains four disjoint chains, meaning that no qubit is reused within that group. For example, one stripe might consist of chains $[0-1-2-3]$, $[4-5-6-7]$, $[8-9-10-11]$, and $[12-13-14-15]$ all packaged together. Any qubit that was not included in a stripe is returned in the `leftover` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "0cae5453", + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import chain\n", + "from collections import defaultdict\n", + "\n", + "\n", + "def stripes16_from_backend(backend):\n", + " \"\"\"\n", + " Creates stripes of 16 qubits, four non-overlapping 4-qubit chains, that cover as much of\n", + " the coupling map as possible. Returns any unused qubits as leftovers.\n", + " \"\"\"\n", + " # get the undirected adjacency list\n", + " edges = backend.coupling_map.get_edges()\n", + " graph = defaultdict(set)\n", + " for u, v in edges:\n", + " graph[u].add(v)\n", + " graph[v].add(u)\n", + "\n", + " qubits = sorted(graph) # all qubit indices that appear\n", + "\n", + " # greedy search for 4-long linear chains (blocks) ────────────\n", + " used = set() # qubits already placed in a block\n", + " blocks = [] # each block is a 4-qubit list\n", + "\n", + " for q in qubits: # deterministic order for reproducibility\n", + " if q in used:\n", + " continue # already consumed by earlier block\n", + "\n", + " # depth-first \"straight\" walk of length 3 without revisiting nodes\n", + " def extend(path):\n", + " if len(path) == 4:\n", + " return path\n", + " tip = path[-1]\n", + " for nbr in sorted(graph[tip]): # deterministic\n", + " if nbr not in path and nbr not in used:\n", + " maybe = extend(path + [nbr])\n", + " if maybe:\n", + " return maybe\n", + " return None\n", + "\n", + " block = extend([q])\n", + " if block: # found a 4-node path\n", + " blocks.append(block)\n", + " used.update(block)\n", + "\n", + " # bundle four 4-qubit blocks into one 16-qubit stripe (max number of measurement compatible with if-else)\n", + " stripes = [\n", + " list(chain.from_iterable(blocks[i : i + 4]))\n", + " for i in range(0, len(blocks) // 4 * 4, 4) # full groups of four\n", + " ]\n", + "\n", + " leftovers = set(qubits) - set(chain.from_iterable(stripes))\n", + " return stripes, leftovers" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "3cf9db43", + "metadata": {}, + "outputs": [], + "source": [ + "initial_layouts, leftover = stripes16_from_backend(backend)" + ] + }, + { + "cell_type": "markdown", + "id": "8121aa97", + "metadata": {}, + "source": [ + "Next, we construct the circuit for each 16-qubit stripe. The routine does the following for each chain:\n", + "\n", + "* Prepare a middle Bell pair: Apply a Hadamard on qubit 1 and a CNOT from qubit 1 to qubit 2. This entangles qubits 1 and 2 (creating an $|\\Phi^+\\rangle = (|00\\rangle + |11\\rangle)/\\sqrt{2}$ Bell state).\n", + "* Entangle the edge qubits: Apply a CNOT from qubit 0 to qubit 1, and a CNOT from qubit 2 to qubit 3. This links the initially separate pairs so that qubits 0 and 3 will become entangled after the next steps. A Hadamard on qubit 2 is also applied (this, combined with the previous CNOTs, forms part of a Bell measurement on qubits 1 and 2). At this point, qubits 0 and 3 are not yet entangled, but qubits 1 and 2 are entangled with them in a larger four-qubit state.\n", + "* Mid-circuit measurements and feedforward: Qubits 1 and 2 (the middle qubits) are measured in the computational basis, yielding two classical bits. Based on those measurement outcomes, we apply conditional operations: if the qubit 1 measurement (call this bit $m_{12}$) is 1, we apply an $X$ gate on qubit 3; if the qubit 2 measurement ($m_{21}$) is 1, we apply a $Z$ gate on qubit 0. These conditional gates (realized by using the Qiskit `if_test`/`if_else` construct) implement the standard teleportation corrections. They “undo” the random Pauli flips that occur due to projecting qubits 1 and 2, ensuring that qubits 0 and 3 end up in a known Bell state regardless of the measurement outcomes. After this step, qubits 0 and 3 should ideally be entangled in the Bell state $|\\Phi^+\\rangle$.\n", + "* Measure Bell pair stabilizers: We then split into two versions of the circuit. In the first version, we measure the $ZZ$ stabilizer on qubits 0 and 3. In the second version, we measure the $XX$ stabilizer on these qubits.\n", + "\n", + "For each 4-qubit initial layout, the above function returns two circuits (one for $ZZ$, one for $XX$ stabilizer measurement). At the end of this step, we have a list of circuits covering every 4-qubit chain on the device. These circuits include mid-circuit measurements and conditional (if/else) operations, which are the key instructions of the dynamic circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "bd04755f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "circuits = create_bell_stab(initial_layouts)\n", + "circuits[-1].draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "97eb3019", + "metadata": {}, + "source": [ + "## Step 2: Optimize the problem for quantum hardware execution\n", + "\n", + "\n", + "Before executing our circuits on real hardware, we need to transpile them to match the device’s physical constraints. Transpilation will map the abstract circuit onto the physical qubits and gate set of the chosen device. Since we have already chosen specific physical qubits for each chain (by providing an `initial_layout` to the circuit generator), we use transpiler `optimization_level=0` with that fixed layout. This tells Qiskit not to reassign qubits or perform any heavy optimizations that could alter the circuit structure. We want to keep the sequence of operations (especially the conditional gates) exactly as specified." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b36f5802", + "metadata": {}, + "outputs": [], + "source": [ + "isa_circuits = []\n", + "for ind, init_layout in enumerate(initial_layouts):\n", + " pm = generate_preset_pass_manager(\n", + " optimization_level=0, backend=backend, initial_layout=init_layout\n", + " )\n", + " isa_circ = pm.run(circuits[ind * 2 : ind * 2 + 2])\n", + " isa_circuits.extend(isa_circ)" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "3ad620f7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isa_circuits[1].draw(\"mpl\", fold=-1, idle_wires=False)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c5edec73", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "\n", + "\n", + "We can now run the experiment on the quantum device. We use Qiskit Runtime and its Sampler primitive to execute the batch of circuits efficiently. The following `experimental` options are only required during the early-access period:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1a5ad8c", + "metadata": {}, + "outputs": [], + "source": [ + "sampler = Sampler(mode=backend)\n", + "sampler.options.experimental = {\n", + " \"execution_path\": \"gen3-experimental\",\n", + "}\n", + "sampler.options.environment.job_tags = [\"cut-bell-pair-test\"]\n", + "job = sampler.run(isa_circuits)" + ] + }, + { + "cell_type": "markdown", + "id": "87484abf", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in the desired classical format\n", + "\n", + "\n", + "The final step is to compute the mean squared error metric (MSE) for each tested qubit group and summarize the results. For each chain, we now have the measured $\\langle Z_0Z_3\\rangle$ and $\\langle X_0X_3\\rangle$. If qubits 0 and 3 were perfectly entangled in a $|\\Phi^+\\rangle$ Bell state, we would expect both of these to be +1. We quantify the deviation using the MSE:\n", + "\n", + "$$\\text{MSE} = \\frac{( \\langle Z_0Z_3\\rangle - 1)^2 + (\\langle X_0X_3\\rangle - 1)^2}{2}.$$\n", + "\n", + "This value is 0 for a perfect Bell pair, and increases as the entangled state gets noisier (with random outcomes giving an expectation around 0, the MSE would approach 1). The code calculates this MSE for each 4-qubit group.\n", + "\n", + "The results reveal a wide range of entanglement quality across the device. This confirms the paper’s finding that there can be over an order of magnitude variation in Bell-state fidelity depending on which physical qubits are used. In practical terms, this means that certain regions or links in the chip are much better at doing mid-circuit measurement and feedforward operations than others. Factors like qubit readout error, qubit lifetime, and crosstalk, likely contribute to these differences. For instance, if one chain includes a particularly noisy readout qubit, the mid-circuit measurement could be unreliable, leading to a poor fidelity for that entangled pair (high MSE)." + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "9abec739", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "layout [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]\n", + "qubits: [0, 1, 2, 3], mse:, 0.0312\n", + "qubits: [4, 5, 6, 7], mse:, 0.0491\n", + "qubits: [8, 9, 10, 11], mse:, 0.0711\n", + "qubits: [12, 13, 14, 15], mse:, 0.0436\n", + " \n", + "layout [16, 23, 22, 21, 17, 27, 26, 25, 18, 31, 30, 29, 19, 35, 34, 33]\n", + "qubits: [16, 23, 22, 21], mse:, 0.0197\n", + "qubits: [17, 27, 26, 25], mse:, 0.113\n", + "qubits: [18, 31, 30, 29], mse:, 0.0287\n", + "qubits: [19, 35, 34, 33], mse:, 0.0433\n", + " \n", + "layout [36, 41, 42, 43, 37, 45, 46, 47, 38, 49, 50, 51, 39, 53, 54, 55]\n", + "qubits: [36, 41, 42, 43], mse:, 0.1645\n", + "qubits: [37, 45, 46, 47], mse:, 0.0409\n", + "qubits: [38, 49, 50, 51], mse:, 0.0519\n", + "qubits: [39, 53, 54, 55], mse:, 0.0829\n", + " \n", + "layout [56, 63, 62, 61, 57, 67, 66, 65, 58, 71, 70, 69, 59, 75, 74, 73]\n", + "qubits: [56, 63, 62, 61], mse:, 0.8663\n", + "qubits: [57, 67, 66, 65], mse:, 0.0375\n", + "qubits: [58, 71, 70, 69], mse:, 0.0664\n", + "qubits: [59, 75, 74, 73], mse:, 0.0291\n", + " \n", + "layout [76, 81, 82, 83, 77, 85, 86, 87, 78, 89, 90, 91, 79, 93, 94, 95]\n", + "qubits: [76, 81, 82, 83], mse:, 0.0598\n", + "qubits: [77, 85, 86, 87], mse:, 0.313\n", + "qubits: [78, 89, 90, 91], mse:, 0.0679\n", + "qubits: [79, 93, 94, 95], mse:, 0.0505\n", + " \n", + "layout [96, 103, 102, 101, 97, 107, 106, 105, 98, 111, 110, 109, 99, 115, 114, 113]\n", + "qubits: [96, 103, 102, 101], mse:, 0.0302\n", + "qubits: [97, 107, 106, 105], mse:, 0.0384\n", + "qubits: [98, 111, 110, 109], mse:, 0.0375\n", + "qubits: [99, 115, 114, 113], mse:, 0.1051\n", + " \n", + "layout [116, 121, 122, 123, 117, 125, 126, 127, 118, 129, 130, 131, 119, 133, 134, 135]\n", + "qubits: [116, 121, 122, 123], mse:, 0.1624\n", + "qubits: [117, 125, 126, 127], mse:, 0.7246\n", + "qubits: [118, 129, 130, 131], mse:, 0.5919\n", + "qubits: [119, 133, 134, 135], mse:, 0.5277\n", + " \n", + "layout [136, 143, 142, 141, 137, 147, 146, 145, 138, 151, 150, 149, 139, 155, 154, 153]\n", + "qubits: [136, 143, 142, 141], mse:, 0.0383\n", + "qubits: [137, 147, 146, 145], mse:, 1.0187\n", + "qubits: [138, 151, 150, 149], mse:, 0.1531\n", + "qubits: [139, 155, 154, 153], mse:, 0.0471\n", + " \n" + ] + } + ], + "source": [ + "layouts_mse = get_mse(job.result(), initial_layouts)" + ] + }, + { + "cell_type": "markdown", + "id": "5af0facd", + "metadata": {}, + "source": [ + "Finally, we visualize the overall performance by plotting the cumulative distribution function (CDF) of the MSE values for all chains. The CDF plot shows the MSE threshold on the x-axis, and the fraction of qubit pairs that have at most that MSE on the y-axis. This curve starts at zero and approaches one as the threshold grows to encompass all data points. A steep rise near a low MSE would indicate that many pairs are high-fidelity; a slow rise means that many pairs have larger errors. We annotate the CDF with the identities of the best pairs. In the plot, each point in the CDF corresponds to one 4-qubit chain’s MSE, and we label the point with the pair of qubit indices $[q0, q3]$ that were entangled in that experiment. This makes it easy to spot which physical qubit pairs are the top performers (the far-left points on the CDF)." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "678ddac9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_mse_ecdfs(layouts_mse, combine_layouts=True)" + ] + }, + { + "cell_type": "markdown", + "id": "78e91ddc", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[\\[1\\] Carrera Vazquez, A., Tornow, C., Ristè, D. et al. Combining quantum processors with real-time classical communication. Nature 636, 75–79 (2024).](https://www.nature.com/articles/s41586-024-08178-2)" + ] + } + ], + "metadata": { + "description": "Benchmark dynamic circuit capabilities with cut Bell pair protocol", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3" + }, + "title": "Benchmark dynamic circuits with cut Bell pairs", + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/index.mdx b/docs/tutorials/index.mdx index de58be8cccf..030a15e658d 100644 --- a/docs/tutorials/index.mdx +++ b/docs/tutorials/index.mdx @@ -79,6 +79,8 @@ This section introduces advanced capabilities within the Qiskit ecosystem that e Workload optimization focuses on either efficient orchestration of classical and quantum resources or tailored methods for improving quantum circuit manipulation. +* [Benchmark dynamic circuits with cut Bell pairs](/docs/tutorials/edc-cut-bell-pair-benchmarking) + * [Introduction to fractional gates](/docs/tutorials/fractional-gates) * [Qiskit AI-powered transpiler service introduction](/docs/tutorials/ai-transpiler-introduction) diff --git a/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/3ad620f7-0.avif b/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/3ad620f7-0.avif new file mode 100644 index 00000000000..6cf75509051 Binary files /dev/null and b/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/3ad620f7-0.avif differ diff --git a/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/678ddac9-0.avif b/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/678ddac9-0.avif new file mode 100644 index 00000000000..363bc940f4a Binary files /dev/null and b/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/678ddac9-0.avif differ diff --git a/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/bd04755f-0.avif b/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/bd04755f-0.avif new file mode 100644 index 00000000000..842d1f3b313 Binary files /dev/null and b/public/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/bd04755f-0.avif differ diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 4f8c9454a82..ddcf6e49a50 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -695,6 +695,9 @@ notifications: "docs/tutorials/multi-product-formula": - "@miamico" - "@jenglick" + "docs/tutorials/edc-cut-bell-pair-benchmarking": + - "@miamico" + - "@jyu00" - "@annaliese-estes" "docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits": - "@henryzou50" diff --git a/scripts/config/notebook-testing.toml b/scripts/config/notebook-testing.toml index bf19bfcdce8..f95b0a1bb7e 100644 --- a/scripts/config/notebook-testing.toml +++ b/scripts/config/notebook-testing.toml @@ -192,6 +192,7 @@ notebooks = [ "docs/tutorials/nishimori-phase-transition.ipynb", "docs/tutorials/global-data-quantum-optimizer.ipynb", "docs/tutorials/colibritd-pde.ipynb", + "docs/tutorials/edc-cut-bell-pair-benchmarking.ipynb", "docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb", # Don't test any learning notebooks