---
title: Run quantum workloads with QRMI
description: Use the Quantum Resource Management Interface to manage IBM Quantum workloads and run a quantum chemistry workflow from an HPC environment.
source: https://quantum.cloud.ibm.com/docs/en/tutorials/run-quantum-workloads-with-qrmi
---

# Run quantum workloads with QRMI

*Usage estimate: under one minute on IBM Quantum® hardware for the SQD section. This estimate excludes queue time and classical processing; runtime can vary.*

## Learning outcomes

1. The role QRMI plays as middleware between HPC schedulers and IBM Quantum hardware
2. How to use the core QRMI lifecycle (`acquire` → `task_start` → `task_status` → `task_result` → `release`) against a real IBM® backend
3. How to use the higher-level Qiskit `SamplerV2` and `QRMIService` wrappers on top of QRMI
4. How HPC schedulers (Slurm) inject quantum resources via environment variables and how applications consume them
5. How to run a complete SQD (Sample-based Quantum Diagonalization) chemistry workflow on N$_2$ by using IBM hardware through QRMI

## Prerequisites

- [Qiskit primitives (Sampler and Estimator)](/docs/guides/primitives)
- [IBM Quantum sessions](/docs/guides/run-jobs-session)
- [IBM Quantum transpilation](/docs/guides/transpile)
- [Sample-based quantum diagonalization (SQD)](/docs/tutorials/sample-based-quantum-diagonalization)
- Basic familiarity with Python virtual environments and quantum chemistry

## Background

### The quantum-HPC integration challenge

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.

### What QRMI is

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:

```
┌─────────────────────────────────────────────────────────────────┐
│                     HPC Application Layer                       │
│          (Slurm job script / Python workflow / CUDA-Q)          │
└───────────────────────────┬─────────────────────────────────────┘
                            │  QRMI API
                            │  acquire() / task_start() / task_result() / release()
┌───────────────────────────▼─────────────────────────────────────┐
│                        QRMI Core (Rust)                         │
│            Python bindings · C bindings · Lua bindings          │
└───────────────────────────┬─────────────────────────────────────┘
                            │
               IBM Quantum Compute Service / IBM Quantum System
```

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).

### Key design choices

**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.

**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.

**Native IBM payload format.** For IBM Quantum backends, QRMI uses OpenQASM 3 JSON payloads (`QiskitPrimitive`) conforming to the Qiskit Runtime schema.

**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.

**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.

### The core API calls

| Call                       | Purpose                                                                                       |
| -------------------------- | --------------------------------------------------------------------------------------------- |
| `qrmi.acquire()`           | Acquire access to the resource (for example, opens a dedicated session); returns a lock token |
| `qrmi.target()`            | Retrieve backend capabilities (qubits, gates, coupling map) as JSON                           |
| `qrmi.task_start(payload)` | Submit a quantum job; returns a job ID                                                        |
| `qrmi.task_status(job_id)` | Poll job status (`Queued`, `Running`, `Completed`, `Failed`)                                  |
| `qrmi.task_result(job_id)` | Retrieve completed job results as a raw JSON string                                           |
| `qrmi.task_stop(job_id)`   | Cancel or clean up a job                                                                      |
| `qrmi.release(lock)`       | Release the resource lock (for example, closes the session)                                   |

### What this tutorial covers

This tutorial is structured in two parts:

**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.

**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`.

## Requirements

Before starting this tutorial, be sure you have the following installed.

### Python environment setup

Pre-built binary wheels are available for Linux on PyPI, so standard `pip install` works directly on Linux/HPC systems.

```bash
python3 -m venv ~/.venvs/qrmi-ibm
source ~/.venvs/qrmi-ibm/bin/activate
python -m pip install "qrmi[ibm]" python-dotenv pyscf ffsim qiskit-addon-sqd matplotlib ipykernel
python -m ipykernel install --user --name qrmi-ibm --display-name "QRMI IBM"
```

> **Platforms without pre-built wheels**
>
> 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)).

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.

### Credentials required

- IBM Quantum: IAM API key and Service CRN from [IBM Quantum Platform]()

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.

```dotenv
ibm_kingston_QRMI_IBM_QCS_ENDPOINT=https://quantum.cloud.ibm.com/api/v1
ibm_kingston_QRMI_IBM_QCS_IAM_ENDPOINT=https://iam.cloud.ibm.com
ibm_kingston_QRMI_IBM_QCS_IAM_APIKEY=<your-iam-api-key>
ibm_kingston_QRMI_IBM_QCS_SERVICE_CRN=<your-crn-starting-with-crn:v1:>
ibm_kingston_QRMI_IBM_QCS_SESSION_MODE=dedicated
ibm_kingston_QRMI_IBM_QCS_SESSION_MAX_TTL=28800
QRMI_JOB_QPU_RESOURCES=ibm_kingston
QRMI_JOB_QPU_TYPES=ibm-quantum-compute-service
```

For a Slurm allocation, use the resource settings and credentials supplied by the cluster. The notebook preserves existing environment values.

## Setup

Import the dependencies and load the resource configuration.

```python
import os
import time
import json
import numpy as np
from dotenv import load_dotenv

