{ "cells": [ { "cell_type": "markdown", "id": "8c35b066", "metadata": {}, "source": [ "# Quantum software tests that catch plausible wrong programs\n", "\n", "**Level:** Undergraduate · **Book connections:** P11, P12, P13, P14, P15, P24\n", "\n", "After this notebook you can:\n", "\n", "- Select state, unitary and statistical tests for different defects.\n", "- Catch relative-phase and reversed-control bugs that pass weak probability tests.\n", "- Use metamorphic properties and deliberate mutations to evaluate a test suite.\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": "1f6b2535", "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": "3030fdca", "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": "565396b6", "metadata": {}, "source": [ "## 1. State verification: choose an informative assertion\n", "\n", "A unit test should fail for a plausible mistake. The Z-basis probabilities of\n", "$|+\\rangle$ and $|{-}\\rangle$ are identical, so they form a useful counterexample\n", "to a weak test. Verify the target state up to a global phase instead:\n", "\n", "$$|\\langle\\psi_{\\rm target}|\\psi_{\\rm actual}\\rangle|^2=1.$$\n", "\n", "`Statevector.equiv` provides this global-phase tolerance. It still distinguishes\n", "physically different relative phases. We catch expected failures below so the\n", "notebook can finish; a production test runner would report them as failed mutations." ] }, { "cell_type": "code", "execution_count": null, "id": "c81f6ac5", "metadata": {}, "outputs": [], "source": [ "from qiskit.quantum_info import Operator\n", "def prepare_minus(include_z=True):\n", " circuit = QuantumCircuit(1)\n", " circuit.h(0)\n", " if include_z:\n", " circuit.z(0)\n", " return circuit\n", "\n", "correct = Statevector.from_instruction(prepare_minus())\n", "missing_z = Statevector.from_instruction(prepare_minus(False))\n", "np.testing.assert_allclose(correct.probabilities(), missing_z.probabilities(), atol=1e-12)\n", "assert correct.equiv(Statevector.from_label(\"-\"))\n", "assert not missing_z.equiv(Statevector.from_label(\"-\"))\n", "assert Statevector(np.exp(.71j)*correct.data).equiv(correct)\n", "print(\"Probability-only test missed the defect; the phase-sensitive state test caught it.\")" ] }, { "cell_type": "markdown", "id": "a8d8c78b", "metadata": {}, "source": [ "## 2. Control direction and bit-order tests\n", "\n", "A Bell preparation test starting at $|00\\rangle$ can conceal defects if the\n", "implementation and expected result repeat the same convention mistake. Test\n", "asymmetric basis inputs and compare the entire small unitary where feasible." ] }, { "cell_type": "code", "execution_count": null, "id": "40489353", "metadata": {}, "outputs": [], "source": [ "reference_cx = QuantumCircuit(2)\n", "reference_cx.cx(0, 1)\n", "reversed_cx = QuantumCircuit(2)\n", "reversed_cx.cx(1, 0)\n", "assert not Operator(reference_cx).equiv(Operator(reversed_cx))\n", "# Native |01> means q0=1, q1=0; q0 controls the flip of q1.\n", "expected_mapping = {\"00\": \"00\", \"01\": \"11\", \"10\": \"10\", \"11\": \"01\"}\n", "for native_input, native_output in expected_mapping.items():\n", " state = Statevector.from_label(native_input).evolve(reference_cx)\n", " assert state.equiv(Statevector.from_label(native_output))\n", "asymmetric = Statevector.from_label(\"01\")\n", "assert not asymmetric.evolve(reversed_cx).equiv(Statevector.from_label(\"11\"))\n", "print(\"All basis mappings checked; reversed control/target mutation caught.\")" ] }, { "cell_type": "markdown", "id": "52a86b34", "metadata": {}, "source": [ "## 3. Metamorphic and parameterised properties\n", "\n", "Sometimes the exact output is difficult to calculate but a relation between\n", "runs is simple. A unitary followed by its inverse must restore any input.\n", "The property $R_y(\\theta)R_y(-\\theta)=I$ tests multiple angles, including\n", "boundaries. This finite parameter sweep is a metamorphic regression test;\n", "it is not a proof covering all real angles." ] }, { "cell_type": "code", "execution_count": null, "id": "509a550e", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED)\n", "angles = [0, np.pi/2, np.pi, -np.pi] + rng.uniform(-2*np.pi, 2*np.pi, 20).tolist()\n", "for theta in angles:\n", " round_trip = QuantumCircuit(1)\n", " round_trip.ry(theta, 0)\n", " round_trip.ry(-theta, 0)\n", " assert Operator(round_trip).equiv(Operator(np.eye(2)))\n", "# General circuit/inverse property on several independently generated inputs.\n", "unitary = QuantumCircuit(2)\n", "unitary.ry(.37, 0)\n", "unitary.h(1)\n", "unitary.cx(1, 0)\n", "unitary.rz(-.81, 1)\n", "for _ in range(12):\n", " raw = rng.normal(size=4)+1j*rng.normal(size=4)\n", " input_state = Statevector(raw/np.linalg.norm(raw))\n", " output_state = input_state.evolve(unitary).evolve(unitary.inverse())\n", " assert output_state.equiv(input_state)\n", "print(\"Parameter and inverse properties passed.\")" ] }, { "cell_type": "markdown", "id": "0ef595fa", "metadata": {}, "source": [ "## 4. Statistical tests: set the tolerance first\n", "\n", "A sampling test should tolerate expected randomness while rejecting a meaningful\n", "defect. For a target probability $p$, Hoeffding's inequality gives\n", "\n", "$$\\Pr(|\\hat p-p|\\ge\\epsilon)\\le2e^{-2N\\epsilon^2}.$$\n", "\n", "For $m$ predeclared tests and total false-rejection budget $\\alpha$, choose\n", "$\\epsilon=\\sqrt{\\ln(2m/\\alpha)/(2N)}$ and use a union bound. This assumes independent\n", "Bernoulli shots within each circuit run. There is still a nonzero failure\n", "probability. Exact seeded counts are not a portable scientific assertion." ] }, { "cell_type": "code", "execution_count": null, "id": "b6da340f", "metadata": {}, "outputs": [], "source": [ "from qiskit.primitives import StatevectorSampler\n", "tested_angles = [.4, 1.2, 2.4]\n", "shots, alpha = 4096, .001\n", "epsilon = np.sqrt(np.log(2*len(tested_angles)/alpha)/(2*shots))\n", "for index, theta in enumerate(tested_angles):\n", " circuit = QuantumCircuit(1)\n", " circuit.ry(theta, 0)\n", " circuit.measure_all()\n", " counts = StatevectorSampler(seed=SEED+index).run([circuit], shots=shots).result()[0].data.meas.get_counts()\n", " estimate = counts.get(\"1\", 0)/shots\n", " target = np.sin(theta/2)**2\n", " assert abs(estimate-target) <= epsilon\n", " print(f\"theta={theta:.2f}, target={target:.4f}, estimate={estimate:.4f}, tolerance={epsilon:.4f}\")\n", "# Deliberate wrong-angle mutation: an observable change much larger than tolerance.\n", "mutant = QuantumCircuit(1)\n", "mutant.ry(.2, 0) # Reference specification below requires 1.2.\n", "mutant.measure_all()\n", "counts = StatevectorSampler(seed=SEED).run([mutant], shots=shots).result()[0].data.meas.get_counts()\n", "mutant_estimate = counts.get(\"1\", 0)/shots\n", "mutation_detected = abs(mutant_estimate-np.sin(1.2/2)**2) > epsilon\n", "assert mutation_detected\n", "print(\"Wrong-angle mutation caught:\", mutation_detected)" ] }, { "cell_type": "markdown", "id": "f6b1b4d3", "metadata": {}, "source": [ "## Test suite summary and practice\n", "\n", "| Intended behavior | Strong small-scale check | Example defect caught |\n", "|---|---|---|\n", "| Prepare a target pure state | State equality up to global phase | Missing relative phase |\n", "| Implement a two-qubit unitary | Full unitary or all basis mappings | Reversed control/target |\n", "| Respect inverse relation | Circuit followed by inverse | Wrong sign or gate order |\n", "| Match a measured probability | Predeclared statistical bound | Wrong rotation angle |\n", "\n", "**Checkpoint:** should multiplying all amplitudes by $i$ fail the state test?\n", "**No:** it is a global phase. Multiplying only one nonzero amplitude by $i$\n", "generally changes relative phase and should be tested.\n", "\n", "1. Add a reversed-gate-order mutation and explain which property catches it.\n", "2. Write the failing test before repairing a missing Z.\n", "3. Reduce the shot budget. Report how the detectable error and tolerance change.\n", "4. Explain why testing only the initial state $|00\\rangle$ is weaker than verifying\n", " a two-qubit unitary, and why full-unitary testing stops scaling.\n", "\n", "**Takeaway:** evaluate a suite with realistic mutations; a passing test that also\n", "accepts the defect provides little evidence for the property you care about." ] }, { "cell_type": "markdown", "id": "66094654", "metadata": {}, "source": [ "## Sources\n", "\n", "- [IBM: Statevector API](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.quantum_info.Statevector)\n", "- [IBM: Operator API](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.quantum_info.Operator)\n", "- [IBM: bit ordering](https://quantum.cloud.ibm.com/docs/en/guides/bit-ordering)\n", "- [Hoeffding: Probability inequalities for sums of bounded random variables](https://doi.org/10.1080/01621459.1963.10500830)" ] } ], "metadata": { "kernelspec": { "display_name": "Quantum book (pinned Python)", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.13.2" }, "quantum_book": { "chapters": [ "P11", "P12", "P13", "P14", "P15", "P24" ], "expected_packages": { "qiskit": "2.6.1", "qiskit-aer": "0.18.2", "qiskit-ibm-runtime": "0.51.0" }, "level": "Undergraduate", "network_on_default_run": false, "verified_release_date": "2026-11-06" } }, "nbformat": 4, "nbformat_minor": 5 }