{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "90f546a9",
   "metadata": {},
   "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 `AzureQuantumProvider` from the QDK. It also shows how to handle parameterized circuits by binding parameters prior to execution."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b838c23",
   "metadata": {},
   "source": [
    "The workflow demonstrated here:\n",
    "\n",
    "1. Build a Qiskit `QuantumCircuit`.\n",
    "2. Reference an existing Azure Quantum workspace with `qdk.azure.Workspace`.\n",
    "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()`)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "004e5982",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "\n",
    "This notebook assumes the `qdk`, Azure Quantum integration, and Qiskit packages are installed. You can install everything (including the Azure + Qiskit extras) with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dfa160e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install \"qdk[azure,qiskit]\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7f37bcd",
   "metadata": {},
   "source": [
    "This installs:\n",
    "- The base qdk package (compiler, OpenQASM/QIR tooling)\n",
    "- Azure Quantum client dependencies for submission\n",
    "- Qiskit for circuit construction\n",
    "\n",
    "After installing, restart the notebook kernel if it was already running. You can verify installation with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f7446ed1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import qiskit, qdk, qdk.azure  # should import without errors"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f761649",
   "metadata": {},
   "source": [
    "## Build a simple Qiskit circuit\n",
    "\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e91dd2d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from qiskit import QuantumCircuit\n",
    "\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\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "efd50528",
   "metadata": {},
   "source": [
    "## Configure Azure Quantum workspace connection\n",
    "\n",
    "To connect to an Azure workspace replace the following variables with your own values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "54206752",
   "metadata": {},
   "outputs": [],
   "source": [
    "subscription_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'\n",
    "resource_group = 'myresourcegroup'\n",
    "workspace_name = 'myworkspace'\n",
    "location = 'westus'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdae59f7",
   "metadata": {},
   "source": [
    "## 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",
    "- **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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9a0f59b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from qdk.azure import Workspace\n",
    "from qdk.azure.qiskit import AzureQuantumProvider\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",
    "\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",
    "job = backend.run(circuit, shots=100, job_name=\"qiskit-provider-job\")\n",
    "print(f\"Job {job.job_id()} submitted — waiting for results...\")\n",
    "\n",
    "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%})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c84df75",
   "metadata": {},
   "source": [
    "## Parameterized Qiskit circuits\n",
    "\n",
    "Many algorithms use symbolic parameters. Before submitting to azure (or before compiling to QIR), all circuit parameters must be bound to numeric values. Below we build a simple entangling ladder, apply a rotation parameterized by `θ` across all qubits, then uncompute the entanglement and measure the first qubit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d766661",
   "metadata": {},
   "outputs": [],
   "source": [
    "from qiskit import QuantumCircuit\n",
    "from qiskit.circuit import Parameter\n",
    "\n",
    "def get_parameterized_circuit(n = 5) -> QuantumCircuit:\n",
    "    theta = Parameter(\"θ\")\n",
    "    qc = QuantumCircuit(n, 1)\n",
    "    qc.h(0)\n",
    "    for i in range(n - 1):\n",
    "        qc.cx(i, i + 1)\n",
    "    qc.rz(theta, range(n))\n",
    "\n",
    "    for i in reversed(range(n - 1)):\n",
    "        qc.cx(i, i + 1)\n",
    "    qc.h(0)\n",
    "    qc.measure(0, 0)\n",
    "    return qc\n",
    "\n",
    "# Build the symbolic (parameterized) circuit\n",
    "parameterized_circuit = get_parameterized_circuit()\n",
    "\n",
    "# Bind θ to a numeric value, then visualize the bound circuit\n",
    "bound_circuit = parameterized_circuit.assign_parameters({\"θ\": 0.5})\n",
    "\n",
    "bound_circuit.draw(output=\"text\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee4d57b4",
   "metadata": {},
   "source": [
    "## Submitting a bound parameterized circuit\n",
    "\n",
    "Here we submit the `bound_circuit` from above using the recommended `AzureQuantumProvider` approach. The printed result shows the measurement counts. Change the value of `θ` (or loop over several values) to explore how the outcome distribution varies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9bb3eb0",
   "metadata": {},
   "outputs": [],
   "source": [
    "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": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