from qrmi import (
    QuantumResource,
    ResourceType,
    Payload,
    TaskStatus,
    get_job_qpu_resources_and_types,
)
from qrmi.primitives import QRMIService
from qrmi.primitives.ibm import SamplerV2, get_target

from qiskit import QuantumCircuit, qasm3
from qiskit.circuit.library import efficient_su2
from qiskit.primitives.containers.sampler_pub import SamplerPub
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# Load credentials from .env without overriding already-set scheduler environment variables
load_dotenv(override=False)

# Preserve resources if injected by Slurm SPANK plugin; fallback to default for interactive run
BACKEND_NAME = os.environ.get("QRMI_JOB_QPU_RESOURCES", "ibm_kingston")
os.environ.setdefault("QRMI_JOB_QPU_RESOURCES", BACKEND_NAME)
os.environ.setdefault("QRMI_JOB_QPU_TYPES", "ibm-quantum-compute-service")

print(f"Backend: {BACKEND_NAME}")
print("Environment ready.")
```

Output:

```
Backend: ibm_kingston
Environment ready.
```

## Small-scale examples

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.

The payload for these initial steps is a small Bell state circuit chosen to be fast and inexpensive to run.

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.

### Step 1: Map the classical problem to a quantum resource

The first step in any QRMI workflow is to create a `QuantumResource` object and verify that it is accessible.

`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.

### Step 2: Optimize the problem for quantum hardware execution

Before submission, use Qiskit to transpile the circuit to the backend's instruction set architecture (ISA), using the `Target` object retrieved in Step 1.

The example then builds a `Payload.QiskitPrimitive`, which wraps the OpenQASM 3 circuit string and job metadata into the IBM primitive schema.

### Step 3: Execute using QRMI primitives

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.

The following cell keeps acquisition, execution, and cleanup together so that failures after acquisition still release a notebook-owned session.

