{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "title",
      "metadata": {},
      "source": [
        "---\n",
        "title: Run quantum workloads with QRMI\n",
        "description: Use the Quantum Resource Management Interface to manage IBM Quantum workloads and run a quantum chemistry workflow from an HPC environment.\n",
        "---\n",
        "\n",
        "{/* cspell:ignore QRMI SPANK GRES Slurm LUCJ CCSD pvdz hcore Pellegrini rustup cregs CUDA SBATCH dotenv */}\n",
        "\n",
        "# Run quantum workloads with QRMI\n",
        "\n",
        "*Usage estimate: under one minute on IBM Quantum® hardware for the SQD section. This estimate excludes queue time and classical processing; runtime can vary.*\n",
        "\n",
        "## Learning outcomes\n",
        "\n",
        "1. The role QRMI plays as middleware between HPC schedulers and IBM Quantum hardware\n",
        "2. How to use the core QRMI lifecycle (`acquire` → `task_start` → `task_status` → `task_result` → `release`) against a real IBM® backend\n",
        "3. How to use the higher-level Qiskit `SamplerV2` and `QRMIService` wrappers on top of QRMI\n",
        "4. How HPC schedulers (Slurm) inject quantum resources via environment variables and how applications consume them\n",
        "5. How to run a complete SQD (Sample-based Quantum Diagonalization) chemistry workflow on N$_2$ by using IBM hardware through QRMI\n",
        "\n",
        "## Prerequisites\n",
        "\n",
        "* [Qiskit primitives (Sampler and Estimator)](/docs/guides/primitives)\n",
        "* [IBM Quantum sessions](/docs/guides/run-jobs-session)\n",
        "* [IBM Quantum transpilation](/docs/guides/transpile)\n",
        "* [Sample-based quantum diagonalization (SQD)](/docs/tutorials/sample-based-quantum-diagonalization)\n",
        "* Basic familiarity with Python virtual environments and quantum chemistry\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "background",
      "metadata": {},
      "source": [
        "## Background\n",
        "\n",
        "### The quantum-HPC integration challenge\n",
        "\n",
        "High-performance computing (HPC) workflows often require seamless coordination between classical compute clusters and quantum processing units (QPUs). Different quantum hardware backends and services expose distinct authentication mechanisms, wire formats, and job lifecycle APIs. Integrating IBM Quantum systems into HPC workload managers (such as Slurm) requires a clean, standard interface for resource acquisition, job execution, and session management.\n",
        "\n",
        "### What QRMI is\n",
        "\n",
        "The **Quantum Resource Management Interface (QRMI)** is a middleware library written in Rust that standardizes access to quantum hardware from HPC schedulers and classical applications. It exposes a single unified lifecycle API:\n",
        "\n",
        "```\n",
        "┌─────────────────────────────────────────────────────────────────┐\n",
        "│                     HPC Application Layer                       │\n",
        "│          (Slurm job script / Python workflow / CUDA-Q)          │\n",
        "└───────────────────────────┬─────────────────────────────────────┘\n",
        "                            │  QRMI API\n",
        "                            │  acquire() / task_start() / task_result() / release()\n",
        "┌───────────────────────────▼─────────────────────────────────────┐\n",
        "│                        QRMI Core (Rust)                         │\n",
        "│            Python bindings · C bindings · Lua bindings          │\n",
        "└───────────────────────────┬─────────────────────────────────────┘\n",
        "                            │\n",
        "               IBM Quantum Compute Service / IBM Quantum System\n",
        "```\n",
        "\n",
        "QRMI is published as an open-source project at [github.com/qiskit-community/qrmi](https://github.com/qiskit-community/qrmi) and is described in the overview paper [arXiv:2506.10052](https://arxiv.org/abs/2506.10052).\n",
        "\n",
        "### Key design choices\n",
        "\n",
        "**Resource lifecycle, not circuit compilation.** QRMI handles the acquire/submit/poll/release lifecycle and nothing else. Circuit compilation, optimization, and transpilation remain in the application layer (for example, Qiskit). This keeps the interface minimal and composable.\n",
        "\n",
        "**Vendor portability model.** While QRMI provides common job-management calls (`acquire`, `task_start`, `task_status`, `task_result`, `release`) across supported hardware backends, changing vendors also requires different compilation passes, vendor-specific payload construction, and result decoding in the application layer.\n",
        "\n",
        "**Native IBM payload format.** For IBM Quantum backends, QRMI uses OpenQASM 3 JSON payloads (`QiskitPrimitive`) conforming to the Qiskit Runtime schema.\n",
        "\n",
        "**Configuration through environment variables.** Credentials and endpoint URLs are read from environment variables at runtime. In an HPC cluster, the Slurm QRMI SPANK plugin sets these automatically when a job is dispatched. In a notebook or interactive session, you load them from a `.env` file. Application code never contains hardcoded credentials or endpoint URLs.\n",
        "\n",
        "**HPC scheduler integration through GRES.** When a Slurm job requests quantum resources using the QRMI SPANK plugin interface (`#SBATCH --gres=qpu:1` and `#SBATCH --qpu=ibm_kingston`), the plugin injects `QRMI_JOB_QPU_RESOURCES` and `QRMI_JOB_QPU_TYPES` into the job environment. Applications call `get_job_qpu_resources_and_types()` to discover which resources were allocated — no hardcoded backend names required. `QRMIService` wraps this pattern for Qiskit users.\n",
        "\n",
        "### The core API calls\n",
        "\n",
        "| Call                       | Purpose                                                                                       |\n",
        "| -------------------------- | --------------------------------------------------------------------------------------------- |\n",
        "| `qrmi.acquire()`           | Acquire access to the resource (for example, opens a dedicated session); returns a lock token |\n",
        "| `qrmi.target()`            | Retrieve backend capabilities (qubits, gates, coupling map) as JSON                           |\n",
        "| `qrmi.task_start(payload)` | Submit a quantum job; returns a job ID                                                        |\n",
        "| `qrmi.task_status(job_id)` | Poll job status (`Queued`, `Running`, `Completed`, `Failed`)                                  |\n",
        "| `qrmi.task_result(job_id)` | Retrieve completed job results as a raw JSON string                                           |\n",
        "| `qrmi.task_stop(job_id)`   | Cancel or clean up a job                                                                      |\n",
        "| `qrmi.release(lock)`       | Release the resource lock (for example, closes the session)                                   |\n",
        "\n",
        "### What this tutorial covers\n",
        "\n",
        "This tutorial is structured in two parts:\n",
        "\n",
        "**Steps 1–3 (small-scale examples):** Introduce the QRMI API with a simple Bell state circuit demonstration on IBM Quantum hardware, covering direct low-level primitive usage as well as high-level `QRMIService` and `SamplerV2` integration.\n",
        "\n",
        "**Large-scale hardware example:** A complete SQD workflow for the N$_2$ molecule at a bond distance of 1.0 $\\AA$ (cc-pVDZ basis active space, 26 spatial orbitals / 52 qubits), executed on IBM Quantum hardware through QRMI. SQD combines quantum sampling of a LUCJ ansatz constructed with `ffsim` and self-consistent configuration recovery with `qiskit-addon-sqd`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "requirements",
      "metadata": {},
      "source": [
        "## Requirements\n",
        "\n",
        "Before starting this tutorial, be sure you have the following installed.\n",
        "\n",
        "### Python environment setup\n",
        "\n",
        "Pre-built binary wheels are available for Linux on PyPI, so standard `pip install` works directly on Linux/HPC systems.\n",
        "\n",
        "```bash\n",
        "python3 -m venv ~/.venvs/qrmi-ibm\n",
        "source ~/.venvs/qrmi-ibm/bin/activate\n",
        "python -m pip install \"qrmi[ibm]\" python-dotenv pyscf ffsim qiskit-addon-sqd matplotlib ipykernel\n",
        "python -m ipykernel install --user --name qrmi-ibm --display-name \"QRMI IBM\"\n",
        "```\n",
        "\n",
        "<Admonition type=\"note\" title=\"Platforms without pre-built wheels\">\n",
        "  If `pip` builds QRMI from source, ensure you have a modern Rust toolchain (Rust ≥ 1.91.1 installed via `rustup` from [rustup.rs](https://rustup.rs)).\n",
        "</Admonition>\n",
        "\n",
        "Select the **QRMI IBM** kernel in Jupyter, then restart it and run the notebook cells in order. The saved outputs are from the contributor's hardware run; installation commands do not specify the exact versions used for that run.\n",
        "\n",
        "### Credentials required\n",
        "\n",
        "* IBM Quantum: IAM API key and Service CRN from [IBM Quantum Platform]()\n",
        "\n",
        "For standalone execution, create a `.env` file next to this notebook with the following values, replacing the credential placeholders. Keep this file private. If you select a different backend, update both its name and the environment variable prefixes.\n",
        "\n",
        "```dotenv\n",
        "ibm_kingston_QRMI_IBM_QCS_ENDPOINT=https://quantum.cloud.ibm.com/api/v1\n",
        "ibm_kingston_QRMI_IBM_QCS_IAM_ENDPOINT=https://iam.cloud.ibm.com\n",
        "ibm_kingston_QRMI_IBM_QCS_IAM_APIKEY=<your-iam-api-key>\n",
        "ibm_kingston_QRMI_IBM_QCS_SERVICE_CRN=<your-crn-starting-with-crn:v1:>\n",
        "ibm_kingston_QRMI_IBM_QCS_SESSION_MODE=dedicated\n",
        "ibm_kingston_QRMI_IBM_QCS_SESSION_MAX_TTL=28800\n",
        "QRMI_JOB_QPU_RESOURCES=ibm_kingston\n",
        "QRMI_JOB_QPU_TYPES=ibm-quantum-compute-service\n",
        "```\n",
        "\n",
        "For a Slurm allocation, use the resource settings and credentials supplied by the cluster. The notebook preserves existing environment values.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "setup-header",
      "metadata": {},
      "source": [
        "## Setup\n",
        "\n",
        "Import the dependencies and load the resource configuration.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "setup-imports",
      "metadata": {
        "tags": [
          "environment-ibm"
        ]
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Backend: ibm_kingston\n",
            "Environment ready.\n"
          ]
        }
      ],
      "source": [
        "import os\n",
        "import time\n",
        "import json\n",
        "import numpy as np\n",
        "from dotenv import load_dotenv\n",
        "\n",
        "from qrmi import (\n",
        "    QuantumResource,\n",
        "    ResourceType,\n",
        "    Payload,\n",
        "    TaskStatus,\n",
        "    get_job_qpu_resources_and_types,\n",
        ")\n",
        "from qrmi.primitives import QRMIService\n",
        "from qrmi.primitives.ibm import SamplerV2, get_target\n",
        "\n",
        "from qiskit import QuantumCircuit, qasm3\n",
        "from qiskit.circuit.library import efficient_su2\n",
        "from qiskit.primitives.containers.sampler_pub import SamplerPub\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "\n",
        "# Load credentials from .env without overriding already-set scheduler environment variables\n",
        "load_dotenv(override=False)\n",
        "\n",
        "# Preserve resources if injected by Slurm SPANK plugin; fallback to default for interactive run\n",
        "BACKEND_NAME = os.environ.get(\"QRMI_JOB_QPU_RESOURCES\", \"ibm_kingston\")\n",
        "os.environ.setdefault(\"QRMI_JOB_QPU_RESOURCES\", BACKEND_NAME)\n",
        "os.environ.setdefault(\"QRMI_JOB_QPU_TYPES\", \"ibm-quantum-compute-service\")\n",
        "\n",
        "print(f\"Backend: {BACKEND_NAME}\")\n",
        "print(\"Environment ready.\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "small-scale-header",
      "metadata": {},
      "source": [
        "## Small-scale examples\n",
        "\n",
        "Steps 1–3 introduce the QRMI API by using simple circuits. Each step maps to a core phase of the QRMI lifecycle against IBM Quantum hardware.\n",
        "\n",
        "The payload for these initial steps is a small Bell state circuit chosen to be fast and inexpensive to run.\n",
        "\n",
        "These examples use hardware because they demonstrate remote resource allocation and job management. A local circuit simulator does not validate the QRMI service and scheduler integration. Running this notebook submits IBM Quantum jobs and requires access to the configured backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step1-header",
      "metadata": {},
      "source": [
        "### Step 1: Map the classical problem to a quantum resource\n",
        "\n",
        "The first step in any QRMI workflow is to create a `QuantumResource` object and verify that it is accessible.\n",
        "\n",
        "`get_target()` retrieves the backend's hardware description (qubit count, basis gates, coupling map) and packages it as a Qiskit `Target` object, which the transpiler uses in Step 2.\n",
        "\n",
        "### Step 2: Optimize the problem for quantum hardware execution\n",
        "\n",
        "Before submission, use Qiskit to transpile the circuit to the backend's instruction set architecture (ISA), using the `Target` object retrieved in Step 1.\n",
        "\n",
        "The example then builds a `Payload.QiskitPrimitive`, which wraps the OpenQASM 3 circuit string and job metadata into the IBM primitive schema.\n",
        "\n",
        "### Step 3: Execute using QRMI primitives\n",
        "\n",
        "With the payload built, the example submits the job and polls for completion. `task_start()` returns a job ID immediately; `task_status()` is polled until the status is no longer `Queued`/`Running`. Results are retrieved as a raw JSON string and parsed to extract measurement samples.\n",
        "\n",
        "The following cell keeps acquisition, execution, and cleanup together so that failures after acquisition still release a notebook-owned session.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "step1-ibm",
      "metadata": {
        "tags": [
          "environment-ibm"
        ]
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Resource id:   ibm_kingston\n",
            "Resource type: ResourceType.IBMQuantumComputeService\n",
            "Accessible:    True\n",
            "Lock token:    2ff43011-aed1-4436-a4df-40f37ec588b7\n",
            "\n",
            "Backend: ibm_kingston\n",
            "Qubits:  156\n",
            "Gates:   ['cz', 'id', 'rx', 'rz', 'rzz', 'sx', 'x', 'xslow']\n",
            "        ┌───┐      ░ ┌─┐   \n",
            "   q_0: ┤ H ├──■───░─┤M├───\n",
            "        └───┘┌─┴─┐ ░ └╥┘┌─┐\n",
            "   q_1: ─────┤ X ├─░──╫─┤M├\n",
            "             └───┘ ░  ║ └╥┘\n",
            "meas: 2/══════════════╩══╩═\n",
            "                      0  1 \n",
            "\n",
            "Transpiled gate counts: OrderedDict([('rz', 6), ('sx', 3), ('measure', 2), ('cz', 1), ('barrier', 1)])\n",
            "Payload ready\n",
            "Job submitted: dai43g8mhr3c73e7a7o0\n",
            "  Status: TaskStatus.Queued\n",
            "  Status: TaskStatus.Running\n",
            "  Status: TaskStatus.Completed\n",
            "\n",
            "Final status: TaskStatus.Completed\n",
            "\n",
            "Measurement counts: {'11': 487, '00': 254, '01': 177, '10': 106}\n",
            "\n",
            "Session released.\n"
          ]
        }
      ],
      "source": [
        "# ── IBM Quantum ───────────────────────────────────────────────────────\n",
        "qrmi = QuantumResource(BACKEND_NAME, ResourceType.IBMQuantumComputeService)\n",
        "# ResourceType.IBMQuantumSystem is the alternative for directly provisioned systems\n",
        "\n",
        "print(f\"Resource id:   {qrmi.resource_id()}\")\n",
        "print(f\"Resource type: {qrmi.resource_type()}\")\n",
        "print(f\"Accessible:    {qrmi.is_accessible()}\")\n",
        "\n",
        "# Acquire exclusive access — open try/finally immediately so every\n",
        "# subsequent failure (target retrieval, transpilation, submission) is covered.\n",
        "# Release is skipped when running under Slurm: the SPANK plugin owns the\n",
        "# session lifecycle and will release it when the job finishes.\n",
        "lock = qrmi.acquire()\n",
        "print(f\"Lock token:    {lock}\")\n",
        "try:\n",
        "    # Retrieve backend capabilities\n",
        "    transpiler_target = get_target(\n",
        "        qrmi\n",
        "    )  # calls qrmi.target() and parses the JSON\n",
        "    target_json = json.loads(qrmi.target().value)\n",
        "    config = target_json.get(\"configuration\", {})\n",
        "    print(f\"\\nBackend: {config.get('backend_name', 'unknown')}\")\n",
        "    print(f\"Qubits:  {config.get('n_qubits', 'unknown')}\")\n",
        "    print(f\"Gates:   {config.get('basis_gates', [])}\")\n",
        "\n",
        "    # ── IBM Quantum ───────────────────────────────────────────────────\n",
        "\n",
        "    # Build a Bell state circuit\n",
        "    qc = QuantumCircuit(2)\n",
        "    qc.h(0)\n",
        "    qc.cx(0, 1)\n",
        "    qc.measure_all()\n",
        "    print(qc.draw(\"text\"))\n",
        "\n",
        "    # Transpile to ISA using the target retrieved in Step 1\n",
        "    pm = generate_preset_pass_manager(\n",
        "        optimization_level=1, target=transpiler_target\n",
        "    )\n",
        "    isa_circuit = pm.run(qc)\n",
        "    print(f\"\\nTranspiled gate counts: {isa_circuit.count_ops()}\")\n",
        "\n",
        "    # Build the QRMI payload\n",
        "    # Payload.QiskitPrimitive wraps the IBM SamplerV2 input schema:\n",
        "    #   pubs: list of [qasm3_string, parameter_values]  (shots goes at top level)\n",
        "    #   program_id: \"sampler\" or \"estimator\"\n",
        "    shots = 1024\n",
        "    pub = SamplerPub.coerce((isa_circuit,), shots)\n",
        "    qasm3_str = qasm3.dumps(\n",
        "        pub.circuit,\n",
        "        disable_constants=True,\n",
        "        allow_aliasing=True,\n",
        "        experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1,\n",
        "    )\n",
        "    # Parameter values as a flat list (empty for non-parametric circuits)\n",
        "    param_array = pub.parameter_values.as_array(\n",
        "        pub.circuit.parameters\n",
        "    ).tolist()\n",
        "\n",
        "    input_json = {\n",
        "        \"pubs\": [\n",
        "            [qasm3_str, param_array]\n",
        "        ],  # list-of-lists; shots at top level\n",
        "        \"version\": 2,\n",
        "        \"support_qiskit\": False,  # True returns binary-encoded Qiskit result\n",
        "        \"shots\": shots,\n",
        "    }\n",
        "    payload = Payload.QiskitPrimitive(\n",
        "        input=json.dumps(input_json), program_id=\"sampler\"\n",
        "    )\n",
        "    print(\"Payload ready\")\n",
        "\n",
        "    # ── IBM Quantum ───────────────────────────────────────────────────\n",
        "\n",
        "    # Submit the job\n",
        "    job_id = qrmi.task_start(payload)\n",
        "    print(f\"Job submitted: {job_id}\")\n",
        "\n",
        "    # Poll until complete\n",
        "    while True:\n",
        "        status = qrmi.task_status(job_id)\n",
        "        print(f\"  Status: {status}\")\n",
        "        if status not in [TaskStatus.Running, TaskStatus.Queued]:\n",
        "            break\n",
        "        time.sleep(5)\n",
        "\n",
        "    print(f\"\\nFinal status: {status}\")\n",
        "\n",
        "    # Retrieve results\n",
        "    # support_qiskit=False → plain JSON; parse directly without ResultDecoder\n",
        "    if status == TaskStatus.Completed:\n",
        "        raw = qrmi.task_result(job_id).value\n",
        "        result = json.loads(raw)\n",
        "        # IBM QCS plain-JSON result shape: {\"results\": [{\"data\": {\"meas\": {\"samples\": [...]}}}]}\n",
        "        # samples is a list of hex-encoded integers; decode to zero-padded bitstrings\n",
        "        samples = result[\"results\"][0][\"data\"][\"meas\"][\"samples\"]\n",
        "        num_bits = sum(reg.size for reg in isa_circuit.cregs)\n",
        "        from collections import Counter\n",
        "\n",
        "        counts = Counter(format(int(s, 16), f\"0{num_bits}b\") for s in samples)\n",
        "        print(f\"\\nMeasurement counts: {dict(counts.most_common(8))}\")\n",
        "        qrmi.task_stop(job_id)\n",
        "    else:\n",
        "        print(f\"Job did not complete. Logs:\\n{qrmi.task_logs(job_id)}\")\n",
        "\n",
        "finally:\n",
        "    # Release only in interactive sessions; under Slurm the SPANK plugin\n",
        "    # manages the session lifecycle and calling release() here would\n",
        "    # prematurely close a session it does not own.\n",
        "    if not os.environ.get(\"SLURM_JOB_ID\"):\n",
        "        qrmi.release(lock)\n",
        "        print(\"\\nSession released.\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "higher-level-header",
      "metadata": {},
      "source": [
        "### Higher-level Qiskit interface: QRMIService and SamplerV2\n",
        "\n",
        "The raw lifecycle above provides explicit control over every call. For standard Qiskit workflows, QRMI provides a `SamplerV2` primitive implementing `BaseSamplerV2`.\n",
        "\n",
        "<Admonition type=\"note\" title=\"Lifecycle management\">\n",
        "  `SamplerV2` handles payload serialization, submission (`task_start`), polling, and result decoding. In an HPC batch setting (for example, with Slurm), allocation and release are managed by the scheduler and the SPANK plugin. In an interactive Python session using direct low-level API objects, `acquire()` and `release()` can be used to explicitly manage dedicated sessions.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "higher-level-sampler",
      "metadata": {
        "tags": [
          "environment-ibm"
        ]
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Using: ibm_kingston (ResourceType.IBMQuantumComputeService)\n",
            "Job ID: dai43jj9k43c73afhrhg | Status: JobStatus.QUEUED\n",
            "Counts (first 5): {'00010': 66, '00100': 28, '11000': 71, '00110': 23, '10100': 14}\n"
          ]
        }
      ],
      "source": [
        "# QRMIService reads QRMI_JOB_QPU_RESOURCES / QRMI_JOB_QPU_TYPES set in Setup or Slurm\n",
        "service = QRMIService()\n",
        "qrmi_svc = service.resources()[0]\n",
        "print(f\"Using: {qrmi_svc.resource_id()} ({qrmi_svc.resource_type()})\")\n",
        "\n",
        "# Build an EfficientSU2 circuit\n",
        "circuit = efficient_su2(5, entanglement=\"linear\")\n",
        "circuit.measure_all()\n",
        "param_values = np.random.rand(circuit.num_parameters)\n",
        "\n",
        "pm = generate_preset_pass_manager(\n",
        "    optimization_level=1, target=get_target(qrmi_svc)\n",
        ")\n",
        "isa_circuit = pm.run(circuit)\n",
        "\n",
        "# SamplerV2 executes jobs against the QRMI resource and decodes results into primitive containers\n",
        "sampler = SamplerV2(qrmi_svc, options={\"default_shots\": 1024})\n",
        "job = sampler.run([(isa_circuit, param_values)])\n",
        "print(f\"Job ID: {job.job_id()} | Status: {job.status()}\")\n",
        "\n",
        "# Poll with retry — re-raise immediately on permanent failures;\n",
        "# only retry on transient network/timeout errors (connection resets, 503s).\n",
        "_TRANSIENT = (\n",
        "    \"503\",\n",
        "    \"Service Unavailable\",\n",
        "    \"ConnectionError\",\n",
        "    \"TimeoutError\",\n",
        "    \"timed out\",\n",
        "    \"Connection reset\",\n",
        ")\n",
        "result = None\n",
        "for attempt in range(60):\n",
        "    try:\n",
        "        result = job.result()  # blocks until complete\n",
        "        break\n",
        "    except Exception as e:\n",
        "        if not any(tok in str(e) for tok in _TRANSIENT):\n",
        "            raise\n",
        "        print(f\"  Transient error on attempt {attempt + 1}: {e}\")\n",
        "        time.sleep(10)\n",
        "\n",
        "if result is not None:\n",
        "    counts = result[0].data.meas.get_counts()\n",
        "    print(f\"Counts (first 5): {dict(list(counts.items())[:5])}\")\n",
        "else:\n",
        "    print(\"Job did not complete after retries.\")\n",
        "\n",
        "if job.errored():\n",
        "    print(f\"Logs:\\n{job.logs()}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "hpc-header",
      "metadata": {},
      "source": [
        "### HPC context: Slurm resource injection\n",
        "\n",
        "In an HPC cluster, users request quantum resources using Slurm GRES syntax along with the QRMI SPANK plugin options. The plugin handles credential and resource injection automatically:\n",
        "\n",
        "```bash\n",
        "#SBATCH --gres=qpu:1\n",
        "#SBATCH --qpu=ibm_kingston\n",
        "python my_workflow.py   # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already set\n",
        "```\n",
        "\n",
        "Application code discovers its allocated resources at runtime — no hardcoded backend names:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "hpc-discovery",
      "metadata": {
        "tags": [
          "environment-ibm"
        ]
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Resources allocated by scheduler:\n",
            "  ibm_kingston  (ibm-quantum-compute-service)\n",
            "\n",
            "QRMIService found: ibm_kingston  accessible=True\n"
          ]
        }
      ],
      "source": [
        "# get_job_qpu_resources_and_types() reads QRMI_JOB_QPU_RESOURCES / QRMI_JOB_QPU_TYPES\n",
        "# set by the Slurm SPANK plugin (or manually above in Setup)\n",
        "qpus, qpu_types = get_job_qpu_resources_and_types()\n",
        "print(\"Resources allocated by scheduler:\")\n",
        "for qpu, qpu_type in zip(qpus, qpu_types):\n",
        "    print(f\"  {qpu}  ({qpu_type})\")\n",
        "\n",
        "# QRMIService wraps this into a list of ready QuantumResource objects\n",
        "for r in QRMIService().resources():\n",
        "    print(\n",
        "        f\"\\nQRMIService found: {r.resource_id()}  accessible={r.is_accessible()}\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "large-scale-header",
      "metadata": {},
      "source": [
        "## Large-scale hardware example: SQD on N$_2$\n",
        "\n",
        "Here we put all components together into a complete quantum chemistry workflow at a larger scale, executed on real IBM Quantum hardware through QRMI.\n",
        "\n",
        "**SQD** combines the following:\n",
        "\n",
        "1. Quantum sampling of a Local Unitary Cluster Jastrow (LUCJ) ansatz constructed using `ffsim` and initialized from CCSD amplitudes\n",
        "2. Hardware-aware transpilation matching the heavy-hex lattice topology via `generate_lucj_pass_manager`\n",
        "3. Sampling execution on IBM Quantum hardware managed through `QRMIService` and QRMI `SamplerV2`\n",
        "4. Classical post-processing: self-consistent configuration recovery and iterative subspace diagonalization using `qiskit-addon-sqd`\n",
        "\n",
        "We apply SQD to N$_2$ at a bond distance of 1.0 $\\AA$ with an active space derived from the `cc-pVDZ` basis set (26 spatial orbitals, corresponding to 52 spin-orbitals/qubits).\n",
        "\n",
        "**Reference energy for N$_2$ /cc-pVDZ active space (bond distance 1.0 $\\AA$):**\n",
        "\n",
        "* Reference energy (separate SCI calculation): **−109.22802922 Ha**\n",
        "\n",
        "<Admonition type=\"note\" title=\"Accuracy of the saved run\">\n",
        "  The SQD run below demonstrates successful end-to-end QRMI execution on IBM Quantum hardware. With a single LUCJ repetition and 100,000 shots the result finishes approximately 23.7 kcal/mol above the reference energy and does not achieve chemical accuracy (≤ 1 kcal/mol). Changing `n_reps`, the shot count, or the number of SQD iterations might improve accuracy, but requires further testing.\n",
        "</Admonition>\n",
        "\n",
        "In the saved run, the `ffsim` pass manager removed the opposite-spin interactions `(24, 24)` and `(20, 20)` because the backend could not accommodate them. The reported results use this adjusted circuit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "large-scale-all",
      "metadata": {
        "tags": [
          "environment-ibm"
        ]
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "WARN: Unable to to identify input symmetry using original axes.\n",
            "Different symmetry axes will be used.\n",
            "\n",
            "converged SCF energy = -108.929838385609\n",
            "N₂/cc-pVDZ active space: 26 orbitals (52 qubits), (5, 5) electrons\n",
            "SCF energy:       -108.92983839 Ha\n",
            "Reference energy: -109.22802922 Ha\n",
            "E(CCSD) = -109.2177884185545  E_corr = -0.2879500329450047\n",
            "CCSD energy:      -109.21778842 Ha\n",
            "Using QRMI resource: ibm_kingston\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "LUCJ circuit: 52 qubits, depth 3\n",
            "Transpiled gate counts: OrderedDict([('sx', 7041), ('rz', 6969), ('cz', 1858), ('measure', 52), ('x', 47), ('barrier', 1)])\n",
            "Job submitted via QRMI: dai43o0mhr3c73e7a81g | Status: JobStatus.QUEUED\n",
            "Waiting for results from hardware...\n",
            "Total shots collected: 100000\n",
            "Fraction of valid configurations sampled: 0.00319\n",
            "Expected fraction from uniform random:     9.6079e-07\n",
            "\n",
            "Running SQD post-processing...\n",
            "Iteration 1\n",
            "  Subsample 0: Energy = -109.09341960 Ha | Subspace dim = 208849\n",
            "  Subsample 1: Energy = -109.11738590 Ha | Subspace dim = 204304\n",
            "  Subsample 2: Energy = -109.09947704 Ha | Subspace dim = 212521\n",
            "Iteration 2\n",
            "  Subsample 0: Energy = -109.16015998 Ha | Subspace dim = 332929\n",
            "  Subsample 1: Energy = -109.16823702 Ha | Subspace dim = 319225\n",
            "  Subsample 2: Energy = -109.16189785 Ha | Subspace dim = 336400\n",
            "Iteration 3\n",
            "  Subsample 0: Energy = -109.17759299 Ha | Subspace dim = 471969\n",
            "  Subsample 1: Energy = -109.17937442 Ha | Subspace dim = 512656\n",
            "  Subsample 2: Energy = -109.17970409 Ha | Subspace dim = 504100\n",
            "Iteration 4\n",
            "  Subsample 0: Energy = -109.18410905 Ha | Subspace dim = 608400\n",
            "  Subsample 1: Energy = -109.18265405 Ha | Subspace dim = 636804\n",
            "  Subsample 2: Energy = -109.18608430 Ha | Subspace dim = 657721\n",
            "Iteration 5\n",
            "  Subsample 0: Energy = -109.18870837 Ha | Subspace dim = 846400\n",
            "  Subsample 1: Energy = -109.18890818 Ha | Subspace dim = 848241\n",
            "  Subsample 2: Energy = -109.19022232 Ha | Subspace dim = 804609\n",
            "\n",
            "=== Energy Summary (N₂/cc-pVDZ active space) ===\n",
            "SCF energy:       -108.92983839 Ha\n",
            "Reference energy: -109.22802922 Ha\n",
            "Final SQD energy: -109.19022232 Ha\n",
            "Energy error:     0.03780690 Ha (23.7238 kcal/mol)\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/run-quantum-workloads-with-qrmi/extracted-outputs/large-scale-all-3.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "from qrmi.primitives.ibm import get_backend\n",
        "import math\n",
        "import os\n",
        "import time\n",
        "from functools import partial\n",
        "from dotenv import load_dotenv\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "import pyscf\n",
        "import pyscf.gto\n",
        "import pyscf.scf\n",
        "import pyscf.cc\n",
        "import pyscf.mcscf\n",
        "import pyscf.ao2mo\n",
        "\n",
        "import ffsim\n",
        "import ffsim.qiskit\n",
        "from qiskit import QuantumCircuit, QuantumRegister\n",
        "from qiskit_addon_sqd.fermion import (\n",
        "    SCIResult,\n",
        "    diagonalize_fermionic_hamiltonian,\n",
        "    solve_sci_batch,\n",
        ")\n",
        "from qrmi.primitives import QRMIService\n",
        "from qrmi.primitives.ibm import SamplerV2, get_target\n",
        "\n",
        "load_dotenv(override=False)\n",
        "os.environ.setdefault(\"QRMI_JOB_QPU_RESOURCES\", \"ibm_kingston\")\n",
        "os.environ.setdefault(\"QRMI_JOB_QPU_TYPES\", \"ibm-quantum-compute-service\")\n",
        "\n",
        "# ── Step 1: Map classical inputs to a quantum problem ─────────────────\n",
        "\n",
        "# Build N2 molecule at 1.0 Å bond distance\n",
        "mol = pyscf.gto.Mole()\n",
        "mol.build(\n",
        "    atom=[[\"N\", (0, 0, 0)], [\"N\", (1.0, 0, 0)]],\n",
        "    basis=\"cc-pvdz\",\n",
        "    symmetry=\"Dooh\",\n",
        ")\n",
        "\n",
        "# Define active space: freeze 2 core orbitals\n",
        "n_frozen = 2\n",
        "active_space = range(n_frozen, mol.nao_nr())\n",
        "\n",
        "# Get molecular integrals\n",
        "scf = pyscf.scf.RHF(mol).run()\n",
        "norb = len(active_space)\n",
        "n_electrons = int(sum(scf.mo_occ[active_space]))\n",
        "n_alpha = (n_electrons + mol.spin) // 2\n",
        "n_beta = (n_electrons - mol.spin) // 2\n",
        "nelec = (n_alpha, n_beta)\n",
        "\n",
        "cas = pyscf.mcscf.CASCI(scf, norb, nelec)\n",
        "mo = cas.sort_mo(active_space, base=0)\n",
        "hcore, nuclear_repulsion_energy = cas.get_h1cas(mo)\n",
        "eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), norb)\n",
        "\n",
        "# Reference energy from external SCI calculation\n",
        "reference_energy = -109.22802921665716\n",
        "\n",
        "print(\n",
        "    f\"N₂/cc-pVDZ active space: {norb} orbitals ({2 * norb} qubits), {nelec} electrons\"\n",
        ")\n",
        "print(f\"SCF energy:       {scf.e_tot:.8f} Ha\")\n",
        "print(f\"Reference energy: {reference_energy:.8f} Ha\")\n",
        "\n",
        "# Get CCSD amplitudes for initializing the LUCJ ansatz\n",
        "ccsd = pyscf.cc.CCSD(\n",
        "    scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]\n",
        ").run()\n",
        "t1 = ccsd.t1\n",
        "t2 = ccsd.t2\n",
        "print(f\"CCSD energy:      {ccsd.e_tot:.8f} Ha\")\n",
        "\n",
        "# Discover backend via QRMIService (QRMI_JOB_QPU_RESOURCES set in Setup)\n",
        "service = QRMIService()\n",
        "qrmi_sqd = service.resources()[0]\n",
        "print(f\"Using QRMI resource: {qrmi_sqd.resource_id()}\")\n",
        "\n",
        "# get_backend() wraps the QRMI resource as a Qiskit backend for layout synthesis\n",
        "\n",
        "backend = get_backend(qrmi_sqd)\n",
        "\n",
        "# Set ansatz properties\n",
        "n_reps = 1\n",
        "pairs_aa = [(p, p + 1) for p in range(norb - 1)]\n",
        "pairs_ab = None\n",
        "\n",
        "# Create pass manager adapted to hardware heavy-hex topology\n",
        "pass_manager, pairs_ab = ffsim.qiskit.generate_lucj_pass_manager(\n",
        "    backend=backend,\n",
        "    norb=norb,\n",
        "    connectivity=\"heavy-hex\",\n",
        "    interaction_pairs=(pairs_aa, pairs_ab),\n",
        "    optimization_level=3,\n",
        ")\n",
        "\n",
        "# Create the compressed LUCJ ansatz operator\n",
        "ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(\n",
        "    t2=t2,\n",
        "    t1=t1,\n",
        "    n_reps=n_reps,\n",
        "    interaction_pairs=(pairs_aa, pairs_ab),\n",
        "    optimize=True,\n",
        "    options=dict(maxiter=1000),\n",
        ")\n",
        "\n",
        "# Assemble the circuit\n",
        "qubits = QuantumRegister(2 * norb, name=\"q\")\n",
        "circuit = QuantumCircuit(qubits)\n",
        "circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)\n",
        "circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)\n",
        "circuit.measure_all()\n",
        "print(f\"LUCJ circuit: {circuit.num_qubits} qubits, depth {circuit.depth()}\")\n",
        "\n",
        "# ── Step 2: Optimize for quantum hardware execution ───────────────────\n",
        "\n",
        "isa_circuit = pass_manager.run(circuit)\n",
        "print(f\"Transpiled gate counts: {isa_circuit.count_ops()}\")\n",
        "\n",
        "# ── Step 3: Execute using Qiskit primitives (QRMI SamplerV2) ─────────\n",
        "\n",
        "sampler = SamplerV2(qrmi_sqd, options={\"default_shots\": 100_000})\n",
        "# sampler.options.environment.job_tags = [\"TUT_SQD\"]\n",
        "job = sampler.run([(isa_circuit,)])\n",
        "print(f\"Job submitted via QRMI: {job.job_id()} | Status: {job.status()}\")\n",
        "print(\"Waiting for results from hardware...\")\n",
        "\n",
        "_TRANSIENT = (\n",
        "    \"503\",\n",
        "    \"Service Unavailable\",\n",
        "    \"ConnectionError\",\n",
        "    \"TimeoutError\",\n",
        "    \"timed out\",\n",
        "    \"Connection reset\",\n",
        ")\n",
        "primitive_result = None\n",
        "for attempt in range(120):\n",
        "    try:\n",
        "        primitive_result = job.result()\n",
        "        break\n",
        "    except Exception as e:\n",
        "        if not any(tok in str(e) for tok in _TRANSIENT):\n",
        "            raise\n",
        "        print(f\"  Transient error on attempt {attempt + 1}: {e}\")\n",
        "        time.sleep(10)\n",
        "\n",
        "if primitive_result is None:\n",
        "    raise RuntimeError(\"Job did not complete after retries\")\n",
        "\n",
        "pub_result = primitive_result[0]\n",
        "bit_array = pub_result.data.meas\n",
        "print(f\"Total shots collected: {bit_array.num_shots}\")\n",
        "\n",
        "# ── Step 4: Post-process and return result in classical format ────────\n",
        "\n",
        "\n",
        "def is_valid_bitstring(\n",
        "    bitstring: str, norb: int, nelec: tuple[int, int]\n",
        ") -> bool:\n",
        "    n_a, n_b = nelec\n",
        "    return (\n",
        "        len(bitstring) == 2 * norb\n",
        "        and bitstring[norb:].count(\"1\") == n_a\n",
        "        and bitstring[:norb].count(\"1\") == n_b\n",
        "    )\n",
        "\n",
        "\n",
        "num_valid = sum(\n",
        "    is_valid_bitstring(b, norb, nelec) for b in bit_array.get_bitstrings()\n",
        ")\n",
        "valid_fraction = num_valid / bit_array.num_shots\n",
        "expected_random = (\n",
        "    math.comb(norb, n_alpha) * math.comb(norb, n_beta) / (2 ** (2 * norb))\n",
        ")\n",
        "print(f\"Fraction of valid configurations sampled: {valid_fraction:.5f}\")\n",
        "print(f\"Expected fraction from uniform random:     {expected_random:.4e}\")\n",
        "\n",
        "# Configure SQD eigensolver\n",
        "energy_tol = 1e-3\n",
        "occupancies_tol = 1e-3\n",
        "max_iterations = 5\n",
        "num_batches = 3\n",
        "samples_per_batch = 300\n",
        "symmetrize_spin = True\n",
        "carryover_threshold = 1e-4\n",
        "max_cycle = 200\n",
        "\n",
        "# Hartree-Fock initial occupancy guess\n",
        "initial_occupancies = (\n",
        "    np.array([1] * n_alpha + [0] * (norb - n_alpha)),\n",
        "    np.array([1] * n_beta + [0] * (norb - n_beta)),\n",
        ")\n",
        "\n",
        "sci_solver = partial(solve_sci_batch, spin_sq=0.0, max_cycle=max_cycle)\n",
        "result_history = []\n",
        "\n",
        "\n",
        "def callback(results: list[SCIResult]):\n",
        "    result_history.append(results)\n",
        "    iteration = len(result_history)\n",
        "    print(f\"Iteration {iteration}\")\n",
        "    for i, res in enumerate(results):\n",
        "        subspace_dim = np.prod(res.sci_state.amplitudes.shape)\n",
        "        print(\n",
        "            f\"  Subsample {i}: Energy = {res.energy + nuclear_repulsion_energy:.8f} Ha | Subspace dim = {subspace_dim}\"\n",
        "        )\n",
        "\n",
        "\n",
        "print(\"\\nRunning SQD post-processing...\")\n",
        "rng = np.random.default_rng(42)\n",
        "sqd_result = diagonalize_fermionic_hamiltonian(\n",
        "    hcore,\n",
        "    eri,\n",
        "    bit_array,\n",
        "    samples_per_batch=samples_per_batch,\n",
        "    norb=norb,\n",
        "    nelec=nelec,\n",
        "    num_batches=num_batches,\n",
        "    energy_tol=energy_tol,\n",
        "    occupancies_tol=occupancies_tol,\n",
        "    max_iterations=max_iterations,\n",
        "    sci_solver=sci_solver,\n",
        "    symmetrize_spin=symmetrize_spin,\n",
        "    initial_occupancies=initial_occupancies,\n",
        "    carryover_threshold=carryover_threshold,\n",
        "    callback=callback,\n",
        "    seed=rng,\n",
        ")\n",
        "\n",
        "final_energy = sqd_result.energy + nuclear_repulsion_energy\n",
        "energy_error = final_energy - reference_energy\n",
        "\n",
        "print(\"\\n=== Energy Summary (N₂/cc-pVDZ active space) ===\")\n",
        "print(f\"SCF energy:       {scf.e_tot:.8f} Ha\")\n",
        "print(f\"Reference energy: {reference_energy:.8f} Ha\")\n",
        "print(f\"Final SQD energy: {final_energy:.8f} Ha\")\n",
        "print(\n",
        "    f\"Energy error:     {energy_error:.8f} Ha ({abs(energy_error) * 627.5:.4f} kcal/mol)\"\n",
        ")\n",
        "\n",
        "# ── Visualization ─────────────────────────────────────────────────────\n",
        "\n",
        "x1 = range(len(result_history))\n",
        "min_e = [\n",
        "    min(res, key=lambda r: r.energy).energy + nuclear_repulsion_energy\n",
        "    for res in result_history\n",
        "]\n",
        "e_diff = [abs(e - reference_energy) for e in min_e]\n",
        "chem_accuracy = 0.001  # ~1 mHa / ~0.6 kcal/mol\n",
        "\n",
        "y2 = np.sum(sqd_result.orbital_occupancies, axis=0)\n",
        "x2 = range(len(y2))\n",
        "\n",
        "fig, axs = plt.subplots(1, 2, figsize=(12, 5))\n",
        "\n",
        "# Energies convergence plot\n",
        "axs[0].plot(x1, e_diff, label=\"Energy error\", marker=\"o\")\n",
        "axs[0].set_xticks(list(x1))\n",
        "axs[0].set_xticklabels(list(x1))\n",
        "axs[0].set_yscale(\"log\")\n",
        "axs[0].axhline(\n",
        "    y=chem_accuracy,\n",
        "    color=\"#BF5700\",\n",
        "    linestyle=\"--\",\n",
        "    label=\"Chemical accuracy (1 mHa)\",\n",
        ")\n",
        "axs[0].set_title(\"SQD Energy Error vs Iteration\")\n",
        "axs[0].set_xlabel(\"Iteration\")\n",
        "axs[0].set_ylabel(\"Energy Error (Ha)\")\n",
        "axs[0].legend()\n",
        "\n",
        "# Spatial orbital occupancy plot\n",
        "axs[1].bar(x2, y2, width=0.8)\n",
        "axs[1].set_xticks(list(x2)[::2])\n",
        "axs[1].set_xticklabels(list(x2)[::2])\n",
        "axs[1].set_title(\"Avg Occupancy per Spatial Orbital\")\n",
        "axs[1].set_xlabel(\"Spatial Orbital Index\")\n",
        "axs[1].set_ylabel(\"Avg Occupancy\")\n",
        "\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "next-steps",
      "metadata": {},
      "source": [
        "## Next steps\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recommendations\">\n",
        "  If you found this work interesting, you might be interested in the following material:\n",
        "\n",
        "  * [Sample-based quantum diagonalization tutorial](/docs/tutorials/sample-based-quantum-diagonalization) — the full SQD chemistry workflow on IBM Quantum Platform, including larger molecules and basis sets\n",
        "  * [Sample-based Krylov quantum diagonalization](/docs/tutorials/sample-based-krylov-quantum-diagonalization) — a related method using time evolution circuits for fermionic lattice models\n",
        "  * [`qiskit-addon-sqd` documentation](/docs/addons/qiskit-addon-sqd) — full API reference and additional tutorials for the SQD post-processing library\n",
        "  * [QRMI GitHub repository](https://github.com/qiskit-community/qrmi) — source code, additional backend examples (CUDA-Q, C, Lua)\n",
        "  * [QRMI overview paper](https://arxiv.org/abs/2506.10052) — technical description of the QRMI architecture and HPC integration\n",
        "  * [IBM Quantum Compute Service sessions guide](/docs/guides/run-jobs-session) — how sessions relate to the QRMI `acquire`/`release` lifecycle for IBM backends\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}