{ "cells": [ { "cell_type": "markdown", "id": "f2f3597d", "metadata": {}, "source": [ "# Bell circuits, finite shots and honest uncertainty\n", "\n", "**Level:** Core · **Book connections:** P04, P05, P07, P12, P21, F13, F18\n", "\n", "After this notebook you can:\n", "\n", "- Read a CNOT circuit and explain its control and target.\n", "- Keep shot counts separate from exact probabilities.\n", "- Use a confidence interval and a predeclared statistical tolerance.\n", "\n", "Run cells from top to bottom in the pinned environment described in `README.md`. All default execution is local. Exact amplitudes are simulator information; a QPU supplies sampled outcomes." ] }, { "cell_type": "code", "execution_count": null, "id": "054e8470", "metadata": {}, "outputs": [], "source": [ "from importlib.metadata import version\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display\n", "from qiskit import QuantumCircuit\n", "from qiskit.quantum_info import Statevector\n", "\n", "EXPECTED = {\"qiskit\": \"2.6.1\", \"qiskit-aer\": \"0.18.2\", \"qiskit-ibm-runtime\": \"0.51.0\"}\n", "for package, expected in EXPECTED.items():\n", " actual = version(package)\n", " assert actual == expected, f\"Use the supplied environment: {package}={actual}, expected {expected}\"\n", "print(\"Verified package versions:\", EXPECTED)\n", "plt.rcParams.update({\"figure.figsize\": (8, 3.5), \"font.size\": 11, \"axes.spines.top\": False,\n", " \"axes.spines.right\": False, \"axes.prop_cycle\": plt.cycler(color=[\"#006DAE\", \"#00A6B2\", \"#9E4A78\"])})\n", "SEED = 20261106\n", "\n", "def draw(circuit):\n", " \"\"\"Render a real circuit; each wire is a qubit, time runs left to right.\"\"\"\n", " fig = circuit.draw(output=\"mpl\", fold=90, style=\"iqp\")\n", " display(fig)\n", " plt.close(fig)\n", "\n", "def book_label(qiskit_label):\n", " \"\"\"For a single register, show q0 first as in the book.\"\"\"\n", " return qiskit_label[::-1]" ] }, { "cell_type": "markdown", "id": "901e8c2e", "metadata": {}, "source": [ "**Read the wires and bit strings carefully.** Qiskit draws `q0` on the top wire, but a\n", "standard statevector basis label or measured single-register count key is written\n", "`q(n−1)…q1q0`: `q0` has integer weight $2^0$. The book often writes `q0` first.\n", "These notebooks keep native Qiskit labels in calculations and use `book_label`\n", "only for an explicitly labelled display. Reversing a label changes the display;\n", "it does not apply a SWAP gate. All count examples here use one classical register\n", "with measurement `qj → cj`; do not reverse arbitrary multi-register strings." ] }, { "cell_type": "markdown", "id": "df77087e", "metadata": {}, "source": [ "## 1. A correlated pair\n", "\n", "CNOT flips its target when its control is 1. Here H acts on `q0`, and\n", "`cx(0, 1)` uses `q0` as control and `q1` as target. This makes\n", "\n", "$$|00\\rangle\\xrightarrow{H_0}\\frac{|00\\rangle+|01\\rangle}{\\sqrt2}\n", " \\xrightarrow{\\mathrm{CX}_{0,1}}\\frac{|00\\rangle+|11\\rangle}{\\sqrt2}.$$\n", "\n", "The middle ket uses native Qiskit order $|q_1q_0\\rangle$. Each individual bit\n", "is random, and the pair agrees in this measurement basis. Correlation in this\n", "one basis alone does not establish a Bell-inequality violation." ] }, { "cell_type": "code", "execution_count": null, "id": "ecb6bbb7", "metadata": {}, "outputs": [], "source": [ "from qiskit.primitives import StatevectorSampler\n", "bell = QuantumCircuit(2, name=\"Bell pair\")\n", "bell.h(0)\n", "bell.cx(0, 1)\n", "draw(bell)\n", "exact = Statevector.from_instruction(bell).probabilities()\n", "np.testing.assert_allclose(exact, [.5, 0, 0, .5], atol=1e-12)\n", "measured = bell.copy()\n", "measured.measure_all()\n", "sampler = StatevectorSampler(seed=SEED)\n", "shots = 1024\n", "counts = sampler.run([measured], shots=shots).result()[0].data.meas.get_counts()\n", "assert sum(counts.values()) == shots\n", "assert set(counts) <= {\"00\", \"11\"}\n", "print(\"Observed counts:\", counts)\n", "print(\"Native order q1q0:\", counts)\n", "print(\"Book display order q0q1:\", {book_label(k): v for k, v in counts.items()})" ] }, { "cell_type": "markdown", "id": "82b4bfe6", "metadata": {}, "source": [ "## 2. Sampling has a budget\n", "\n", "One shot returns one measured bit string. If $k$ of $N$ shots are `11`, our\n", "estimate is $\\hat p=k/N$. The standard error is approximately\n", "$\\sqrt{\\hat p(1-\\hat p)/N}$. Four times as many shots roughly halves it.\n", "A seed makes a local experiment repeatable; real QPU shots cannot be replayed\n", "by setting a simulator seed.\n", "\n", "We use a Wilson interval because a symmetric $\\hat p\\pm 1.96\\,SE$ interval\n", "behaves poorly near 0 and 1. This is a nominal 95% interval under independent,\n", "identically distributed Bernoulli shots. It does not cover drifting calibration\n", "or all other sources of experimental uncertainty." ] }, { "cell_type": "code", "execution_count": null, "id": "bc67ba11", "metadata": {}, "outputs": [], "source": [ "def wilson(k, n, z=1.959963984540054):\n", " if not (0 <= k <= n and n > 0):\n", " raise ValueError(\"Require 0 <= k <= n and n > 0\")\n", " p = k / n\n", " denominator = 1 + z*z/n\n", " center = (p + z*z/(2*n)) / denominator\n", " half = z*np.sqrt(p*(1-p)/n + z*z/(4*n*n)) / denominator\n", " return center-half, center+half\n", "\n", "k = counts.get(\"11\", 0)\n", "print(\"p-hat:\", k/shots, \"Wilson 95% interval:\", wilson(k, shots))\n", "# Predeclare an acceptance tolerance rather than searching for one after seeing data.\n", "delta = .001\n", "epsilon = np.sqrt(np.log(2/delta)/(2*shots))\n", "# Hoeffding: P(|p-hat - p| >= epsilon) <= delta for independent Bernoulli shots.\n", "assert abs(k/shots - .5) <= epsilon\n", "print(\"Predeclared 0.1% false-rejection bound, tolerance:\", epsilon)\n", "\n", "budgets = [64, 256, 1024, 4096]\n", "estimates, intervals = [], []\n", "for n in budgets:\n", " c = StatevectorSampler(seed=SEED+n).run([measured], shots=n).result()[0].data.meas.get_counts()\n", " p = c.get(\"11\", 0)/n\n", " lo, hi = wilson(c.get(\"11\", 0), n)\n", " estimates.append(p)\n", " intervals.append((p-lo, hi-p))\n", "plt.errorbar(budgets, estimates, yerr=np.array(intervals).T, fmt=\"o\", capsize=5)\n", "plt.axhline(.5, color=\"gray\", linestyle=\"--\", label=\"Exact ideal probability\")\n", "plt.xscale(\"log\", base=2)\n", "plt.xlabel(\"Shots\")\n", "plt.ylabel(\"Estimated P(11), nominal 95% interval\")\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "2f5825d3", "metadata": {}, "source": [ "## 3. Check endianness with an asymmetric example\n", "\n", "`00` and `11` look the same when reversed, so Bell tests can hide a bit-order\n", "defect. Prepare `q0=1, q1=0` as an independent regression test." ] }, { "cell_type": "code", "execution_count": null, "id": "c8fb6f5f", "metadata": {}, "outputs": [], "source": [ "probe = QuantumCircuit(2)\n", "probe.x(0)\n", "probe.measure_all()\n", "probe_counts = StatevectorSampler(seed=SEED).run([probe], shots=16).result()[0].data.meas.get_counts()\n", "assert probe_counts == {\"01\": 16}\n", "assert {book_label(k): v for k, v in probe_counts.items()} == {\"10\": 16}\n", "print(\"Native q1q0 vs book q0q1:\", probe_counts, {book_label(k): v for k, v in probe_counts.items()})" ] }, { "cell_type": "markdown", "id": "bef66dd0", "metadata": {}, "source": [ "## Checkpoint and practice\n", "\n", "A run returns `11` 530 times out of 1024. Does this prove the circuit is biased?\n", "**No:** finite sampling creates variation. Quantify the discrepancy against a\n", "tolerance chosen before the experiment. A confidence interval is also random;\n", "it need not contain the ideal value on every run.\n", "\n", "1. Change the shot budget to 64. Compare uncertainty, not just the point estimate.\n", "2. Remove CNOT. Which new bit strings become possible? Explain from the wires.\n", "3. Apply Z to `q0` of the Bell pair. The Z-basis counts stay the same. Explain why\n", " an additional basis-sensitive test is needed to verify the state.\n", "\n", "**Takeaway:** exact state checks and statistical checks answer different questions." ] }, { "cell_type": "markdown", "id": "78415897", "metadata": {}, "source": [ "## Sources\n", "\n", "- [IBM: bit ordering](https://quantum.cloud.ibm.com/docs/en/guides/bit-ordering)\n", "- [IBM: Statevector API](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.quantum_info.Statevector)\n", "- [IBM: StatevectorSampler API](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.primitives.StatevectorSampler)\n", "- [NIST: confidence limits for a proportion](https://www.itl.nist.gov/div898/handbook/prc/section2/prc241.htm)" ] } ], "metadata": { "kernelspec": { "display_name": "Quantum book (pinned Python)", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.13.2" }, "quantum_book": { "chapters": [ "P04", "P05", "P07", "P12", "P21", "F13", "F18" ], "expected_packages": { "qiskit": "2.6.1", "qiskit-aer": "0.18.2", "qiskit-ibm-runtime": "0.51.0" }, "level": "Core", "network_on_default_run": false, "verified_release_date": "2026-11-06" } }, "nbformat": 4, "nbformat_minor": 5 }