diff --git a/build.py b/build.py index 619658e014..4cd38aaa3d 100755 --- a/build.py +++ b/build.py @@ -702,8 +702,10 @@ def run_ci_historic_benchmark(): or f.startswith("iterative_phase_estimation.") or f.startswith("repeat_until_success.") or f.startswith("python-deps.") - or f.startswith("submit_qiskit_circuit_to_azure.") or f.startswith("cirq_submission_to_azure.") + or f.startswith("neutral_atom_simulator.") + or f.startswith("qir_circuit_submission_to_azure.") + or f.startswith("qiskit_submission_to_azure") or f.startswith("pennylane_submission_to_azure.") or f.startswith("benzene.") or f.startswith("parallel_teleport.") 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/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 +} 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..5b32d6292b --- /dev/null +++ b/samples/python_interop/qir_circuit_submission_to_azure.ipynb @@ -0,0 +1,289 @@ +{ + "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", + "- `qiskit_submission_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 Bell-state circuit — two entangled qubits — to demonstrate the Qiskit path.\n" + ] + }, + { + "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.cx(0, 1)\n", + "qiskit_circuit.measure([0, 1], [0, 1])\n", + "\n", + "qiskit_circuit.draw(output=\"text\")\n" + ] + }, + { + "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", + "- **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.\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} 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 } diff --git a/samples/python_interop/submit_qiskit_circuit_to_azure.ipynb b/samples/python_interop/qiskit_submission_to_azure.ipynb similarity index 51% rename from samples/python_interop/submit_qiskit_circuit_to_azure.ipynb rename to samples/python_interop/qiskit_submission_to_azure.ipynb index f599862e8c..8489f0cfcf 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 `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", + "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)`).\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()`)." + "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()`)." ] }, { @@ -80,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." ] }, { @@ -94,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\")" ] }, @@ -133,9 +122,14 @@ "id": "cdae59f7", "metadata": {}, "source": [ - "### Approach A: Using Azure Quantum Provider\n", + "## Submit the circuit to Azure Quantum\n", + "\n", + "`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", - "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." + "- **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." ] }, { @@ -148,61 +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", - "\n", - " workspace = Workspace(\n", - " subscription_id=subscription_id,\n", - " resource_group=resource_group,\n", - " name=workspace_name,\n", - " location=location,\n", - " )\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", - " backend = provider.get_backend(target_name)\n", - " job = backend.run(circuit, shots, job_name=name)\n", - " return job.result().get_counts(circuit)\n", + "provider = AzureQuantumProvider(workspace)\n", "\n", - "provider_counts = submit_qiskit_circuit_to_azure_provider(circuit, \"rigetti.sim.qvm\", \"qiskit-provider-job\")\n", - "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" + "# 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": "ee47d6f6", + "id": "ce1b1809", "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", + "# Replace with any backend name from the list above\n", + "target_name = \"rigetti.sim.qvm\"\n", + "backend = provider.get_backend(target_name)\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", + "job = backend.run(circuit, shots=100, job_name=\"qiskit-provider-job\")\n", + "print(f\"Job {job.job_id()} submitted — waiting for results...\")\n", "\n", - "results = submit_qiskit_circuit_to_azure_via_qasm(circuit, \"rigetti.sim.qvm\", \"qiskit-via-qasm-job\")\n", - "print(results)" + "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%})\")" ] }, { @@ -265,32 +237,42 @@ "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)" + ] + }, + { + "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": { - "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,