```python
# ── IBM Quantum ───────────────────────────────────────────────────────
qrmi = QuantumResource(BACKEND_NAME, ResourceType.IBMQuantumComputeService)
# ResourceType.IBMQuantumSystem is the alternative for directly provisioned systems

print(f"Resource id:   {qrmi.resource_id()}")
print(f"Resource type: {qrmi.resource_type()}")
print(f"Accessible:    {qrmi.is_accessible()}")

# Acquire exclusive access — open try/finally immediately so every
# subsequent failure (target retrieval, transpilation, submission) is covered.
# Release is skipped when running under Slurm: the SPANK plugin owns the
# session lifecycle and will release it when the job finishes.
lock = qrmi.acquire()
print(f"Lock token:    {lock}")
try:
    # Retrieve backend capabilities
    transpiler_target = get_target(
        qrmi
    )  # calls qrmi.target() and parses the JSON
    target_json = json.loads(qrmi.target().value)
    config = target_json.get("configuration", {})
    print(f"\nBackend: {config.get('backend_name', 'unknown')}")
    print(f"Qubits:  {config.get('n_qubits', 'unknown')}")
    print(f"Gates:   {config.get('basis_gates', [])}")

    # ── IBM Quantum ───────────────────────────────────────────────────

    # Build a Bell state circuit
    qc = QuantumCircuit(2)
    qc.h(0)
    qc.cx(0, 1)
    qc.measure_all()
    print(qc.draw("text"))

    # Transpile to ISA using the target retrieved in Step 1
    pm = generate_preset_pass_manager(
        optimization_level=1, target=transpiler_target
    )
    isa_circuit = pm.run(qc)
    print(f"\nTranspiled gate counts: {isa_circuit.count_ops()}")

    # Build the QRMI payload
    # Payload.QiskitPrimitive wraps the IBM SamplerV2 input schema:
    #   pubs: list of [qasm3_string, parameter_values]  (shots goes at top level)
    #   program_id: "sampler" or "estimator"
    shots = 1024
    pub = SamplerPub.coerce((isa_circuit,), shots)
    qasm3_str = qasm3.dumps(
        pub.circuit,
        disable_constants=True,
        allow_aliasing=True,
        experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1,
    )
    # Parameter values as a flat list (empty for non-parametric circuits)
    param_array = pub.parameter_values.as_array(
        pub.circuit.parameters
    ).tolist()

    input_json = {
        "pubs": [
            [qasm3_str, param_array]
        ],  # list-of-lists; shots at top level
        "version": 2,
        "support_qiskit": False,  # True returns binary-encoded Qiskit result
        "shots": shots,
    }
    payload = Payload.QiskitPrimitive(
        input=json.dumps(input_json), program_id="sampler"
    )
    print("Payload ready")

    # ── IBM Quantum ───────────────────────────────────────────────────

    # Submit the job
    job_id = qrmi.task_start(payload)
    print(f"Job submitted: {job_id}")

    # Poll until complete
    while True:
        status = qrmi.task_status(job_id)
        print(f"  Status: {status}")
        if status not in [TaskStatus.Running, TaskStatus.Queued]:
            break
        time.sleep(5)

    print(f"\nFinal status: {status}")

    # Retrieve results
    # support_qiskit=False → plain JSON; parse directly without ResultDecoder
    if status == TaskStatus.Completed:
        raw = qrmi.task_result(job_id).value
        result = json.loads(raw)
        # IBM QCS plain-JSON result shape: {"results": [{"data": {"meas": {"samples": [...]}}}]}
        # samples is a list of hex-encoded integers; decode to zero-padded bitstrings
        samples = result["results"][0]["data"]["meas"]["samples"]
        num_bits = sum(reg.size for reg in isa_circuit.cregs)
        from collections import Counter

        counts = Counter(format(int(s, 16), f"0{num_bits}b") for s in samples)
        print(f"\nMeasurement counts: {dict(counts.most_common(8))}")
        qrmi.task_stop(job_id)
    else:
        print(f"Job did not complete. Logs:\n{qrmi.task_logs(job_id)}")

finally:
    # Release only in interactive sessions; under Slurm the SPANK plugin
    # manages the session lifecycle and calling release() here would
    # prematurely close a session it does not own.
    if not os.environ.get("SLURM_JOB_ID"):
        qrmi.release(lock)
        print("\nSession released.")
```

Output:

```
Resource id:   ibm_kingston
Resource type: ResourceType.IBMQuantumComputeService
Accessible:    True
Lock token:    2ff43011-aed1-4436-a4df-40f37ec588b7

Backend: ibm_kingston
Qubits:  156
Gates:   ['cz', 'id', 'rx', 'rz', 'rzz', 'sx', 'x', 'xslow']
        ┌───┐      ░ ┌─┐   
   q_0: ┤ H ├──■───░─┤M├───
        └───┘┌─┴─┐ ░ └╥┘┌─┐
   q_1: ─────┤ X ├─░──╫─┤M├
             └───┘ ░  ║ └╥┘
meas: 2/══════════════╩══╩═
                      0  1 

Transpiled gate counts: OrderedDict([('rz', 6), ('sx', 3), ('measure', 2), ('cz', 1), ('barrier', 1)])
Payload ready
Job submitted: dai43g8mhr3c73e7a7o0
  Status: TaskStatus.Queued
  Status: TaskStatus.Running
  Status: TaskStatus.Completed

Final status: TaskStatus.Completed

Measurement counts: {'11': 487, '00': 254, '01': 177, '10': 106}

Session released.
```

### Higher-level Qiskit interface: QRMIService and SamplerV2

The raw lifecycle above provides explicit control over every call. For standard Qiskit workflows, QRMI provides a `SamplerV2` primitive implementing `BaseSamplerV2`.

