From 5f23b746496be7236ba551e4ba146186436577a3 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 25 Mar 2026 11:55:46 -0700 Subject: [PATCH 1/9] Added neutral atom demo notebook --- .../python_interop/neutral_atom_demo.ipynb | 468 ++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 samples/python_interop/neutral_atom_demo.ipynb diff --git a/samples/python_interop/neutral_atom_demo.ipynb b/samples/python_interop/neutral_atom_demo.ipynb new file mode 100644 index 0000000000..f24fd3af6f --- /dev/null +++ b/samples/python_interop/neutral_atom_demo.ipynb @@ -0,0 +1,468 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dd6f5b56", + "metadata": {}, + "source": [ + "# Local Neutral Atom Simulator Demo\n", + "## Qiskit & Cirq submission with qubit loss noise\n", + "\n", + "This notebook demonstrates how to run quantum circuits on the local **NeutralAtomDevice** simulator using both Qiskit and Cirq.\n", + "\n", + "Key features shown:\n", + "- Native gate decomposition (`Rz`, `SX`, `CZ`)\n", + "- Qubit loss noise modelling via `NoiseConfig`\n", + "- Separation of **accepted shots** (clean `{0,1}` results) from **raw shots** (includes qubit loss markers)\n", + "- Side-by-side histogram visualizations for both frameworks\n" + ] + }, + { + "cell_type": "markdown", + "id": "0e464004", + "metadata": {}, + "source": [ + "## 1. Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d963721", + "metadata": {}, + "outputs": [], + "source": [ + "# Qiskit\n", + "from qiskit import QuantumCircuit, transpile\n", + "from qiskit.visualization import plot_histogram\n", + "\n", + "# Qiskit neutral-atom backend\n", + "from qdk.qiskit import NeutralAtomBackend\n", + "\n", + "# Cirq\n", + "import cirq\n", + "\n", + "# Cirq neutral-atom sampler\n", + "from qdk.cirq import NeutralAtomSampler\n", + "\n", + "# Shared noise configuration\n", + "from qdk.simulation import NoiseConfig\n", + "\n", + "# Plotting\n", + "import matplotlib.pyplot as plt\n", + "from collections import Counter\n", + "\n", + "print(\"Imports successful.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "68a4e541", + "metadata": {}, + "source": [ + "## 2. Build the GHZ Circuit (Qiskit)\n", + "\n", + "We construct a 3-qubit GHZ state circuit and then **transpile** it to the native gate set\n", + "supported by the NeutralAtomDevice: `{Rz, SX, CZ}`.\n", + "\n", + "Transpiling first lets us run with `skip_transpilation=True`, which bypasses the backend's\n", + "own transpile step and gives us full control over the decomposition.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71696166", + "metadata": {}, + "outputs": [], + "source": [ + "n_qubits = 3\n", + "\n", + "# High-level GHZ circuit\n", + "ghz_qiskit = QuantumCircuit(n_qubits, n_qubits)\n", + "ghz_qiskit.h(0)\n", + "for i in range(n_qubits - 1):\n", + " ghz_qiskit.cx(i, i + 1)\n", + "ghz_qiskit.measure(range(n_qubits), range(n_qubits))\n", + "\n", + "print(\"High-level circuit:\")\n", + "print(ghz_qiskit.draw())\n", + "\n", + "# Decompose to native gate set {Rz, SX, CZ}\n", + "backend = NeutralAtomBackend()\n", + "native_qiskit = transpile(\n", + " ghz_qiskit,\n", + " backend=backend\n", + ")\n", + "\n", + "print(\"\\nNative gate circuit:\")\n", + "print(native_qiskit.draw())\n" + ] + }, + { + "cell_type": "markdown", + "id": "3cb69bbe", + "metadata": {}, + "source": [ + "## 3. Noiseless Simulation — Qiskit\n", + "\n", + "Run the native circuit on the local simulator with **no noise**.\n", + "We expect to see only the two ideal GHZ outcomes: `000` and `111`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3009cd24", + "metadata": {}, + "outputs": [], + "source": [ + "SHOTS = 500\n", + "SEED = 42\n", + "\n", + "job_noiseless = backend.run(\n", + " native_qiskit,\n", + " shots=SHOTS,\n", + " seed=SEED,\n", + " skip_transpilation=True,\n", + ")\n", + "result_noiseless = job_noiseless.result()\n", + "data_noiseless = result_noiseless.results[0].data\n", + "\n", + "print(\"Noiseless counts:\", dict(data_noiseless.counts))\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "plot_histogram(dict(data_noiseless.counts), ax=ax)\n", + "ax.set_title(\"Qiskit — Noiseless GHZ\")\n", + "ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + "ax.set_axisbelow(True)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "58bfb69d", + "metadata": {}, + "source": [ + "## 4. Noisy Simulation — Qiskit (8% Rz qubit loss)\n", + "\n", + "Configure an 8% qubit-loss probability on `Rz` gates and re-run.\n", + "\n", + "The `H` gate decomposes to `Rz` gates in the native set, so every qubit passes through\n", + "at least one `Rz` and has a chance of being lost.\n", + "\n", + "**Accepted shots** (`counts`) exclude any shot where at least one qubit was lost. \n", + "**Raw shots** (`raw_counts`) include all shots; lost qubits appear as `\"-\"` in the bitstring.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b901ce8b", + "metadata": {}, + "outputs": [], + "source": [ + "noise = NoiseConfig()\n", + "noise.rz.loss = 0.08 # 8% qubit-loss probability per Rz gate\n", + "\n", + "job_noisy = backend.run(\n", + " native_qiskit,\n", + " shots=SHOTS,\n", + " noise=noise,\n", + " seed=SEED,\n", + " skip_transpilation=True,\n", + ")\n", + "result_noisy = job_noisy.result()\n", + "data_noisy = result_noisy.results[0].data\n", + "\n", + "accepted = dict(data_noisy.counts)\n", + "raw = dict(data_noisy.raw_counts)\n", + "\n", + "total_accepted = sum(accepted.values())\n", + "total_raw = sum(raw.values())\n", + "lost = total_raw - total_accepted\n", + "\n", + "print(f\"Total shots : {total_raw}\")\n", + "print(f\"Accepted : {total_accepted} ({100*total_accepted/total_raw:.1f}%)\")\n", + "print(f\"Lost : {lost} ({100*lost/total_raw:.1f}%)\")\n", + "print()\n", + "print(\"Accepted counts:\", accepted)\n", + "print(\"Raw counts :\", raw)\n" + ] + }, + { + "cell_type": "markdown", + "id": "5a2e0164", + "metadata": {}, + "source": [ + "## 5. Visualize Qiskit Results — Filtered vs Raw Histograms\n", + "\n", + "Side-by-side comparison showing the accepted (clean) distribution on the left and the full\n", + "raw distribution (including loss bitstrings) on the right.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b06d4fd", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", + "\n", + "plot_histogram(accepted, ax=axes[0])\n", + "plot_histogram(raw, ax=axes[1])\n", + "\n", + "titles = [\n", + " \"Qiskit — Accepted shots (loss filtered out)\",\n", + " \"Qiskit — Raw shots (loss bitstrings included)\",\n", + "]\n", + "for ax, title in zip(axes, titles):\n", + " ax.set_title(title)\n", + " ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + " ax.set_axisbelow(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "723aa403", + "metadata": {}, + "source": [ + "## 6. Build the GHZ Circuit (Cirq)\n", + "\n", + "Cirq circuits work directly with `cirq.LineQubit` objects. \n", + "The `NeutralAtomSampler` accepts standard Cirq circuits and internally converts them to\n", + "OpenQASM 3.0 before simulating.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3490b0a7", + "metadata": {}, + "outputs": [], + "source": [ + "qubits = cirq.LineQubit.range(n_qubits)\n", + "\n", + "ghz_cirq = cirq.Circuit(\n", + " cirq.H(qubits[0]),\n", + " *[cirq.CNOT(qubits[i], qubits[i + 1]) for i in range(n_qubits - 1)],\n", + " cirq.measure(*qubits, key=\"result\"),\n", + ")\n", + "\n", + "print(ghz_cirq)\n" + ] + }, + { + "cell_type": "markdown", + "id": "2b8badcb", + "metadata": {}, + "source": [ + "## 7. Noiseless Simulation — Cirq\n", + "\n", + "Run with no noise and confirm only `000` and `111` appear.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fe63a0c", + "metadata": {}, + "outputs": [], + "source": [ + "sampler = NeutralAtomSampler()\n", + "\n", + "cirq_result_noiseless = sampler.run(ghz_cirq, repetitions=SHOTS)\n", + "\n", + "# histogram() returns a Counter of integer bitmask → count\n", + "cirq_noiseless_counts = dict(cirq_result_noiseless.histogram(key=\"result\"))\n", + "\n", + "# Format integer keys as binary bitstrings for readability\n", + "cirq_noiseless_str = {format(k, f\"0{n_qubits}b\"): v for k, v in cirq_noiseless_counts.items()}\n", + "print(\"Noiseless Cirq counts:\", cirq_noiseless_str)\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "bars = ax.bar(cirq_noiseless_str.keys(), cirq_noiseless_str.values())\n", + "ax.set_title(\"Cirq — Noiseless GHZ\")\n", + "ax.set_xlabel(\"Outcome\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + "ax.set_axisbelow(True)\n", + "for bar in bars:\n", + " ax.text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height(),\n", + " str(int(bar.get_height())),\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=9,\n", + " )\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "21d1679b", + "metadata": {}, + "source": [ + "## 8. Noisy Simulation — Cirq (8% Rz qubit loss)\n", + "\n", + "The same `NoiseConfig` is passed to the Cirq sampler.\n", + "\n", + "- **`result.measurements[\"result\"]`** — accepted shots only (int8 array, no loss rows)\n", + "- **`result.raw_measurements()[\"result\"]`** — all shots; lost qubits are `\"-\"` strings\n", + "- **`result.raw_shots`** — raw simulator output as a list of `Result` enum tuples\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03d72b07", + "metadata": {}, + "outputs": [], + "source": [ + "noisy_sampler = NeutralAtomSampler(noise=noise, seed=SEED)\n", + "\n", + "cirq_result_noisy = noisy_sampler.run(ghz_cirq, repetitions=SHOTS)\n", + "\n", + "# Accepted measurements: int8 2-D array, shape = (accepted_shots, n_qubits)\n", + "accepted_arr = cirq_result_noisy.measurements[\"result\"]\n", + "\n", + "# Raw measurements: str 2-D array, shape = (total_shots, n_qubits), \"-\" for loss\n", + "raw_arr = cirq_result_noisy.raw_measurements()[\"result\"]\n", + "\n", + "cirq_accepted = sum(1 for _ in accepted_arr) # accepted_arr.shape[0]\n", + "cirq_total = raw_arr.shape[0]\n", + "cirq_lost = cirq_total - cirq_accepted\n", + "\n", + "print(f\"Total shots : {cirq_total}\")\n", + "print(f\"Accepted : {cirq_accepted} ({100*cirq_accepted/cirq_total:.1f}%)\")\n", + "print(f\"Lost : {cirq_lost} ({100*cirq_lost/cirq_total:.1f}%)\")\n", + "\n", + "# Build string-keyed counters from the raw array rows\n", + "def arr_to_counts(arr):\n", + " return Counter(\"\".join(row) for row in arr)\n", + "\n", + "cirq_accepted_counts = arr_to_counts(accepted_arr.astype(str))\n", + "cirq_raw_counts = arr_to_counts(raw_arr)\n", + "\n", + "print(\"\\nAccepted counts:\", dict(cirq_accepted_counts))\n", + "print(\"Raw counts :\", dict(cirq_raw_counts))\n" + ] + }, + { + "cell_type": "markdown", + "id": "a4c1a155", + "metadata": {}, + "source": [ + "## 9. Visualize Cirq Results — Filtered vs Raw Histograms" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a11edf92", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_counts(ax, counts, title):\n", + " labels = sorted(counts.keys())\n", + " values = [counts[k] for k in labels]\n", + " bars = ax.bar(labels, values)\n", + " ax.set_title(title)\n", + " ax.set_xlabel(\"Outcome\")\n", + " ax.set_ylabel(\"Count\")\n", + " ax.tick_params(axis=\"x\", rotation=45)\n", + " # Dashed horizontal gridlines\n", + " ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + " ax.set_axisbelow(True)\n", + " # Count labels above each bar\n", + " for bar, value in zip(bars, values):\n", + " ax.text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height(),\n", + " str(value),\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=9,\n", + " )\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", + "plot_counts(axes[0], dict(cirq_accepted_counts), \"Cirq — Accepted shots (loss filtered out)\")\n", + "plot_counts(axes[1], dict(cirq_raw_counts), \"Cirq — Raw shots (loss bitstrings included)\")\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "5d92825d", + "metadata": {}, + "source": [ + "## 10. Side-by-Side Comparison — Qiskit vs Cirq\n", + "\n", + "A 2×2 subplot grid comparing all four scenarios:\n", + "\n", + "| | Accepted (filtered) | Raw (with loss) |\n", + "|---|---|---|\n", + "| **Qiskit** | top-left | top-right |\n", + "| **Cirq** | bottom-left | bottom-right |\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13559f71", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(2, 2, figsize=(16, 8))\n", + "fig.suptitle(\n", + " f\"NeutralAtomDevice — GHZ ({n_qubits} qubits, {SHOTS} shots, 8% Rz loss)\",\n", + " fontsize=14,\n", + " fontweight=\"bold\",\n", + ")\n", + "\n", + "# Row 0: Qiskit\n", + "plot_counts(axes[0, 0], accepted, \"Qiskit — Accepted\")\n", + "plot_counts(axes[0, 1], raw, \"Qiskit — Raw (loss included)\")\n", + "\n", + "# Row 1: Cirq\n", + "plot_counts(axes[1, 0], dict(cirq_accepted_counts), \"Cirq — Accepted\")\n", + "plot_counts(axes[1, 1], dict(cirq_raw_counts), \"Cirq — Raw (loss included)\")\n", + "\n", + "# Add framework labels on the left\n", + "for ax, label in zip(axes[:, 0], [\"Qiskit\", \"Cirq\"]):\n", + " ax.set_ylabel(f\"{label}\\nCount\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From cbef0b89bb0d6475e6eeff90302e905a6205e424 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 10:42:38 -0700 Subject: [PATCH 2/9] Manual Submission notebook --- .../python_interop/neutral_atom_demo.ipynb | 16 +- .../pennylane_submission_to_azure.ipynb | 16 +- .../qir_circuit_submission_to_azure.ipynb | 292 ++++++++++++++++++ ...ipynb => qiskit_submission_to_azure.ipynb} | 74 +---- 4 files changed, 300 insertions(+), 98 deletions(-) create mode 100644 samples/python_interop/qir_circuit_submission_to_azure.ipynb rename samples/python_interop/{submit_qiskit_circuit_to_azure.ipynb => qiskit_submission_to_azure.ipynb} (67%) diff --git a/samples/python_interop/neutral_atom_demo.ipynb b/samples/python_interop/neutral_atom_demo.ipynb index f24fd3af6f..28fee03eff 100644 --- a/samples/python_interop/neutral_atom_demo.ipynb +++ b/samples/python_interop/neutral_atom_demo.ipynb @@ -445,22 +445,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": ".venv", - "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.13.12" + "name": "python" } }, "nbformat": 4, diff --git a/samples/python_interop/pennylane_submission_to_azure.ipynb b/samples/python_interop/pennylane_submission_to_azure.ipynb index c3fd61e198..c9b1275df7 100644 --- a/samples/python_interop/pennylane_submission_to_azure.ipynb +++ b/samples/python_interop/pennylane_submission_to_azure.ipynb @@ -178,22 +178,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": ".venv", - "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.13.8" + "name": "python" } }, "nbformat": 4, diff --git a/samples/python_interop/qir_circuit_submission_to_azure.ipynb b/samples/python_interop/qir_circuit_submission_to_azure.ipynb new file mode 100644 index 0000000000..95f5781cbb --- /dev/null +++ b/samples/python_interop/qir_circuit_submission_to_azure.ipynb @@ -0,0 +1,292 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b1c8553d", + "metadata": {}, + "source": [ + "# Manual QIR Circuit Submission to Azure Quantum\n", + "\n", + "This notebook demonstrates how to submit quantum circuits to Azure Quantum via an explicit **OpenQASM 3 → QIR** compilation path, covering both **Qiskit** and **Cirq** circuits.\n", + "\n", + "This approach gives full visibility and control over intermediate compilation artifacts. It is useful when you need to inspect or modify OpenQASM or QIR before submission, target backends that require raw QIR, or share a single submission pipeline across frameworks.\n", + "\n", + "For the recommended getting-started experience for each framework, see:\n", + "- `submit_qiskit_circuit_to_azure.ipynb`\n", + "- `cirq_submission_to_azure.ipynb`" + ] + }, + { + "cell_type": "markdown", + "id": "906a9b09", + "metadata": {}, + "source": [ + "The same compilation pipeline works for both Qiskit and Cirq circuits:\n", + "\n", + "1. Build a circuit in Qiskit or Cirq.\n", + "2. Export it to **OpenQASM 3** text — this intermediate representation can be inspected or modified.\n", + "3. Compile the OpenQASM 3 source to **QIR** with `qdk.openqasm.compile` (choose a `TargetProfile`).\n", + "4. Connect to an Azure Quantum workspace with `qdk.azure.Workspace`.\n", + "5. Select a target (e.g. a simulator such as `rigetti.sim.qvm`).\n", + "6. Submit the QIR payload (`target.submit(qir, job_name, shots)`) and retrieve results (`job.get_results()`)." + ] + }, + { + "cell_type": "markdown", + "id": "243e083e", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Install the `qdk` package with `azure` and whichever framework extras you need. Install both if you plan to run both sections of this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afb12537", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install \"qdk[azure,cirq,qiskit]\"" + ] + }, + { + "cell_type": "markdown", + "id": "83d2907f", + "metadata": {}, + "source": [ + "After installing, restart the notebook kernel if it was already running. Verify installation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e3b8778", + "metadata": {}, + "outputs": [], + "source": [ + "import cirq, qiskit, qdk, qdk.azure # should import without errors" + ] + }, + { + "cell_type": "markdown", + "id": "4c503ee7", + "metadata": {}, + "source": [ + "## Configure Azure Quantum workspace connection\n", + "\n", + "Replace the placeholder values with your own workspace details. These are shared across both the Qiskit and Cirq examples below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20dc0946", + "metadata": {}, + "outputs": [], + "source": [ + "subscription_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'\n", + "resource_group = 'myresourcegroup'\n", + "workspace_name = 'myworkspace'\n", + "location = 'westus'\n", + "\n", + "# Replace with any QIR-capable target name from your workspace\n", + "target_name = \"rigetti.sim.qvm\"" + ] + }, + { + "cell_type": "markdown", + "id": "bbee43f6", + "metadata": {}, + "source": [ + "---\n", + "## Part A: Submitting a Qiskit circuit\n", + "\n", + "### Build a Qiskit circuit\n", + "\n", + "We use a simple two-qubit superposition circuit to demonstrate the Qiskit path." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d0e8013", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "qiskit_circuit = QuantumCircuit(2, 2)\n", + "qiskit_circuit.h(0)\n", + "qiskit_circuit.measure(0, 0)\n", + "qiskit_circuit.h(1)\n", + "qiskit_circuit.measure(1, 1)\n", + "\n", + "qiskit_circuit.draw(output=\"text\")" + ] + }, + { + "cell_type": "markdown", + "id": "f5848792", + "metadata": {}, + "source": [ + "### Export to OpenQASM 3\n", + "\n", + "`qiskit.qasm3.dumps` converts a Qiskit circuit to an OpenQASM 3 string. This intermediate text can be inspected or modified before compilation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85413498", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import qasm3\n", + "\n", + "qasm3_str_qiskit = qasm3.dumps(qiskit_circuit)\n", + "print(qasm3_str_qiskit)" + ] + }, + { + "cell_type": "markdown", + "id": "43904206", + "metadata": {}, + "source": [ + "### Compile to QIR and submit\n", + "\n", + "Compile the OpenQASM 3 source to QIR, then connect to the workspace and submit the payload." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6f32b4c", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk.openqasm import compile\n", + "from qdk import TargetProfile\n", + "from qdk.azure import Workspace\n", + "\n", + "qir_qiskit = compile(qasm3_str_qiskit, target_profile=TargetProfile.Base)\n", + "\n", + "workspace = Workspace(\n", + " subscription_id=subscription_id,\n", + " resource_group=resource_group,\n", + " name=workspace_name,\n", + " location=location,\n", + ")\n", + "\n", + "target = workspace.get_targets(target_name)\n", + "job = target.submit(qir_qiskit, \"qiskit-manual-job\", shots=100)\n", + "print(\"Job submitted. Waiting for results...\")\n", + "\n", + "results = job.get_results()\n", + "print(results)" + ] + }, + { + "cell_type": "markdown", + "id": "c580ff95", + "metadata": {}, + "source": [ + "---\n", + "## Part B: Submitting a Cirq circuit\n", + "\n", + "### Build a Cirq circuit\n", + "\n", + "We create a simple Bell-state circuit with two named measurement keys." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d3e980e", + "metadata": {}, + "outputs": [], + "source": [ + "import cirq\n", + "\n", + "q0, q1 = cirq.LineQubit.range(2)\n", + "cirq_circuit = cirq.Circuit(\n", + " cirq.H(q0),\n", + " cirq.CNOT(q0, q1),\n", + " cirq.measure(q0, key=\"q0\"),\n", + " cirq.measure(q1, key=\"q1\"),\n", + ")\n", + "print(cirq_circuit)" + ] + }, + { + "cell_type": "markdown", + "id": "7978d457", + "metadata": {}, + "source": [ + "### Export to OpenQASM 3\n", + "\n", + "`circuit.to_qasm(version=\"3.0\")` converts a Cirq circuit to an OpenQASM 3 string. This intermediate text can be inspected or modified before compilation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8aead8b", + "metadata": {}, + "outputs": [], + "source": [ + "qasm3_str_cirq = cirq_circuit.to_qasm(version=\"3.0\")\n", + "print(qasm3_str_cirq)" + ] + }, + { + "cell_type": "markdown", + "id": "ba3547ee", + "metadata": {}, + "source": [ + "### Compile to QIR and submit\n", + "\n", + "The compile and submission steps are identical to Part A — `compile` and `workspace` are already available from the cells above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f00236c7", + "metadata": {}, + "outputs": [], + "source": [ + "qir_cirq = compile(qasm3_str_cirq, target_profile=TargetProfile.Base)\n", + "\n", + "target = workspace.get_targets(target_name)\n", + "job = target.submit(qir_cirq, \"cirq-manual-job\", shots=100)\n", + "print(\"Job submitted. Waiting for results...\")\n", + "\n", + "results = job.get_results()\n", + "print(results)" + ] + }, + { + "cell_type": "markdown", + "id": "0270d310", + "metadata": {}, + "source": [ + "## Notes\n", + "\n", + "- **TargetProfile**: Use `TargetProfile.Base` for most simulators and hardware targets. Use `TargetProfile.Adaptive_RI` for targets that support adaptive circuits with classical branching.\n", + "- **Parameterized circuits (Qiskit)**: All circuit parameters must be bound before calling `qasm3.dumps`. Use `circuit.assign_parameters({...})` to produce a fully bound circuit.\n", + "- **Parameterized circuits (Cirq)**: Circuits must be fully resolved with `cirq.resolve_parameters()` before calling `to_qasm()`; the OpenQASM 3 exporter does not support free symbols.\n", + "- **Shared compilation step**: The `compile → submit` pipeline is identical for both frameworks — the only difference is how the OpenQASM 3 string is produced.\n", + "- **QIR inspection**: The `qir` object is LLVM bitcode. You can write it to a `.bc` file for offline inspection with LLVM tools." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/samples/python_interop/submit_qiskit_circuit_to_azure.ipynb b/samples/python_interop/qiskit_submission_to_azure.ipynb similarity index 67% rename from samples/python_interop/submit_qiskit_circuit_to_azure.ipynb rename to samples/python_interop/qiskit_submission_to_azure.ipynb index f599862e8c..49e53bba78 100644 --- a/samples/python_interop/submit_qiskit_circuit_to_azure.ipynb +++ b/samples/python_interop/qiskit_submission_to_azure.ipynb @@ -7,7 +7,7 @@ "source": [ "# Submitting Qiskit Circuits to Azure Quantum with the QDK\n", "\n", - "This notebook demonstrates two ways to run Qiskit `QuantumCircuit` jobs on Azure Quantum using the QDK: (A) direct submission via `AzureQuantumProvider` for a streamlined workflow, and (B) an explicit OpenQASM 3 → QIR compilation path for transparency and artifact inspection. It also shows how to handle parameterized circuits by binding parameters prior to execution." + "This notebook demonstrates how to run Qiskit `QuantumCircuit` jobs on Azure Quantum using the `AzureQuantumProvider` from the QDK. It also shows how to handle parameterized circuits by binding parameters prior to execution." ] }, { @@ -15,21 +15,12 @@ "id": "2b838c23", "metadata": {}, "source": [ - "This workflow demonstrates two methods of submission:\n", + "The workflow demonstrated here:\n", "\n", - "### A. Direct submission via `AzureQuantumProvider` (recommended when available)\n", "1. Build (or load) a Qiskit `QuantumCircuit`.\n", "2. Reference an existing Azure Quantum workspace with `qdk.azure.Workspace`.\n", "3. Instantiate `AzureQuantumProvider(workspace)` and pick a backend (`provider.get_backend()`).\n", - "4. Call `backend.run(circuit, shots, job_name=...)` and fetch results (`job.result().get_counts(circuit)`).\n", - "\n", - "### B. Submit via OpenQASM compilation (explicit OpenQASM → QIR → submit)\n", - "1. Build (or load) a Qiskit `QuantumCircuit`.\n", - "2. Convert it to OpenQASM 3 text (`qiskit.qasm3.dumps`).\n", - "3. Compile the OpenQASM 3 source to QIR with `qdk.openqasm.compile` (choose a `TargetProfile`).\n", - "4. Reference an existing Azure Quantum workspace with `qdk.azure.Workspace`.\n", - "5. Select a target (e.g. a simulator such as `rigetti.sim.qvm`).\n", - "6. Submit the QIR payload (`target.submit(qir, job_name, shots)`), then retrieve results (`job.get_results()`)." + "4. Call `backend.run(circuit, shots, job_name=...)` and fetch results (`job.result().get_counts(circuit)`)." ] }, { @@ -133,9 +124,9 @@ "id": "cdae59f7", "metadata": {}, "source": [ - "### Approach A: Using Azure Quantum Provider\n", + "## Submit the circuit to Azure Quantum\n", "\n", - "Here we use the recommended submission method with `AzureQuantumProvider` to submit the Qiskit circuit straight to an Azure Quantum backend. This avoids having to explicitly do QASM translation or QIR compilation steps." + "Use `AzureQuantumProvider` to submit the Qiskit circuit straight to an Azure Quantum backend. This handles the circuit-to-QIR compilation automatically without requiring explicit QASM translation steps." ] }, { @@ -166,45 +157,6 @@ "print(provider_counts)" ] }, - { - "cell_type": "markdown", - "id": "fbf60412", - "metadata": {}, - "source": [ - "### Approach B: Submit via OpenQASM compilation\n", - "\n", - "Below we show the longer path that exposes intermediate artifacts. This is useful if you want to inspect or transform OpenQASM/QIR, or integrate with tooling that consumes QIR directly.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ee47d6f6", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import qasm3\n", - "from qdk.openqasm import compile\n", - "from qdk import TargetProfile\n", - "\n", - "def submit_qiskit_circuit_to_azure_via_qasm(circuit, target_name, name, shots=100):\n", - " qasm3_str = qasm3.dumps(circuit)\n", - " qir = compile(qasm3_str, target_profile=TargetProfile.Base)\n", - "\n", - " workspace = Workspace(\n", - " subscription_id=subscription_id,\n", - " resource_group=resource_group,\n", - " name=workspace_name,\n", - " location=location,\n", - " )\n", - " target = workspace.get_targets(target_name)\n", - " job = target.submit(qir, name, shots=shots)\n", - " return job.get_results()\n", - "\n", - "results = submit_qiskit_circuit_to_azure_via_qasm(circuit, \"rigetti.sim.qvm\", \"qiskit-via-qasm-job\")\n", - "print(results)" - ] - }, { "cell_type": "markdown", "id": "9c84df75", @@ -275,22 +227,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": ".venv", - "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.13.9" + "name": "python" } }, "nbformat": 4, From cb577e518fb466b89685449d77dc37f40049725c Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 11:12:12 -0700 Subject: [PATCH 3/9] Handle Qubit Loss in Qiskit Azure notebook --- .../python_interop/neutral_atom_demo.ipynb | 4 +-- .../qiskit_submission_to_azure.ipynb | 32 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/samples/python_interop/neutral_atom_demo.ipynb b/samples/python_interop/neutral_atom_demo.ipynb index 28fee03eff..4233b3211d 100644 --- a/samples/python_interop/neutral_atom_demo.ipynb +++ b/samples/python_interop/neutral_atom_demo.ipynb @@ -12,9 +12,9 @@ "\n", "Key features shown:\n", "- Native gate decomposition (`Rz`, `SX`, `CZ`)\n", - "- Qubit loss noise modelling via `NoiseConfig`\n", + "- Qubit loss noise modeling via `NoiseConfig`\n", "- Separation of **accepted shots** (clean `{0,1}` results) from **raw shots** (includes qubit loss markers)\n", - "- Side-by-side histogram visualizations for both frameworks\n" + "- Side-by-side histogram visualizations for both frameworks" ] }, { diff --git a/samples/python_interop/qiskit_submission_to_azure.ipynb b/samples/python_interop/qiskit_submission_to_azure.ipynb index 49e53bba78..5ce9ba3c3e 100644 --- a/samples/python_interop/qiskit_submission_to_azure.ipynb +++ b/samples/python_interop/qiskit_submission_to_azure.ipynb @@ -126,7 +126,12 @@ "source": [ "## Submit the circuit to Azure Quantum\n", "\n", - "Use `AzureQuantumProvider` to submit the Qiskit circuit straight to an Azure Quantum backend. This handles the circuit-to-QIR compilation automatically without requiring explicit QASM translation steps." + "`AzureQuantumProvider` exposes Azure Quantum targets as Qiskit backends. When you call `provider.get_backend(target_name)`, the SDK returns one of two kinds of backend:\n", + "\n", + "- **Provider-specific backends** — Some hardware vendors (e.g. IonQ, Quantinuum) ship dedicated Qiskit backend classes that have native integration with their APIs. These handle gate translation and result parsing using hardware-specific logic.\n", + "- **Generic QIR backends** — For any other target that accepts QIR input, the SDK automatically wraps it as a generic backend. These compile the Qiskit circuit to OpenQASM 3 and then to QIR internally, so you don't have to manage those steps manually.\n", + "\n", + "In practice `get_backend()` selects the right type for you — you use the same call regardless of which backend type is returned." ] }, { @@ -224,6 +229,31 @@ ")\n", "print(results)" ] + }, + { + "cell_type": "markdown", + "id": "0ce5d167", + "metadata": {}, + "source": [ + "## Handling qubit loss on noisy hardware\n", + "\n", + "On some hardware backends — particularly neutral-atom and trapped-ion devices — a qubit may be lost before measurement (e.g. an atom is ejected from the trap). When this happens, the backend records `\"-\"` in the bitstring position for that qubit rather than `\"0\"` or `\"1\"`. Because loss shots contain non-binary characters, they cannot be included in standard counts or probability distributions, which assume a fixed binary alphabet. The SDK therefore separates them automatically.\n", + "\n", + "The result data (`job.result().results[0].data`) exposes two parallel sets of fields:\n", + "\n", + "| Field | What it contains |\n", + "|---|---|\n", + "| **`counts`** | Bitstring → shot count, accepted shots only (no `\"-\"`) |\n", + "| **`probabilities`** | Bitstring → empirical probability, accepted shots only |\n", + "| **`memory`** | Per-shot bitstring list, accepted shots only |\n", + "| **`raw_counts`** | Bitstring → shot count, all shots (loss shots have `\"-\"`) |\n", + "| **`raw_probabilities`** | Bitstring → empirical probability, all shots |\n", + "| **`raw_memory`** | Per-shot bitstring list, all shots |\n", + "\n", + "Use the **accepted** fields (`counts`, `probabilities`, `memory`) for any downstream analysis that expects clean binary bitstrings. Use the **raw** fields (`raw_counts`, `raw_probabilities`, `raw_memory`) to inspect loss patterns — for example, to calculate the overall loss rate or identify which qubit positions are being lost most frequently.\n", + "\n", + "> **Tip**: A high loss rate may indicate hardware instability or a circuit that is too deep for the current calibration." + ] } ], "metadata": { From 311dc9783410e805a4926de089bed6e457a4df51 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 12:41:12 -0700 Subject: [PATCH 4/9] Updated the azure-submission notebooks --- .../cirq_submission_to_azure.ipynb | 146 +++++++++++------- .../qiskit_submission_to_azure.ipynb | 78 ++++++---- 2 files changed, 139 insertions(+), 85 deletions(-) diff --git a/samples/python_interop/cirq_submission_to_azure.ipynb b/samples/python_interop/cirq_submission_to_azure.ipynb index fdfadd15b1..a324a4253a 100644 --- a/samples/python_interop/cirq_submission_to_azure.ipynb +++ b/samples/python_interop/cirq_submission_to_azure.ipynb @@ -7,7 +7,7 @@ "source": [ "# Submitting Cirq Circuits to Azure Quantum with the QDK\n", "\n", - "This notebook shows how to take a Cirq `Circuit`, export it to OpenQASM 3, compile that OpenQASM 3 source to QIR using the Quantum Development Kit (QDK) Python APIs, and submit it as a job to an Azure Quantum target." + "This notebook demonstrates how to run Cirq `Circuit` jobs on Azure Quantum using `AzureQuantumService` from the QDK." ] }, { @@ -17,12 +17,10 @@ "source": [ "The workflow demonstrated here:\n", "\n", - "1. Build (or load) a Cirq `Circuit`.\n", - "2. Convert it to OpenQASM 3 text via `circuit.to_qasm(version=\"3.0\")`.\n", - "3. Compile the OpenQASM 3 source to QIR with `qdk.openqasm.compile`.\n", - "4. Connect to (or create) an Azure Quantum workspace using `qdk.azure.Workspace`.\n", - "5. Pick a target (e.g., a simulator like `rigetti.sim.qvm`).\n", - "6. Submit the QIR payload and retrieve measurement results." + "1. Build a Cirq `Circuit` with named measurement keys.\n", + "2. Reference an existing Azure Quantum workspace with `AzureQuantumService`.\n", + "3. Browse available targets via `service.targets()`.\n", + "4. Call `service.create_job(program=circuit, repetitions=..., target=...)` and fetch results (`job.results()`)." ] }, { @@ -32,7 +30,7 @@ "source": [ "## Prerequisites\n", "\n", - "Ensure the `qdk` package is installed with `azure` and `cirq` extras. If not, install dependencies below." + "This notebook assumes the `qdk` package with Azure Quantum and Cirq support is installed. You can install everything with:" ] }, { @@ -50,7 +48,12 @@ "id": "20b9ed32", "metadata": {}, "source": [ - "After installing, restart the kernel if necessary. Verify imports:" + "This installs:\n", + "- The base `qdk` package (compiler, OpenQASM/QIR tooling)\n", + "- Azure Quantum client dependencies for submission\n", + "- Cirq for circuit construction\n", + "\n", + "After installing, restart the notebook kernel if it was already running. You can verify installation with:" ] }, { @@ -68,8 +71,9 @@ "id": "e2b111db", "metadata": {}, "source": [ - "## Submitting a simple Cirq circuit\n", - "We'll build a small circuit creating a superposition on one qubit and flipping another, then measuring both. Afterwards we submit it to an Azure Quantum target." + "## Build a simple Cirq circuit\n", + "\n", + "We start with a Bell-state circuit with two named measurement keys — one per qubit. Named keys are required so the result dictionary has clearly labeled registers." ] }, { @@ -79,15 +83,16 @@ "metadata": {}, "outputs": [], "source": [ - "# Build a simple circuit\n", + "import cirq\n", + "\n", "q0, q1 = cirq.LineQubit.range(2)\n", - "simple_circuit = cirq.Circuit(\n", + "circuit = cirq.Circuit(\n", " cirq.H(q0),\n", - " cirq.measure(q0, key='m0'),\n", - " cirq.X(q1),\n", - " cirq.measure(q1, key='m1'),\n", + " cirq.CNOT(q0, q1),\n", + " cirq.measure(q0, key=\"q0\"),\n", + " cirq.measure(q1, key=\"q1\"),\n", ")\n", - "print(simple_circuit)" + "print(circuit)" ] }, { @@ -96,7 +101,8 @@ "metadata": {}, "source": [ "## Configure Azure Quantum workspace connection\n", - "Replace the placeholder values below with your own subscription, resource group, workspace name, and location." + "\n", + "To connect to an Azure workspace replace the following variables with your own values." ] }, { @@ -113,42 +119,42 @@ ] }, { - "cell_type": "code", - "execution_count": null, - "id": "9f336702", + "cell_type": "markdown", + "id": "184b6e57", "metadata": {}, - "outputs": [], "source": [ - "from qdk.openqasm import compile\n", - "from qdk.azure import Workspace\n", - "from qdk import TargetProfile\n", - "\n", - "def submit_cirq_circuit_to_azure(circuit: cirq.Circuit, target_name: str, name: str, shots: int = 100):\n", - " # 1. Export to OpenQASM 3\n", - " qasm3_str = circuit.to_qasm(version='3.0')\n", - " # 2. Compile to QIR with a base target profile\n", - " qir = compile(qasm3_str, target_profile=TargetProfile.Base)\n", - " # 3. Connect workspace\n", - " workspace = Workspace(\n", - " subscription_id=subscription_id,\n", - " resource_group=resource_group,\n", - " name=workspace_name,\n", - " location=location,\n", - " )\n", - " # 4. Select target (string e.g. 'rigetti.sim.qvm' or other available target)\n", - " target = workspace.get_targets(target_name)\n", - " # 5. Submit QIR payload\n", - " job = target.submit(qir, name, shots=shots)\n", - " return job.get_results()" + "## Submit the circuit to Azure Quantum\n", + "\n", + "`AzureQuantumService` exposes Azure Quantum targets as Cirq-compatible target objects. When you call `service.targets()`, the SDK returns one of two kinds of target:\n", + "\n", + "- **Provider-specific targets** — Some hardware vendors (e.g. IonQ, Quantinuum) ship dedicated Cirq target classes with native integration for their APIs, handling gate translation and result parsing using hardware-specific logic.\n", + "- **Generic QIR targets** — For any other target that accepts QIR input, the SDK automatically wraps it as an `AzureGenericQirCirqTarget`. These compile the Cirq circuit to OpenQASM 3 and then to QIR internally, so you don't have to manage those steps manually.\n", + "\n", + "In practice `service.targets()` selects the right type for each target — you use the same `service.create_job()` call regardless of which target type is returned." ] }, { - "cell_type": "markdown", - "id": "a0436202", + "cell_type": "code", + "execution_count": null, + "id": "9f336702", "metadata": {}, + "outputs": [], "source": [ - "### Submit the simple circuit\n", - "(Make sure you changed the workspace credentials above.)" + "from qdk.azure import Workspace\n", + "from azure.quantum.cirq import AzureQuantumService\n", + "\n", + "workspace = Workspace(\n", + " subscription_id=subscription_id,\n", + " resource_group=resource_group,\n", + " name=workspace_name,\n", + " location=location,\n", + ")\n", + "\n", + "service = AzureQuantumService(workspace)\n", + "\n", + "# List available targets\n", + "for target in service.targets():\n", + " print(f\"{target.name:45s} {type(target).__name__}\")" ] }, { @@ -158,9 +164,31 @@ "metadata": {}, "outputs": [], "source": [ - "# Uncomment after setting workspace credentials\n", - "# results = submit_cirq_circuit_to_azure(simple_circuit, 'rigetti.sim.qvm', 'cirq-simple-job')\n", - "# print(results)" + "from collections import Counter\n", + "\n", + "# Replace with any target name from the list above\n", + "target_name = \"rigetti.sim.qvm\"\n", + "\n", + "job = service.create_job(\n", + " program=circuit,\n", + " repetitions=100,\n", + " name=\"cirq-bell-job\",\n", + " target=target_name,\n", + ")\n", + "print(f\"Job {job.job_id()} submitted — waiting for results...\")\n", + "\n", + "result = job.results()\n", + "\n", + "# Combine separate measurement keys into joint bitstrings\n", + "keys = sorted(result.measurements.keys())\n", + "joint = Counter(\n", + " \"\".join(str(int(result.measurements[k][i][0])) for k in keys)\n", + " for i in range(len(result.measurements[keys[0]]))\n", + ")\n", + "total = sum(joint.values())\n", + "print(f\"\\nResults ({total} shots) [keys: {', '.join(keys)}]:\")\n", + "for bitstring, count in sorted(joint.items()):\n", + " print(f\" {bitstring}: {count:4d} ({count/total:.1%})\")" ] }, { @@ -168,9 +196,21 @@ "id": "1478e88a", "metadata": {}, "source": [ - "## Notes\n", - "- Ensure all measurement keys appear before any classical condition usage if you introduce classical controls.\n", - "- Multi-target or custom gates may need full decomposition before submission if they produce unsupported classical constructs." + "## Handling qubit loss on noisy hardware\n", + "\n", + "On some hardware backends — particularly neutral-atom and trapped-ion devices — a qubit may be lost before measurement (e.g. an atom is ejected from the trap). When this happens, the backend records `\"-\"` in the bitstring position for that qubit rather than `\"0\"` or `\"1\"`. Because loss shots contain non-binary characters, they cannot be included in standard measurement arrays, which assume a fixed binary alphabet. The SDK therefore separates them automatically.\n", + "\n", + "The `cirq.ResultDict` returned by `job.results()` exposes two ways to access shots:\n", + "\n", + "| Field | What it contains |\n", + "|---|---|\n", + "| **`result.measurements[key]`** | NumPy int8 array of accepted shots only (no `\"-\"`), shape `(accepted_shots, num_qubits)` |\n", + "| **`result.raw_measurements()[key]`** | String array of all shots (loss shots have `\"-\"`), same key structure as `measurements` |\n", + "| **`result.raw_shots`** | The original shot objects exactly as returned by the backend |\n", + "\n", + "Use `result.measurements` for any downstream analysis that expects clean binary arrays. Use `result.raw_measurements()` and `result.raw_shots` to inspect loss patterns — for example, to calculate the overall loss rate or identify which qubit positions are being lost most frequently.\n", + "\n", + "> **Tip**: A high loss rate may indicate hardware instability or a circuit that is too deep for the current calibration." ] } ], diff --git a/samples/python_interop/qiskit_submission_to_azure.ipynb b/samples/python_interop/qiskit_submission_to_azure.ipynb index 5ce9ba3c3e..8489f0cfcf 100644 --- a/samples/python_interop/qiskit_submission_to_azure.ipynb +++ b/samples/python_interop/qiskit_submission_to_azure.ipynb @@ -7,7 +7,7 @@ "source": [ "# Submitting Qiskit Circuits to Azure Quantum with the QDK\n", "\n", - "This notebook demonstrates how to run Qiskit `QuantumCircuit` jobs on Azure Quantum using the `AzureQuantumProvider` from the QDK. It also shows how to handle parameterized circuits by binding parameters prior to execution." + "This notebook demonstrates how to run Qiskit `QuantumCircuit` jobs on Azure Quantum using `AzureQuantumProvider` from the QDK. It also shows how to handle parameterized circuits by binding parameters prior to execution." ] }, { @@ -17,10 +17,10 @@ "source": [ "The workflow demonstrated here:\n", "\n", - "1. Build (or load) a Qiskit `QuantumCircuit`.\n", + "1. Build a Qiskit `QuantumCircuit`.\n", "2. Reference an existing Azure Quantum workspace with `qdk.azure.Workspace`.\n", - "3. Instantiate `AzureQuantumProvider(workspace)` and pick a backend (`provider.get_backend()`).\n", - "4. Call `backend.run(circuit, shots, job_name=...)` and fetch results (`job.result().get_counts(circuit)`)." + "3. Instantiate `AzureQuantumProvider(workspace)` and browse available backends (`provider.backends()`).\n", + "4. Pick a backend and call `backend.run(circuit, shots=...)` and fetch results (`job.result().get_counts()`)." ] }, { @@ -71,9 +71,9 @@ "id": "4f761649", "metadata": {}, "source": [ - "## Submitting a simple Qiskit circuit\n", + "## Build a simple Qiskit circuit\n", "\n", - "We start with a minimal circuit that prepares single-qubit superpositions on two qubits and measures them. After constructing the circuit we’ll submit it to an Azure Quantum target." + "We start with a Bell-state circuit — a Hadamard on qubit 0 followed by a CNOT — which produces the entangled state $\\frac{1}{\\sqrt{2}}(|00\\rangle + |11\\rangle)$. After constructing the circuit we'll submit it to an Azure Quantum target." ] }, { @@ -85,14 +85,12 @@ "source": [ "from qiskit import QuantumCircuit\n", "\n", - "circuit = QuantumCircuit(2, 2)\n", - "\n", - "circuit.h(0)\n", - "circuit.measure(0, 0)\n", - "\n", - "circuit.h(1)\n", - "circuit.measure(1, 1)\n", + "q = QuantumCircuit(2, 2)\n", + "q.h(0)\n", + "q.cx(0, 1)\n", + "q.measure([0, 1], [0, 1])\n", "\n", + "circuit = q\n", "circuit.draw(output=\"text\")" ] }, @@ -144,22 +142,39 @@ "from qdk.azure import Workspace\n", "from qdk.azure.qiskit import AzureQuantumProvider\n", "\n", - "def submit_qiskit_circuit_to_azure_provider(circuit, target_name, name, shots=100):\n", + "workspace = Workspace(\n", + " subscription_id=subscription_id,\n", + " resource_group=resource_group,\n", + " name=workspace_name,\n", + " location=location,\n", + ")\n", + "\n", + "provider = AzureQuantumProvider(workspace)\n", "\n", - " workspace = Workspace(\n", - " subscription_id=subscription_id,\n", - " resource_group=resource_group,\n", - " name=workspace_name,\n", - " location=location,\n", - " )\n", + "# List available backends\n", + "for backend in provider.backends():\n", + " print(f\"{backend.name:45s} {type(backend).__name__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce1b1809", + "metadata": {}, + "outputs": [], + "source": [ + "# Replace with any backend name from the list above\n", + "target_name = \"rigetti.sim.qvm\"\n", + "backend = provider.get_backend(target_name)\n", "\n", - " provider = AzureQuantumProvider(workspace)\n", - " backend = provider.get_backend(target_name)\n", - " job = backend.run(circuit, shots, job_name=name)\n", - " return job.result().get_counts(circuit)\n", + "job = backend.run(circuit, shots=100, job_name=\"qiskit-provider-job\")\n", + "print(f\"Job {job.job_id()} submitted — waiting for results...\")\n", "\n", - "provider_counts = submit_qiskit_circuit_to_azure_provider(circuit, \"rigetti.sim.qvm\", \"qiskit-provider-job\")\n", - "print(provider_counts)" + "counts = job.result().get_counts()\n", + "total = sum(counts.values())\n", + "print(f\"\\nResults ({total} shots):\")\n", + "for bitstring, count in sorted(counts.items()):\n", + " print(f\" {bitstring}: {count:4d} ({count/total:.1%})\")" ] }, { @@ -222,12 +237,11 @@ "metadata": {}, "outputs": [], "source": [ - "results = submit_qiskit_circuit_to_azure_provider(\n", - " bound_circuit,\n", - " \"rigetti.sim.qvm\",\n", - " \"qiskit-parameterized-job\"\n", - ")\n", - "print(results)" + "parameterized_job = backend.run(bound_circuit, shots=100, job_name=\"qiskit-parameterized-job\")\n", + "print(f\"Job {parameterized_job.job_id()} submitted — waiting for results...\")\n", + "\n", + "parameterized_counts = parameterized_job.result().get_counts()\n", + "print(parameterized_counts)" ] }, { From 2dd7e7cf42fb2b7fb444a17548135891867d75ae Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 13:11:41 -0700 Subject: [PATCH 5/9] Add a neutral atom simulator notebook --- .../neutral_atom_simulator.ipynb | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 samples/python_interop/neutral_atom_simulator.ipynb diff --git a/samples/python_interop/neutral_atom_simulator.ipynb b/samples/python_interop/neutral_atom_simulator.ipynb new file mode 100644 index 0000000000..fc16f2de3e --- /dev/null +++ b/samples/python_interop/neutral_atom_simulator.ipynb @@ -0,0 +1,458 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "70e7454a", + "metadata": {}, + "source": [ + "# Simulating Circuits Locally with the Neutral Atom Simulator\n", + "\n", + "The QDK includes a local **neutral atom device simulator** that models the behavior of neutral-atom quantum hardware, including qubit loss noise. This makes it useful for testing circuits and understanding noisy results before submitting to real hardware on Azure Quantum.\n", + "\n", + "This notebook shows how to:\n", + "- Run circuits against the local simulator using both **Qiskit** (`NeutralAtomBackend`) and **Cirq** (`NeutralAtomSampler`)\n", + "- Configure qubit loss noise via `NoiseConfig`\n", + "- Interpret results that include loss markers, using the **accepted** (clean) and **raw** (all shots) result fields" + ] + }, + { + "cell_type": "markdown", + "id": "9ac71a38", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Install the `qdk` package with Qiskit and Cirq support:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc300bf3", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install \"qdk[qiskit,cirq]\" matplotlib" + ] + }, + { + "cell_type": "markdown", + "id": "181fe979", + "metadata": {}, + "source": [ + "After installing, restart the kernel if it was already running. Then verify imports:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12819c3c", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit, transpile\n", + "from qiskit.visualization import plot_histogram\n", + "from qdk.qiskit import NeutralAtomBackend\n", + "\n", + "import cirq\n", + "from qdk.cirq import NeutralAtomSampler\n", + "\n", + "from qdk.simulation import NoiseConfig\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from collections import Counter\n", + "\n", + "print(\"Imports successful.\")" + ] + }, + { + "cell_type": "markdown", + "id": "f173eb7c", + "metadata": {}, + "source": [ + "## The circuit: 3-qubit GHZ state\n", + "\n", + "We use a 3-qubit GHZ circuit throughout this notebook. It produces the maximally entangled state:\n", + "\n", + "$$|\\text{GHZ}\\rangle = \\frac{1}{\\sqrt{2}}(|000\\rangle + |111\\rangle)$$\n", + "\n", + "This is a good noise demonstration circuit because:\n", + "- Ideal results are simple: approximately 50% `000` and 50% `111`\n", + "- Any other outcome or missing qubit clearly indicates noise\n", + "- With 3 qubits, each shot has three independent chances to experience qubit loss\n", + "\n", + "We define both a Qiskit and a Cirq version of the same circuit below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad3f4e58", + "metadata": {}, + "outputs": [], + "source": [ + "n_qubits = 3\n", + "SHOTS = 500\n", + "SEED = 42" + ] + }, + { + "cell_type": "markdown", + "id": "4b7bbee4", + "metadata": {}, + "source": [ + "---\n", + "## Part A: Qiskit\n", + "\n", + "### Build and transpile the circuit\n", + "\n", + "The `NeutralAtomBackend` natively supports the gate set `{Rz, SX, CZ}`. Circuits written with higher-level gates (like `H` and `CX`) must first be transpiled into this native set.\n", + "\n", + "We transpile manually with `skip_transpilation=True` on the subsequent `run()` calls. This gives us full control over the decomposition and avoids the backend re-transpiling on each run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96082349", + "metadata": {}, + "outputs": [], + "source": [ + "# Build the high-level GHZ circuit\n", + "ghz_qiskit = QuantumCircuit(n_qubits, n_qubits)\n", + "ghz_qiskit.h(0)\n", + "for i in range(n_qubits - 1):\n", + " ghz_qiskit.cx(i, i + 1)\n", + "ghz_qiskit.measure(range(n_qubits), range(n_qubits))\n", + "\n", + "print(\"High-level circuit:\")\n", + "print(ghz_qiskit.draw())\n", + "\n", + "# Transpile to the native gate set {Rz, SX, CZ}\n", + "backend = NeutralAtomBackend()\n", + "native_qiskit = transpile(ghz_qiskit, backend=backend)\n", + "\n", + "print(\"\\nNative gate circuit:\")\n", + "print(native_qiskit.draw())" + ] + }, + { + "cell_type": "markdown", + "id": "29def8fb", + "metadata": {}, + "source": [ + "### Noiseless simulation\n", + "\n", + "Run the native circuit with no noise configured. We expect to see only the two ideal GHZ outcomes: `000` and `111`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c72e5935", + "metadata": {}, + "outputs": [], + "source": [ + "job_noiseless = backend.run(\n", + " native_qiskit,\n", + " shots=SHOTS,\n", + " seed=SEED,\n", + " skip_transpilation=True,\n", + ")\n", + "data_noiseless = job_noiseless.result().results[0].data\n", + "\n", + "print(\"Noiseless counts:\", dict(data_noiseless.counts))\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "plot_histogram(dict(data_noiseless.counts), ax=ax)\n", + "ax.set_title(\"Qiskit — Noiseless GHZ\")\n", + "ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + "ax.set_axisbelow(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "1cc1ae90", + "metadata": {}, + "source": [ + "### Configure qubit loss noise\n", + "\n", + "`NoiseConfig` controls the per-gate noise parameters. Here we set an 8% qubit-loss probability on `Rz` gates.\n", + "\n", + "Since the `H` gate decomposes to `Rz` gates in the native set, every qubit passes through at least one `Rz` and has a chance of being lost before measurement. When a qubit is lost, the simulator records `\"-\"` in its bitstring position rather than `\"0\"` or `\"1\"`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70749b30", + "metadata": {}, + "outputs": [], + "source": [ + "noise = NoiseConfig()\n", + "noise.rz.loss = 0.08 # 8% qubit-loss probability per Rz gate" + ] + }, + { + "cell_type": "markdown", + "id": "ae8361a6", + "metadata": {}, + "source": [ + "### Noisy simulation and reading results\n", + "\n", + "The result data (`job.result().results[0].data`) exposes two parallel sets of fields:\n", + "\n", + "| Field | What it contains |\n", + "|---|---|\n", + "| **`counts`** | Bitstring → shot count, accepted shots only (no `\"-\"`) |\n", + "| **`probabilities`** | Bitstring → empirical probability, accepted shots only |\n", + "| **`memory`** | Per-shot bitstring list, accepted shots only |\n", + "| **`raw_counts`** | Bitstring → shot count, all shots (loss shots have `\"-\"`) |\n", + "| **`raw_probabilities`** | Bitstring → empirical probability, all shots |\n", + "| **`raw_memory`** | Per-shot bitstring list, all shots |\n", + "\n", + "Use the **accepted** fields for downstream analysis. Use the **raw** fields to inspect or quantify loss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bb7d3f1", + "metadata": {}, + "outputs": [], + "source": [ + "job_noisy = backend.run(\n", + " native_qiskit,\n", + " shots=SHOTS,\n", + " noise=noise,\n", + " seed=SEED,\n", + " skip_transpilation=True,\n", + ")\n", + "data_noisy = job_noisy.result().results[0].data\n", + "\n", + "accepted = dict(data_noisy.counts)\n", + "raw = dict(data_noisy.raw_counts)\n", + "\n", + "total_raw = sum(raw.values())\n", + "total_accepted = sum(accepted.values())\n", + "total_lost = total_raw - total_accepted\n", + "\n", + "print(f\"Total shots : {total_raw}\")\n", + "print(f\"Accepted : {total_accepted} ({100 * total_accepted / total_raw:.1f}%)\")\n", + "print(f\"Lost : {total_lost} ({100 * total_lost / total_raw:.1f}%)\")\n", + "print()\n", + "print(\"Accepted counts:\", accepted)\n", + "print(\"Raw counts :\", raw)" + ] + }, + { + "cell_type": "markdown", + "id": "63b0aa3e", + "metadata": {}, + "source": [ + "### Visualize: accepted vs raw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9cead95", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", + "\n", + "plot_histogram(accepted, ax=axes[0])\n", + "plot_histogram(raw, ax=axes[1])\n", + "\n", + "for ax, title in zip(axes, [\n", + " \"Qiskit — Accepted shots (loss filtered out)\",\n", + " \"Qiskit — Raw shots (loss bitstrings included)\",\n", + "]):\n", + " ax.set_title(title)\n", + " ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + " ax.set_axisbelow(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "700f305b", + "metadata": {}, + "source": [ + "---\n", + "## Part B: Cirq\n", + "\n", + "### Build the circuit\n", + "\n", + "The `NeutralAtomSampler` accepts standard Cirq circuits directly. It internally converts them to OpenQASM 3.0 before simulating, so no manual transpilation step is needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3448c0a2", + "metadata": {}, + "outputs": [], + "source": [ + "qubits = cirq.LineQubit.range(n_qubits)\n", + "\n", + "ghz_cirq = cirq.Circuit(\n", + " cirq.H(qubits[0]),\n", + " *[cirq.CNOT(qubits[i], qubits[i + 1]) for i in range(n_qubits - 1)],\n", + " cirq.measure(*qubits, key=\"result\"),\n", + ")\n", + "\n", + "print(ghz_cirq)" + ] + }, + { + "cell_type": "markdown", + "id": "d3ec4b8a", + "metadata": {}, + "source": [ + "### Noiseless simulation\n", + "\n", + "Run with no noise and confirm only `000` and `111` appear." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b2d36ef", + "metadata": {}, + "outputs": [], + "source": [ + "sampler = NeutralAtomSampler()\n", + "cirq_result_noiseless = sampler.run(ghz_cirq, repetitions=SHOTS)\n", + "\n", + "# histogram() returns a Counter of integer bitmask → count; format as binary strings\n", + "cirq_noiseless_counts = {\n", + " format(k, f\"0{n_qubits}b\"): v\n", + " for k, v in cirq_result_noiseless.histogram(key=\"result\").items()\n", + "}\n", + "print(\"Noiseless counts:\", cirq_noiseless_counts)\n", + "\n", + "def plot_bar(ax, counts, title):\n", + " labels = sorted(counts.keys())\n", + " values = [counts[k] for k in labels]\n", + " bars = ax.bar(labels, values)\n", + " ax.set_title(title)\n", + " ax.set_xlabel(\"Outcome\")\n", + " ax.set_ylabel(\"Count\")\n", + " ax.tick_params(axis=\"x\", rotation=45)\n", + " ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", + " ax.set_axisbelow(True)\n", + " for bar, value in zip(bars, values):\n", + " ax.text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height(), str(value),\n", + " ha=\"center\", va=\"bottom\", fontsize=9,\n", + " )\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "plot_bar(ax, cirq_noiseless_counts, \"Cirq — Noiseless GHZ\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d2318fdd", + "metadata": {}, + "source": [ + "### Noisy simulation and reading results\n", + "\n", + "Pass the same `NoiseConfig` to `NeutralAtomSampler`. The result object exposes:\n", + "\n", + "| Field | What it contains |\n", + "|---|---|\n", + "| **`result.measurements[key]`** | NumPy int8 array of accepted shots only (no `\"-\"`), shape `(accepted_shots, n_qubits)` |\n", + "| **`result.raw_measurements()[key]`** | String array of all shots, `\"-\"` where a qubit was lost, same shape |\n", + "| **`result.raw_shots`** | The original shot objects as returned by the simulator |\n", + "\n", + "Use `result.measurements` for analysis. Use `result.raw_measurements()` and `result.raw_shots` to inspect loss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f067bfc", + "metadata": {}, + "outputs": [], + "source": [ + "noisy_sampler = NeutralAtomSampler(noise=noise, seed=SEED)\n", + "cirq_result_noisy = noisy_sampler.run(ghz_cirq, repetitions=SHOTS)\n", + "\n", + "# Accepted: int8 array, shape = (accepted_shots, n_qubits)\n", + "accepted_arr = cirq_result_noisy.measurements[\"result\"]\n", + "\n", + "# Raw: string array, shape = (total_shots, n_qubits), \"-\" for lost qubits\n", + "raw_arr = cirq_result_noisy.raw_measurements()[\"result\"]\n", + "\n", + "cirq_total = raw_arr.shape[0]\n", + "cirq_accepted = accepted_arr.shape[0]\n", + "cirq_lost = cirq_total - cirq_accepted\n", + "\n", + "print(f\"Total shots : {cirq_total}\")\n", + "print(f\"Accepted : {cirq_accepted} ({100 * cirq_accepted / cirq_total:.1f}%)\")\n", + "print(f\"Lost : {cirq_lost} ({100 * cirq_lost / cirq_total:.1f}%)\")\n", + "\n", + "def arr_to_counts(arr):\n", + " return Counter(\"\".join(row) for row in arr)\n", + "\n", + "cirq_accepted_counts = arr_to_counts(accepted_arr.astype(str))\n", + "cirq_raw_counts = arr_to_counts(raw_arr)\n", + "\n", + "print(\"\\nAccepted counts:\", dict(cirq_accepted_counts))\n", + "print(\"Raw counts :\", dict(cirq_raw_counts))" + ] + }, + { + "cell_type": "markdown", + "id": "486fa6d4", + "metadata": {}, + "source": [ + "### Visualize: accepted vs raw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4898a60", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", + "plot_bar(axes[0], dict(cirq_accepted_counts), \"Cirq — Accepted shots (loss filtered out)\")\n", + "plot_bar(axes[1], dict(cirq_raw_counts), \"Cirq — Raw shots (loss bitstrings included)\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "06b1d027", + "metadata": {}, + "source": [ + "## Notes\n", + "\n", + "- **`NoiseConfig` gate coverage**: Qubit-loss probability can be set independently per gate type (e.g. `noise.rz.loss`, `noise.sx.loss`, `noise.cz.loss`). You can mix different rates to model realistic hardware calibration profiles.\n", + "- **Why `skip_transpilation=True` (Qiskit only)**: Passing `skip_transpilation=True` tells the backend that the circuit is already in the native gate set. If you omit it, the backend will transpile automatically, but transpiling once and reusing saves time when running many shots or noise configurations.\n", + "- **Cirq result arrays**: Cirq's `measurements` and `raw_measurements()` return 2D NumPy arrays with one row per shot and one column per qubit. The `arr_to_counts` helper above joins each row into a single bitstring (e.g. `[\"1\", \"1\", \"1\"]` → `\"111\"`) to make the distribution easy to inspect and visualize.\n", + "- **Next steps**: Once your circuit behaves as expected locally, submit it to real neutral-atom hardware on Azure Quantum using `cirq_submission_to_azure.ipynb` or `qiskit_submission_to_azure.ipynb`.\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 4ab9fbc07b55bf05a63bac1a86db7e35f7a9e274 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 13:12:42 -0700 Subject: [PATCH 6/9] silly metadata --- samples/python_interop/qiskit.ipynb | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/samples/python_interop/qiskit.ipynb b/samples/python_interop/qiskit.ipynb index cae20d12bd..697a2f4877 100644 --- a/samples/python_interop/qiskit.ipynb +++ b/samples/python_interop/qiskit.ipynb @@ -457,24 +457,10 @@ } ], "metadata": { - "kernelspec": { - "display_name": ".venv", - "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.13.3" - } - }, + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } From 684e0183dbfe18ce21a86f71a5e57079cd086db7 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 13:23:21 -0700 Subject: [PATCH 7/9] touch up to the manual submission notebook --- .../qir_circuit_submission_to_azure.ipynb | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/samples/python_interop/qir_circuit_submission_to_azure.ipynb b/samples/python_interop/qir_circuit_submission_to_azure.ipynb index 95f5781cbb..33dfb3b72b 100644 --- a/samples/python_interop/qir_circuit_submission_to_azure.ipynb +++ b/samples/python_interop/qir_circuit_submission_to_azure.ipynb @@ -12,7 +12,7 @@ "This approach gives full visibility and control over intermediate compilation artifacts. It is useful when you need to inspect or modify OpenQASM or QIR before submission, target backends that require raw QIR, or share a single submission pipeline across frameworks.\n", "\n", "For the recommended getting-started experience for each framework, see:\n", - "- `submit_qiskit_circuit_to_azure.ipynb`\n", + "- `qiskit_submission_to_azure.ipynb`\n", "- `cirq_submission_to_azure.ipynb`" ] }, @@ -86,9 +86,9 @@ "metadata": {}, "outputs": [], "source": [ - "subscription_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'\n", - "resource_group = 'myresourcegroup'\n", - "workspace_name = 'myworkspace'\n", + "subscription_id = '677fc922-91d0-4bf6-9b06-4274d319a0fa'\n", + "resource_group = 'devtools-team'\n", + "workspace_name = 'devtools-v1'\n", "location = 'westus'\n", "\n", "# Replace with any QIR-capable target name from your workspace\n", @@ -105,7 +105,7 @@ "\n", "### Build a Qiskit circuit\n", "\n", - "We use a simple two-qubit superposition circuit to demonstrate the Qiskit path." + "We use a Bell-state circuit — two entangled qubits — to demonstrate the Qiskit path.\n" ] }, { @@ -119,11 +119,10 @@ "\n", "qiskit_circuit = QuantumCircuit(2, 2)\n", "qiskit_circuit.h(0)\n", - "qiskit_circuit.measure(0, 0)\n", - "qiskit_circuit.h(1)\n", - "qiskit_circuit.measure(1, 1)\n", + "qiskit_circuit.cx(0, 1)\n", + "qiskit_circuit.measure([0, 1], [0, 1])\n", "\n", - "qiskit_circuit.draw(output=\"text\")" + "qiskit_circuit.draw(output=\"text\")\n" ] }, { @@ -275,10 +274,8 @@ "## Notes\n", "\n", "- **TargetProfile**: Use `TargetProfile.Base` for most simulators and hardware targets. Use `TargetProfile.Adaptive_RI` for targets that support adaptive circuits with classical branching.\n", - "- **Parameterized circuits (Qiskit)**: All circuit parameters must be bound before calling `qasm3.dumps`. Use `circuit.assign_parameters({...})` to produce a fully bound circuit.\n", - "- **Parameterized circuits (Cirq)**: Circuits must be fully resolved with `cirq.resolve_parameters()` before calling `to_qasm()`; the OpenQASM 3 exporter does not support free symbols.\n", "- **Shared compilation step**: The `compile → submit` pipeline is identical for both frameworks — the only difference is how the OpenQASM 3 string is produced.\n", - "- **QIR inspection**: The `qir` object is LLVM bitcode. You can write it to a `.bc` file for offline inspection with LLVM tools." + "- **QIR inspection**: The `qir` object is LLVM bitcode. You can write it to a `.bc` file for offline inspection with LLVM tools.\n" ] } ], From 721efe797be0a7896f2dd83d4a259ac3d2c7f029 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 13:23:47 -0700 Subject: [PATCH 8/9] remove demo notebook --- .../python_interop/neutral_atom_demo.ipynb | 454 ------------------ 1 file changed, 454 deletions(-) delete mode 100644 samples/python_interop/neutral_atom_demo.ipynb diff --git a/samples/python_interop/neutral_atom_demo.ipynb b/samples/python_interop/neutral_atom_demo.ipynb deleted file mode 100644 index 4233b3211d..0000000000 --- a/samples/python_interop/neutral_atom_demo.ipynb +++ /dev/null @@ -1,454 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "dd6f5b56", - "metadata": {}, - "source": [ - "# Local Neutral Atom Simulator Demo\n", - "## Qiskit & Cirq submission with qubit loss noise\n", - "\n", - "This notebook demonstrates how to run quantum circuits on the local **NeutralAtomDevice** simulator using both Qiskit and Cirq.\n", - "\n", - "Key features shown:\n", - "- Native gate decomposition (`Rz`, `SX`, `CZ`)\n", - "- Qubit loss noise modeling via `NoiseConfig`\n", - "- Separation of **accepted shots** (clean `{0,1}` results) from **raw shots** (includes qubit loss markers)\n", - "- Side-by-side histogram visualizations for both frameworks" - ] - }, - { - "cell_type": "markdown", - "id": "0e464004", - "metadata": {}, - "source": [ - "## 1. Import Libraries" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d963721", - "metadata": {}, - "outputs": [], - "source": [ - "# Qiskit\n", - "from qiskit import QuantumCircuit, transpile\n", - "from qiskit.visualization import plot_histogram\n", - "\n", - "# Qiskit neutral-atom backend\n", - "from qdk.qiskit import NeutralAtomBackend\n", - "\n", - "# Cirq\n", - "import cirq\n", - "\n", - "# Cirq neutral-atom sampler\n", - "from qdk.cirq import NeutralAtomSampler\n", - "\n", - "# Shared noise configuration\n", - "from qdk.simulation import NoiseConfig\n", - "\n", - "# Plotting\n", - "import matplotlib.pyplot as plt\n", - "from collections import Counter\n", - "\n", - "print(\"Imports successful.\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "68a4e541", - "metadata": {}, - "source": [ - "## 2. Build the GHZ Circuit (Qiskit)\n", - "\n", - "We construct a 3-qubit GHZ state circuit and then **transpile** it to the native gate set\n", - "supported by the NeutralAtomDevice: `{Rz, SX, CZ}`.\n", - "\n", - "Transpiling first lets us run with `skip_transpilation=True`, which bypasses the backend's\n", - "own transpile step and gives us full control over the decomposition.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "71696166", - "metadata": {}, - "outputs": [], - "source": [ - "n_qubits = 3\n", - "\n", - "# High-level GHZ circuit\n", - "ghz_qiskit = QuantumCircuit(n_qubits, n_qubits)\n", - "ghz_qiskit.h(0)\n", - "for i in range(n_qubits - 1):\n", - " ghz_qiskit.cx(i, i + 1)\n", - "ghz_qiskit.measure(range(n_qubits), range(n_qubits))\n", - "\n", - "print(\"High-level circuit:\")\n", - "print(ghz_qiskit.draw())\n", - "\n", - "# Decompose to native gate set {Rz, SX, CZ}\n", - "backend = NeutralAtomBackend()\n", - "native_qiskit = transpile(\n", - " ghz_qiskit,\n", - " backend=backend\n", - ")\n", - "\n", - "print(\"\\nNative gate circuit:\")\n", - "print(native_qiskit.draw())\n" - ] - }, - { - "cell_type": "markdown", - "id": "3cb69bbe", - "metadata": {}, - "source": [ - "## 3. Noiseless Simulation — Qiskit\n", - "\n", - "Run the native circuit on the local simulator with **no noise**.\n", - "We expect to see only the two ideal GHZ outcomes: `000` and `111`.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3009cd24", - "metadata": {}, - "outputs": [], - "source": [ - "SHOTS = 500\n", - "SEED = 42\n", - "\n", - "job_noiseless = backend.run(\n", - " native_qiskit,\n", - " shots=SHOTS,\n", - " seed=SEED,\n", - " skip_transpilation=True,\n", - ")\n", - "result_noiseless = job_noiseless.result()\n", - "data_noiseless = result_noiseless.results[0].data\n", - "\n", - "print(\"Noiseless counts:\", dict(data_noiseless.counts))\n", - "fig, ax = plt.subplots(figsize=(6, 4))\n", - "plot_histogram(dict(data_noiseless.counts), ax=ax)\n", - "ax.set_title(\"Qiskit — Noiseless GHZ\")\n", - "ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", - "ax.set_axisbelow(True)\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "id": "58bfb69d", - "metadata": {}, - "source": [ - "## 4. Noisy Simulation — Qiskit (8% Rz qubit loss)\n", - "\n", - "Configure an 8% qubit-loss probability on `Rz` gates and re-run.\n", - "\n", - "The `H` gate decomposes to `Rz` gates in the native set, so every qubit passes through\n", - "at least one `Rz` and has a chance of being lost.\n", - "\n", - "**Accepted shots** (`counts`) exclude any shot where at least one qubit was lost. \n", - "**Raw shots** (`raw_counts`) include all shots; lost qubits appear as `\"-\"` in the bitstring.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b901ce8b", - "metadata": {}, - "outputs": [], - "source": [ - "noise = NoiseConfig()\n", - "noise.rz.loss = 0.08 # 8% qubit-loss probability per Rz gate\n", - "\n", - "job_noisy = backend.run(\n", - " native_qiskit,\n", - " shots=SHOTS,\n", - " noise=noise,\n", - " seed=SEED,\n", - " skip_transpilation=True,\n", - ")\n", - "result_noisy = job_noisy.result()\n", - "data_noisy = result_noisy.results[0].data\n", - "\n", - "accepted = dict(data_noisy.counts)\n", - "raw = dict(data_noisy.raw_counts)\n", - "\n", - "total_accepted = sum(accepted.values())\n", - "total_raw = sum(raw.values())\n", - "lost = total_raw - total_accepted\n", - "\n", - "print(f\"Total shots : {total_raw}\")\n", - "print(f\"Accepted : {total_accepted} ({100*total_accepted/total_raw:.1f}%)\")\n", - "print(f\"Lost : {lost} ({100*lost/total_raw:.1f}%)\")\n", - "print()\n", - "print(\"Accepted counts:\", accepted)\n", - "print(\"Raw counts :\", raw)\n" - ] - }, - { - "cell_type": "markdown", - "id": "5a2e0164", - "metadata": {}, - "source": [ - "## 5. Visualize Qiskit Results — Filtered vs Raw Histograms\n", - "\n", - "Side-by-side comparison showing the accepted (clean) distribution on the left and the full\n", - "raw distribution (including loss bitstrings) on the right.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b06d4fd", - "metadata": {}, - "outputs": [], - "source": [ - "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", - "\n", - "plot_histogram(accepted, ax=axes[0])\n", - "plot_histogram(raw, ax=axes[1])\n", - "\n", - "titles = [\n", - " \"Qiskit — Accepted shots (loss filtered out)\",\n", - " \"Qiskit — Raw shots (loss bitstrings included)\",\n", - "]\n", - "for ax, title in zip(axes, titles):\n", - " ax.set_title(title)\n", - " ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", - " ax.set_axisbelow(True)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "id": "723aa403", - "metadata": {}, - "source": [ - "## 6. Build the GHZ Circuit (Cirq)\n", - "\n", - "Cirq circuits work directly with `cirq.LineQubit` objects. \n", - "The `NeutralAtomSampler` accepts standard Cirq circuits and internally converts them to\n", - "OpenQASM 3.0 before simulating.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3490b0a7", - "metadata": {}, - "outputs": [], - "source": [ - "qubits = cirq.LineQubit.range(n_qubits)\n", - "\n", - "ghz_cirq = cirq.Circuit(\n", - " cirq.H(qubits[0]),\n", - " *[cirq.CNOT(qubits[i], qubits[i + 1]) for i in range(n_qubits - 1)],\n", - " cirq.measure(*qubits, key=\"result\"),\n", - ")\n", - "\n", - "print(ghz_cirq)\n" - ] - }, - { - "cell_type": "markdown", - "id": "2b8badcb", - "metadata": {}, - "source": [ - "## 7. Noiseless Simulation — Cirq\n", - "\n", - "Run with no noise and confirm only `000` and `111` appear.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2fe63a0c", - "metadata": {}, - "outputs": [], - "source": [ - "sampler = NeutralAtomSampler()\n", - "\n", - "cirq_result_noiseless = sampler.run(ghz_cirq, repetitions=SHOTS)\n", - "\n", - "# histogram() returns a Counter of integer bitmask → count\n", - "cirq_noiseless_counts = dict(cirq_result_noiseless.histogram(key=\"result\"))\n", - "\n", - "# Format integer keys as binary bitstrings for readability\n", - "cirq_noiseless_str = {format(k, f\"0{n_qubits}b\"): v for k, v in cirq_noiseless_counts.items()}\n", - "print(\"Noiseless Cirq counts:\", cirq_noiseless_str)\n", - "\n", - "fig, ax = plt.subplots(figsize=(6, 4))\n", - "bars = ax.bar(cirq_noiseless_str.keys(), cirq_noiseless_str.values())\n", - "ax.set_title(\"Cirq — Noiseless GHZ\")\n", - "ax.set_xlabel(\"Outcome\")\n", - "ax.set_ylabel(\"Count\")\n", - "ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", - "ax.set_axisbelow(True)\n", - "for bar in bars:\n", - " ax.text(\n", - " bar.get_x() + bar.get_width() / 2,\n", - " bar.get_height(),\n", - " str(int(bar.get_height())),\n", - " ha=\"center\",\n", - " va=\"bottom\",\n", - " fontsize=9,\n", - " )\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "id": "21d1679b", - "metadata": {}, - "source": [ - "## 8. Noisy Simulation — Cirq (8% Rz qubit loss)\n", - "\n", - "The same `NoiseConfig` is passed to the Cirq sampler.\n", - "\n", - "- **`result.measurements[\"result\"]`** — accepted shots only (int8 array, no loss rows)\n", - "- **`result.raw_measurements()[\"result\"]`** — all shots; lost qubits are `\"-\"` strings\n", - "- **`result.raw_shots`** — raw simulator output as a list of `Result` enum tuples\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03d72b07", - "metadata": {}, - "outputs": [], - "source": [ - "noisy_sampler = NeutralAtomSampler(noise=noise, seed=SEED)\n", - "\n", - "cirq_result_noisy = noisy_sampler.run(ghz_cirq, repetitions=SHOTS)\n", - "\n", - "# Accepted measurements: int8 2-D array, shape = (accepted_shots, n_qubits)\n", - "accepted_arr = cirq_result_noisy.measurements[\"result\"]\n", - "\n", - "# Raw measurements: str 2-D array, shape = (total_shots, n_qubits), \"-\" for loss\n", - "raw_arr = cirq_result_noisy.raw_measurements()[\"result\"]\n", - "\n", - "cirq_accepted = sum(1 for _ in accepted_arr) # accepted_arr.shape[0]\n", - "cirq_total = raw_arr.shape[0]\n", - "cirq_lost = cirq_total - cirq_accepted\n", - "\n", - "print(f\"Total shots : {cirq_total}\")\n", - "print(f\"Accepted : {cirq_accepted} ({100*cirq_accepted/cirq_total:.1f}%)\")\n", - "print(f\"Lost : {cirq_lost} ({100*cirq_lost/cirq_total:.1f}%)\")\n", - "\n", - "# Build string-keyed counters from the raw array rows\n", - "def arr_to_counts(arr):\n", - " return Counter(\"\".join(row) for row in arr)\n", - "\n", - "cirq_accepted_counts = arr_to_counts(accepted_arr.astype(str))\n", - "cirq_raw_counts = arr_to_counts(raw_arr)\n", - "\n", - "print(\"\\nAccepted counts:\", dict(cirq_accepted_counts))\n", - "print(\"Raw counts :\", dict(cirq_raw_counts))\n" - ] - }, - { - "cell_type": "markdown", - "id": "a4c1a155", - "metadata": {}, - "source": [ - "## 9. Visualize Cirq Results — Filtered vs Raw Histograms" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a11edf92", - "metadata": {}, - "outputs": [], - "source": [ - "def plot_counts(ax, counts, title):\n", - " labels = sorted(counts.keys())\n", - " values = [counts[k] for k in labels]\n", - " bars = ax.bar(labels, values)\n", - " ax.set_title(title)\n", - " ax.set_xlabel(\"Outcome\")\n", - " ax.set_ylabel(\"Count\")\n", - " ax.tick_params(axis=\"x\", rotation=45)\n", - " # Dashed horizontal gridlines\n", - " ax.yaxis.grid(True, linestyle=\"--\", alpha=0.7)\n", - " ax.set_axisbelow(True)\n", - " # Count labels above each bar\n", - " for bar, value in zip(bars, values):\n", - " ax.text(\n", - " bar.get_x() + bar.get_width() / 2,\n", - " bar.get_height(),\n", - " str(value),\n", - " ha=\"center\",\n", - " va=\"bottom\",\n", - " fontsize=9,\n", - " )\n", - "\n", - "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", - "plot_counts(axes[0], dict(cirq_accepted_counts), \"Cirq — Accepted shots (loss filtered out)\")\n", - "plot_counts(axes[1], dict(cirq_raw_counts), \"Cirq — Raw shots (loss bitstrings included)\")\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "id": "5d92825d", - "metadata": {}, - "source": [ - "## 10. Side-by-Side Comparison — Qiskit vs Cirq\n", - "\n", - "A 2×2 subplot grid comparing all four scenarios:\n", - "\n", - "| | Accepted (filtered) | Raw (with loss) |\n", - "|---|---|---|\n", - "| **Qiskit** | top-left | top-right |\n", - "| **Cirq** | bottom-left | bottom-right |\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "13559f71", - "metadata": {}, - "outputs": [], - "source": [ - "fig, axes = plt.subplots(2, 2, figsize=(16, 8))\n", - "fig.suptitle(\n", - " f\"NeutralAtomDevice — GHZ ({n_qubits} qubits, {SHOTS} shots, 8% Rz loss)\",\n", - " fontsize=14,\n", - " fontweight=\"bold\",\n", - ")\n", - "\n", - "# Row 0: Qiskit\n", - "plot_counts(axes[0, 0], accepted, \"Qiskit — Accepted\")\n", - "plot_counts(axes[0, 1], raw, \"Qiskit — Raw (loss included)\")\n", - "\n", - "# Row 1: Cirq\n", - "plot_counts(axes[1, 0], dict(cirq_accepted_counts), \"Cirq — Accepted\")\n", - "plot_counts(axes[1, 1], dict(cirq_raw_counts), \"Cirq — Raw (loss included)\")\n", - "\n", - "# Add framework labels on the left\n", - "for ax, label in zip(axes[:, 0], [\"Qiskit\", \"Cirq\"]):\n", - " ax.set_ylabel(f\"{label}\\nCount\")\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 550cc6c53d980e05d116569835ad6787c60c8346 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 27 Mar 2026 13:32:11 -0700 Subject: [PATCH 9/9] gotta leave that blank for the users to fill in --- .../python_interop/qir_circuit_submission_to_azure.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/python_interop/qir_circuit_submission_to_azure.ipynb b/samples/python_interop/qir_circuit_submission_to_azure.ipynb index 33dfb3b72b..5b32d6292b 100644 --- a/samples/python_interop/qir_circuit_submission_to_azure.ipynb +++ b/samples/python_interop/qir_circuit_submission_to_azure.ipynb @@ -86,9 +86,9 @@ "metadata": {}, "outputs": [], "source": [ - "subscription_id = '677fc922-91d0-4bf6-9b06-4274d319a0fa'\n", - "resource_group = 'devtools-team'\n", - "workspace_name = 'devtools-v1'\n", + "subscription_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'\n", + "resource_group = 'myresourcegroup'\n", + "workspace_name = 'myworkspace'\n", "location = 'westus'\n", "\n", "# Replace with any QIR-capable target name from your workspace\n",