From 3b5321b0b3532eb482ad6302cc45257154e9db14 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 2 Oct 2026 14:48:58 -0500 Subject: [PATCH] Add Quantinuum quickstart notebook --- qBraid-Runtime/README.md | 1 + qBraid-Runtime/quantinuum_quickstart.ipynb | 541 +++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 qBraid-Runtime/quantinuum_quickstart.ipynb diff --git a/qBraid-Runtime/README.md b/qBraid-Runtime/README.md index a4f8d29..4190ca3 100644 --- a/qBraid-Runtime/README.md +++ b/qBraid-Runtime/README.md @@ -10,5 +10,6 @@ Notebooks demonstrating [qBraid Runtime](https://docs.qbraid.com/runtime) featur | [qbraid_pulser_pasqal.ipynb](qbraid_pulser_pasqal.ipynb) | Analog quantum computing with Pulser on Pasqal devices | | [qbraid_runtime_bloqade_aquila.ipynb](qbraid_runtime_bloqade_aquila.ipynb) | Bloqade programs on QuEra Aquila via qBraid Runtime | | [qbraid_runtime_qbraid_provider_aws_devices.ipynb](qbraid_runtime_qbraid_provider_aws_devices.ipynb) | Accessing AWS devices through the QbraidProvider | +| [quantinuum_quickstart.ipynb](quantinuum_quickstart.ipynb) | Quantinuum's H1/H2 emulators: noiseless vs. noisy, compiler optimisation levels, mid-circuit measurement | | [quobly_alloy_forge_quickstart.ipynb](quobly_alloy_forge_quickstart.ipynb) | Quobly's silicon spin-qubit emulator: noiseless vs. the Pioneer hardware noise model | | [quobly_ghz_noise_benchmark.ipynb](quobly_ghz_noise_benchmark.ipynb) | Growing a GHZ chain along Quobly's linear coupling map to measure noise accumulation | diff --git a/qBraid-Runtime/quantinuum_quickstart.ipynb b/qBraid-Runtime/quantinuum_quickstart.ipynb new file mode 100644 index 0000000..6bbcea9 --- /dev/null +++ b/qBraid-Runtime/quantinuum_quickstart.ipynb @@ -0,0 +1,541 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# qbraid.runtime.quantinuum\n", + "\n", + "> **Billing notice:** the Quantinuum emulators on qBraid are currently **free to run** (0 credits). You need a qBraid API key, but no Quantinuum or NEXUS account.\n", + "\n", + "[Quantinuum](https://www.quantinuum.com)'s System Model H1 and H2 are **trapped-ion** quantum computers: all-to-all qubit connectivity, high-fidelity gates, and native support for measuring and resetting qubits partway through a circuit. qBraid gives you four of Quantinuum's [NEXUS](https://nexus.quantinuum.com)-hosted emulators of those machines:\n", + "\n", + "| Device ID | Qubits | Noise |\n", + "|---|---|---|\n", + "| `quantinuum:quantinuum:sim:h1-1le` | 20 | none (ideal statevector) |\n", + "| `quantinuum:quantinuum:sim:h2-1le` | 26 | none (ideal statevector) |\n", + "| `quantinuum:quantinuum:sim:h1-emulator` | 20 | H1 hardware noise model |\n", + "| `quantinuum:quantinuum:sim:h2-emulator` | 26 | H2 hardware noise model |\n", + "\n", + "The **LE** targets are noiseless: the only randomness in the counts is shot noise, which makes them the place to check that a circuit is *correct*. The **Emulator** targets add Quantinuum's model of the real device's errors, so they show what a circuit would look like on hardware.\n", + "\n", + "This notebook goes through the qBraid platform with `QbraidProvider`. If you have your own Quantinuum NEXUS credentials, `QuantinuumProvider` talks to NEXUS directly instead; see [`Provider-Jobs/qbraid_runtime_quantinuum_provider.ipynb`](../Provider-Jobs/qbraid_runtime_quantinuum_provider.ipynb).\n", + "\n", + "It covers:\n", + "\n", + "1. Connecting and running a first circuit\n", + "2. Inspecting the program Quantinuum's compiler actually ran\n", + "3. The `optimisation_level` runtime option\n", + "4. Noiseless vs. noisy emulation\n", + "5. Mid-circuit measurement and reset\n", + "6. Running several circuits" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "\n", + "# \"%%capture\" hides the install output; comment it out if you need to debug this step.\n", + "%pip install 'qbraid[qiskit,visualization]'" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Connect to the device\n", + "\n", + "`QbraidProvider` picks up your API key from `~/.qbraid/qbraidrc` or the `QBRAID_API_KEY`\n", + "environment variable. Inside qBraid Lab it is already configured for you." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "from qbraid.runtime import QbraidProvider\n", + "\n", + "provider = QbraidProvider()\n", + "device = provider.get_device(\"quantinuum:quantinuum:sim:h2-1le\")\n", + "\n", + "device.metadata()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "26 qubits, `ONLINE`, and zero pricing. The device takes OpenQASM 2, and qBraid transpiles to\n", + "it for you, so you can pass a Qiskit, Cirq, Braket, pyQuil, or pytket circuit straight to\n", + "`run()`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "device.supported_run_inputs()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## A first run\n", + "\n", + "A three-qubit GHZ state, written in Qiskit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "\n", + "def ghz(num_qubits: int) -> QuantumCircuit:\n", + " circuit = QuantumCircuit(num_qubits)\n", + " circuit.h(0)\n", + " for qubit in range(num_qubits - 1):\n", + " circuit.cx(qubit, qubit + 1)\n", + " circuit.measure_all()\n", + " return circuit\n", + "\n", + "\n", + "circuit = ghz(3)\n", + "circuit.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "Every NEXUS job runs in stages: the circuit is uploaded, **compiled** for the target, and then\n", + "executed. Even a tiny circuit usually takes a minute or two from submission to result, so\n", + "wait with a timeout rather than blocking indefinitely:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "def get_counts(job, timeout: int = 600) -> dict[str, int]:\n", + " job.wait_for_final_state(timeout=timeout, poll_interval=5)\n", + " return job.result().data.get_counts()\n", + "\n", + "\n", + "job = device.run(circuit, shots=200)\n", + "get_counts(job)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Two peaks, `000` and `111`, as a GHZ state should give. Keys are little-endian: **qubit 0 is\n", + "the rightmost bit**.\n", + "\n", + "## What actually ran\n", + "\n", + "Before execution, NEXUS compiles the circuit into the trapped-ion **native gate set**:\n", + "single-qubit rotations `U1q` and `rz`, and a two-qubit `RZZ` (ZZ) interaction. Your `CX`\n", + "gates don't exist on the hardware. `compiled_program()` returns the program that was really\n", + "executed, as OpenQASM 2 in Quantinuum's `hqslib1` dialect:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "compiled = job.compiled_program()\n", + "\n", + "print(compiled.format)\n", + "print(compiled.data)" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "The two `CX` gates became two `RZZ` gates plus single-qubit rotations around them.\n", + "\n", + "## Runtime options\n", + "\n", + "Settings go in a **`runtime_options` dict**:\n", + "\n", + "```python\n", + "device.run(circuit, shots=200, runtime_options={\"optimisation_level\": 2})\n", + "```\n", + "\n", + "The Quantinuum integration supports one option:\n", + "\n", + "| Option | What it does |\n", + "|---|---|\n", + "| `optimisation_level` | How hard the NEXUS compiler works to simplify the circuit: an integer from `0` (rebase to native gates only) to `3`. Default `1` |\n", + "\n", + "Anything else in `runtime_options` is **ignored**, without an error. In particular there is no\n", + "`seed`: sampling is never reproducible across jobs." + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "## Choosing an optimisation level\n", + "\n", + "Here is a circuit with a lot of redundancy: two Hadamards that cancel, two `CX` gates that\n", + "cancel, and two `RZ` rotations that cancel. All that is left is a Bell pair." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "redundant = QuantumCircuit(2)\n", + "redundant.h(0)\n", + "redundant.h(0)\n", + "redundant.cx(0, 1)\n", + "redundant.cx(0, 1)\n", + "redundant.rz(0.3, 1)\n", + "redundant.rz(-0.3, 1)\n", + "redundant.h(0)\n", + "redundant.cx(0, 1)\n", + "redundant.measure_all()\n", + "\n", + "redundant.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "Submit it at levels 0, 1 and 2. The jobs run in parallel on NEXUS, so submit all three\n", + "before waiting on any:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "opt_jobs = {\n", + " level: device.run(redundant, shots=200, runtime_options={\"optimisation_level\": level})\n", + " for level in (0, 1, 2)\n", + "}\n", + "\n", + "for level, opt_job in opt_jobs.items():\n", + " counts = get_counts(opt_job)\n", + " two_qubit_gates = opt_job.compiled_program().data.count(\"RZZ\")\n", + " print(f\"level {level}: two-qubit gates = {two_qubit_gates}, counts {counts}\")" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "Same answer at every level, since the LE emulator is noiseless. The difference is in the\n", + "compiled program: level 0 keeps all three `CX` gates as three `RZZ` gates, and level 2 cuts\n", + "them down to the single one the Bell pair needs.\n", + "\n", + "That matters off the noiseless emulator. Two-qubit gates are the main source of error on\n", + "trapped-ion hardware, and the main driver of a hardware job's cost in HQCs (Quantinuum's\n", + "billing unit), so fewer of them means both cleaner results and a cheaper run.\n", + "\n", + "The compiler validates the level. An out-of-range value fails the job before anything runs,\n", + "and the reason is on the job's status, in `status_message`, which is the first place to look\n", + "when a job fails:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "bad_job = device.run(redundant, shots=10, runtime_options={\"optimisation_level\": 5})\n", + "bad_job.wait_for_final_state(timeout=600, poll_interval=5)\n", + "\n", + "status = bad_job.status()\n", + "print(status)\n", + "print(status.status_message)" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Noiseless vs. noisy\n", + "\n", + "Now an eight-qubit GHZ state, on the noiseless H2 emulator and on the H2 emulator with the\n", + "hardware noise model. Ideally every shot reads all zeros or all ones; anything else is an\n", + "error." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "noisy_device = provider.get_device(\"quantinuum:quantinuum:sim:h2-emulator\")\n", + "\n", + "ghz8 = ghz(8)\n", + "ideal_job = device.run(ghz8, shots=200)\n", + "noisy_job = noisy_device.run(ghz8, shots=200)\n", + "\n", + "for name, ghz_job in [(\"H2-1LE\", ideal_job), (\"H2-Emulator\", noisy_job)]:\n", + " counts = get_counts(ghz_job)\n", + " good = counts.get(\"0\" * 8, 0) + counts.get(\"1\" * 8, 0)\n", + " print(f\"{name:12} {good / sum(counts.values()):.1%} of shots in 00000000 or 11111111\")" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "The noiseless emulator only ever returns the two ideal outcomes. On the noisy emulator a few\n", + "shots leak into other bitstrings. The emulator models the device's gate, measurement,\n", + "state-preparation, and idling errors, so the more gates and qubits a circuit uses, the more\n", + "of these you see.\n", + "\n", + "Develop and debug on an LE target, then check on the matching Emulator target how the circuit\n", + "holds up under realistic noise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "from qbraid.visualization import plot_histogram\n", + "\n", + "plot_histogram(get_counts(noisy_job))" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "## Mid-circuit measurement and reset\n", + "\n", + "Trapped-ion qubits can be measured and reset **partway through** a circuit, then reused.\n", + "Here qubit 0 is flipped to 1, measured, reset to 0, and reused to build a Bell pair with\n", + "qubit 1:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import ClassicalRegister, QuantumRegister\n", + "\n", + "qubits = QuantumRegister(2, \"q\")\n", + "bits = ClassicalRegister(3, \"c\")\n", + "reuse = QuantumCircuit(qubits, bits)\n", + "\n", + "reuse.x(0)\n", + "reuse.measure(0, 0) # always 1\n", + "reuse.reset(0)\n", + "reuse.h(0)\n", + "reuse.cx(0, 1)\n", + "reuse.measure(0, 1)\n", + "reuse.measure(1, 2)\n", + "\n", + "reuse.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "get_counts(device.run(reuse, shots=200))" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "The first measurement (rightmost bit) is always `1`, and the Bell pair after the reset gives\n", + "`00` or `11` on the other two bits, so only `001` and `111` appear.\n", + "\n", + "## Running several circuits\n", + "\n", + "The Quantinuum devices don't support **batch jobs** (several circuits in one job), so\n", + "`as_batch=True` is rejected before anything is submitted:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"batch support:\", device.profile.batch_job_support)\n", + "\n", + "try:\n", + " device.run([ghz(2), ghz(3)], shots=100, as_batch=True)\n", + "except ValueError as err:\n", + " print(\"ValueError:\", err)" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "Pass a list of circuits **without** `as_batch` and each one is submitted as a **separate job**;\n", + "`run()` returns a list of jobs, in order. They run in parallel on NEXUS, so a sweep takes\n", + "roughly as long as its slowest circuit, not the sum.\n", + "\n", + "A natural use is a parameter sweep. Rotate one qubit by $\\theta$ with `RY` and the\n", + "probability of measuring 1 is $\\sin^2(\\theta/2)$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "angles = np.linspace(0, np.pi, 6)\n", + "\n", + "sweep = []\n", + "for theta in angles:\n", + " circuit = QuantumCircuit(1)\n", + " circuit.ry(theta, 0)\n", + " circuit.measure_all()\n", + " sweep.append(circuit)\n", + "\n", + "sweep_jobs = device.run(sweep, shots=200)\n", + "print(len(sweep_jobs), \"jobs\")\n", + "\n", + "sweep_counts = [get_counts(sweep_job) for sweep_job in sweep_jobs]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "measured = [counts.get(\"1\", 0) / sum(counts.values()) for counts in sweep_counts]\n", + "\n", + "fine = np.linspace(0, np.pi, 200)\n", + "plt.plot(fine, np.sin(fine / 2) ** 2, label=r\"$\\sin^2(\\theta/2)$\")\n", + "plt.plot(angles, measured, \"o\", label=\"H2-1LE, one job per angle\")\n", + "plt.xlabel(r\"$\\theta$\")\n", + "plt.ylabel(\"P(1)\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "Each point is its own job with its own ID, status, and independent sampling.\n", + "\n", + "## Summary\n", + "\n", + "- Four Quantinuum emulators are available through `QbraidProvider`, free of charge: `h1-1le` and `h2-1le` are noiseless, `h1-emulator` and `h2-emulator` model the hardware's noise.\n", + "- NEXUS compiles every circuit to the trapped-ion native gates; `job.compiled_program()` shows what ran.\n", + "- The one runtime option is `optimisation_level` (`0` to `3`, default `1`), which trades compile effort for fewer two-qubit gates. Other options are ignored, and there is no `seed`.\n", + "- Mid-circuit measurement and qubit reset work as on the hardware.\n", + "- There are no batch jobs: a list of circuits runs as separate, parallel jobs.\n", + "- When a job fails, `job.status().status_message` says why." + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "
\n", + "Copyright Notice: \n", + " All rights reserved © [2026] qBraid. This notebook is part of the qBraid-Lab-Demo repository.\n", + "The qBraid-Lab-Demo is licensed under the Apache License, Version 2.0.\n", + "You may obtain a copy of the License at .\n", + "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "
" + ] + } + ], + "metadata": { + "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.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}