> **Lifecycle management**
>
> `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.

```python
# QRMIService reads QRMI_JOB_QPU_RESOURCES / QRMI_JOB_QPU_TYPES set in Setup or Slurm
service = QRMIService()
qrmi_svc = service.resources()[0]
print(f"Using: {qrmi_svc.resource_id()} ({qrmi_svc.resource_type()})")

# Build an EfficientSU2 circuit
circuit = efficient_su2(5, entanglement="linear")
circuit.measure_all()
param_values = np.random.rand(circuit.num_parameters)

pm = generate_preset_pass_manager(
    optimization_level=1, target=get_target(qrmi_svc)
)
isa_circuit = pm.run(circuit)

# SamplerV2 executes jobs against the QRMI resource and decodes results into primitive containers
sampler = SamplerV2(qrmi_svc, options={"default_shots": 1024})
job = sampler.run([(isa_circuit, param_values)])
print(f"Job ID: {job.job_id()} | Status: {job.status()}")

# Poll with retry — re-raise immediately on permanent failures;
# only retry on transient network/timeout errors (connection resets, 503s).
_TRANSIENT = (
    "503",
    "Service Unavailable",
    "ConnectionError",
    "TimeoutError",
    "timed out",
    "Connection reset",
)
result = None
for attempt in range(60):
    try:
        result = job.result()  # blocks until complete
        break
    except Exception as e:
        if not any(tok in str(e) for tok in _TRANSIENT):
            raise
        print(f"  Transient error on attempt {attempt + 1}: {e}")
        time.sleep(10)

if result is not None:
    counts = result[0].data.meas.get_counts()
    print(f"Counts (first 5): {dict(list(counts.items())[:5])}")
else:
    print("Job did not complete after retries.")

if job.errored():
    print(f"Logs:\n{job.logs()}")
```

Output:

```
Using: ibm_kingston (ResourceType.IBMQuantumComputeService)
Job ID: dai43jj9k43c73afhrhg | Status: JobStatus.QUEUED
Counts (first 5): {'00010': 66, '00100': 28, '11000': 71, '00110': 23, '10100': 14}
```

### HPC context: Slurm resource injection

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:

```bash
#SBATCH --gres=qpu:1
#SBATCH --qpu=ibm_kingston
python my_workflow.py   # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already set
```

Application code discovers its allocated resources at runtime — no hardcoded backend names:

```python
# get_job_qpu_resources_and_types() reads QRMI_JOB_QPU_RESOURCES / QRMI_JOB_QPU_TYPES
# set by the Slurm SPANK plugin (or manually above in Setup)
qpus, qpu_types = get_job_qpu_resources_and_types()
print("Resources allocated by scheduler:")
for qpu, qpu_type in zip(qpus, qpu_types):
    print(f"  {qpu}  ({qpu_type})")

# QRMIService wraps this into a list of ready QuantumResource objects
for r in QRMIService().resources():
    print(
        f"\nQRMIService found: {r.resource_id()}  accessible={r.is_accessible()}"
    )
```

Output:

```
Resources allocated by scheduler:
  ibm_kingston  (ibm-quantum-compute-service)

QRMIService found: ibm_kingston  accessible=True
```

## Large-scale hardware example: SQD on N$_2$

Here we put all components together into a complete quantum chemistry workflow at a larger scale, executed on real IBM Quantum hardware through QRMI.

**SQD** combines the following:

1. Quantum sampling of a Local Unitary Cluster Jastrow (LUCJ) ansatz constructed using `ffsim` and initialized from CCSD amplitudes
2. Hardware-aware transpilation matching the heavy-hex lattice topology via `generate_lucj_pass_manager`
3. Sampling execution on IBM Quantum hardware managed through `QRMIService` and QRMI `SamplerV2`
4. Classical post-processing: self-consistent configuration recovery and iterative subspace diagonalization using `qiskit-addon-sqd`

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).

**Reference energy for N$_2$ /cc-pVDZ active space (bond distance 1.0 $\AA$):**

- Reference energy (separate SCI calculation): **−109.22802922 Ha**

> **Accuracy of the saved run**
>
> 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.

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.

```python
from qrmi.primitives.ibm import get_backend
import math
import os
import time
from functools import partial
from dotenv import load_dotenv
import numpy as np
import matplotlib.pyplot as plt

import pyscf
import pyscf.gto
import pyscf.scf
import pyscf.cc
import pyscf.mcscf
import pyscf.ao2mo

import ffsim
import ffsim.qiskit
from qiskit import QuantumCircuit, QuantumRegister
from qiskit_addon_sqd.fermion import (
    SCIResult,
    diagonalize_fermionic_hamiltonian,
    solve_sci_batch,
)
from qrmi.primitives import QRMIService
from qrmi.primitives.ibm import SamplerV2, get_target

load_dotenv(override=False)
os.environ.setdefault("QRMI_JOB_QPU_RESOURCES", "ibm_kingston")
os.environ.setdefault("QRMI_JOB_QPU_TYPES", "ibm-quantum-compute-service")

# ── Step 1: Map classical inputs to a quantum problem ─────────────────

# Build N2 molecule at 1.0 Å bond distance
mol = pyscf.gto.Mole()
mol.build(
    atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]],
    basis="cc-pvdz",
    symmetry="Dooh",
)

# Define active space: freeze 2 core orbitals
n_frozen = 2
active_space = range(n_frozen, mol.nao_nr())

# Get molecular integrals
scf = pyscf.scf.RHF(mol).run()
norb = len(active_space)
n_electrons = int(sum(scf.mo_occ[active_space]))
n_alpha = (n_electrons + mol.spin) // 2
n_beta = (n_electrons - mol.spin) // 2
nelec = (n_alpha, n_beta)

cas = pyscf.mcscf.CASCI(scf, norb, nelec)
mo = cas.sort_mo(active_space, base=0)
hcore, nuclear_repulsion_energy = cas.get_h1cas(mo)
eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), norb)

# Reference energy from external SCI calculation
reference_energy = -109.22802921665716

print(
    f"N₂/cc-pVDZ active space: {norb} orbitals ({2 * norb} qubits), {nelec} electrons"
)
print(f"SCF energy:       {scf.e_tot:.8f} Ha")
print(f"Reference energy: {reference_energy:.8f} Ha")

# Get CCSD amplitudes for initializing the LUCJ ansatz
ccsd = pyscf.cc.CCSD(
    scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]
).run()
t1 = ccsd.t1
t2 = ccsd.t2
print(f"CCSD energy:      {ccsd.e_tot:.8f} Ha")

# Discover backend via QRMIService (QRMI_JOB_QPU_RESOURCES set in Setup)
service = QRMIService()
qrmi_sqd = service.resources()[0]
print(f"Using QRMI resource: {qrmi_sqd.resource_id()}")

# get_backend() wraps the QRMI resource as a Qiskit backend for layout synthesis

backend = get_backend(qrmi_sqd)

# Set ansatz properties
n_reps = 1
pairs_aa = [(p, p + 1) for p in range(norb - 1)]
pairs_ab = None

# Create pass manager adapted to hardware heavy-hex topology
pass_manager, pairs_ab = ffsim.qiskit.generate_lucj_pass_manager(
    backend=backend,
    norb=norb,
    connectivity="heavy-hex",
    interaction_pairs=(pairs_aa, pairs_ab),
    optimization_level=3,
)

# Create the compressed LUCJ ansatz operator
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
    t2=t2,
    t1=t1,
    n_reps=n_reps,
    interaction_pairs=(pairs_aa, pairs_ab),
    optimize=True,
    options=dict(maxiter=1000),
)

# Assemble the circuit
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()
print(f"LUCJ circuit: {circuit.num_qubits} qubits, depth {circuit.depth()}")

# ── Step 2: Optimize for quantum hardware execution ───────────────────

isa_circuit = pass_manager.run(circuit)
print(f"Transpiled gate counts: {isa_circuit.count_ops()}")

# ── Step 3: Execute using Qiskit primitives (QRMI SamplerV2) ─────────

sampler = SamplerV2(qrmi_sqd, options={"default_shots": 100_000})
# sampler.options.environment.job_tags = ["TUT_SQD"]
job = sampler.run([(isa_circuit,)])
print(f"Job submitted via QRMI: {job.job_id()} | Status: {job.status()}")
print("Waiting for results from hardware...")

_TRANSIENT = (
    "503",
    "Service Unavailable",
    "ConnectionError",
    "TimeoutError",
    "timed out",
    "Connection reset",
)
primitive_result = None
for attempt in range(120):
    try:
        primitive_result = job.result()
        break
    except Exception as e:
        if not any(tok in str(e) for tok in _TRANSIENT):
            raise
        print(f"  Transient error on attempt {attempt + 1}: {e}")
        time.sleep(10)

if primitive_result is None:
    raise RuntimeError("Job did not complete after retries")

pub_result = primitive_result[0]
bit_array = pub_result.data.meas
print(f"Total shots collected: {bit_array.num_shots}")

# ── Step 4: Post-process and return result in classical format ────────


def is_valid_bitstring(
    bitstring: str, norb: int, nelec: tuple[int, int]
) -> bool:
    n_a, n_b = nelec
    return (
        len(bitstring) == 2 * norb
        and bitstring[norb:].count("1") == n_a
        and bitstring[:norb].count("1") == n_b
    )


num_valid = sum(
    is_valid_bitstring(b, norb, nelec) for b in bit_array.get_bitstrings()
)
valid_fraction = num_valid / bit_array.num_shots
expected_random = (
    math.comb(norb, n_alpha) * math.comb(norb, n_beta) / (2 ** (2 * norb))
)
print(f"Fraction of valid configurations sampled: {valid_fraction:.5f}")
print(f"Expected fraction from uniform random:     {expected_random:.4e}")

# Configure SQD eigensolver
energy_tol = 1e-3
occupancies_tol = 1e-3
max_iterations = 5
num_batches = 3
samples_per_batch = 300
symmetrize_spin = True
carryover_threshold = 1e-4
max_cycle = 200

# Hartree-Fock initial occupancy guess
initial_occupancies = (
    np.array([1] * n_alpha + [0] * (norb - n_alpha)),
    np.array([1] * n_beta + [0] * (norb - n_beta)),
)

sci_solver = partial(solve_sci_batch, spin_sq=0.0, max_cycle=max_cycle)
result_history = []


def callback(results: list[SCIResult]):
    result_history.append(results)
    iteration = len(result_history)
    print(f"Iteration {iteration}")
    for i, res in enumerate(results):
        subspace_dim = np.prod(res.sci_state.amplitudes.shape)
        print(
            f"  Subsample {i}: Energy = {res.energy + nuclear_repulsion_energy:.8f} Ha | Subspace dim = {subspace_dim}"
        )


print("\nRunning SQD post-processing...")
rng = np.random.default_rng(42)
sqd_result = diagonalize_fermionic_hamiltonian(
    hcore,
    eri,
    bit_array,
    samples_per_batch=samples_per_batch,
    norb=norb,
    nelec=nelec,
    num_batches=num_batches,
    energy_tol=energy_tol,
    occupancies_tol=occupancies_tol,
    max_iterations=max_iterations,
    sci_solver=sci_solver,
    symmetrize_spin=symmetrize_spin,
    initial_occupancies=initial_occupancies,
    carryover_threshold=carryover_threshold,
    callback=callback,
    seed=rng,
)

final_energy = sqd_result.energy + nuclear_repulsion_energy
energy_error = final_energy - reference_energy

print("\n=== Energy Summary (N₂/cc-pVDZ active space) ===")
print(f"SCF energy:       {scf.e_tot:.8f} Ha")
print(f"Reference energy: {reference_energy:.8f} Ha")
print(f"Final SQD energy: {final_energy:.8f} Ha")
print(
    f"Energy error:     {energy_error:.8f} Ha ({abs(energy_error) * 627.5:.4f} kcal/mol)"
)

# ── Visualization ─────────────────────────────────────────────────────

x1 = range(len(result_history))
min_e = [
    min(res, key=lambda r: r.energy).energy + nuclear_repulsion_energy
    for res in result_history
]
e_diff = [abs(e - reference_energy) for e in min_e]
chem_accuracy = 0.001  # ~1 mHa / ~0.6 kcal/mol

y2 = np.sum(sqd_result.orbital_occupancies, axis=0)
x2 = range(len(y2))

fig, axs = plt.subplots(1, 2, figsize=(12, 5))

# Energies convergence plot
axs[0].plot(x1, e_diff, label="Energy error", marker="o")
axs[0].set_xticks(list(x1))
axs[0].set_xticklabels(list(x1))
axs[0].set_yscale("log")
axs[0].axhline(
    y=chem_accuracy,
    color="#BF5700",
    linestyle="--",
    label="Chemical accuracy (1 mHa)",
)
axs[0].set_title("SQD Energy Error vs Iteration")
axs[0].set_xlabel("Iteration")
axs[0].set_ylabel("Energy Error (Ha)")
axs[0].legend()

# Spatial orbital occupancy plot
axs[1].bar(x2, y2, width=0.8)
axs[1].set_xticks(list(x2)[::2])
axs[1].set_xticklabels(list(x2)[::2])
axs[1].set_title("Avg Occupancy per Spatial Orbital")
axs[1].set_xlabel("Spatial Orbital Index")
axs[1].set_ylabel("Avg Occupancy")

plt.tight_layout()
plt.show()
```

Output:

```

WARN: Unable to to identify input symmetry using original axes.
Different symmetry axes will be used.

converged SCF energy = -108.929838385609
N₂/cc-pVDZ active space: 26 orbitals (52 qubits), (5, 5) electrons
SCF energy:       -108.92983839 Ha
Reference energy: -109.22802922 Ha
E(CCSD) = -109.2177884185545  E_corr = -0.2879500329450047
CCSD energy:      -109.21778842 Ha
Using QRMI resource: ibm_kingston
```

```
LUCJ circuit: 52 qubits, depth 3
Transpiled gate counts: OrderedDict([('sx', 7041), ('rz', 6969), ('cz', 1858), ('measure', 52), ('x', 47), ('barrier', 1)])
Job submitted via QRMI: dai43o0mhr3c73e7a81g | Status: JobStatus.QUEUED
Waiting for results from hardware...
Total shots collected: 100000
Fraction of valid configurations sampled: 0.00319
Expected fraction from uniform random:     9.6079e-07

Running SQD post-processing...
Iteration 1
  Subsample 0: Energy = -109.09341960 Ha | Subspace dim = 208849
  Subsample 1: Energy = -109.11738590 Ha | Subspace dim = 204304
  Subsample 2: Energy = -109.09947704 Ha | Subspace dim = 212521
Iteration 2
  Subsample 0: Energy = -109.16015998 Ha | Subspace dim = 332929
  Subsample 1: Energy = -109.16823702 Ha | Subspace dim = 319225
  Subsample 2: Energy = -109.16189785 Ha | Subspace dim = 336400
Iteration 3
  Subsample 0: Energy = -109.17759299 Ha | Subspace dim = 471969
  Subsample 1: Energy = -109.17937442 Ha | Subspace dim = 512656
  Subsample 2: Energy = -109.17970409 Ha | Subspace dim = 504100
Iteration 4
  Subsample 0: Energy = -109.18410905 Ha | Subspace dim = 608400
  Subsample 1: Energy = -109.18265405 Ha | Subspace dim = 636804
  Subsample 2: Energy = -109.18608430 Ha | Subspace dim = 657721
Iteration 5
  Subsample 0: Energy = -109.18870837 Ha | Subspace dim = 846400
  Subsample 1: Energy = -109.18890818 Ha | Subspace dim = 848241
  Subsample 2: Energy = -109.19022232 Ha | Subspace dim = 804609

=== Energy Summary (N₂/cc-pVDZ active space) ===
SCF energy:       -108.92983839 Ha
Reference energy: -109.22802922 Ha
Final SQD energy: -109.19022232 Ha
Energy error:     0.03780690 Ha (23.7238 kcal/mol)
```

![Output of the previous code cell](https://quantum.cloud.ibm.com/docs/images/tutorials/run-quantum-workloads-with-qrmi/extracted-outputs/large-scale-all-3.avif)

## Next steps

> **Recommendations**
>
> If you found this work interesting, you might be interested in the following material:
>
> - [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
> - [Sample-based Krylov quantum diagonalization](/docs/tutorials/sample-based-krylov-quantum-diagonalization) — a related method using time evolution circuits for fermionic lattice models
> - [`qiskit-addon-sqd` documentation](/docs/addons/qiskit-addon-sqd) — full API reference and additional tutorials for the SQD post-processing library
> - [QRMI GitHub repository](https://github.com/qiskit-community/qrmi) — source code, additional backend examples (CUDA-Q, C, Lua)
> - [QRMI overview paper](https://arxiv.org/abs/2506.10052) — technical description of the QRMI architecture and HPC integration
> - [IBM Quantum Compute Service sessions guide](/docs/guides/run-jobs-session) — how sessions relate to the QRMI `acquire`/`release` lifecycle for IBM backends
