Exécutez des charges de travail quantiques avec QRMI
Estimation du temps d'exécution : moins d'une minute sur un matériel d' IBM Quantum®, pour la section SQD. Cette estimation ne tient pas compte du temps d'attente ni du traitement classique; la durée d'exécution peut varier.
Acquis d'apprentissage
- Le rôle joué par QRMI en tant que couche intermédiaire entre les planificateurs HPC et le matériel d' IBM Quantum
- Comment utiliser le cycle de vie de base du QRMI (
acquire→task_start→task_status→task_result→release) avec un backend « real- IBM® » - Comment utiliser les couches de haut niveau de Qiskit et
SamplerV2les wrappersQRMIServices'appuyant sur QRMI - Comment les planificateurs HPC (Slurm) intègrent les ressources quantiques via des variables d'environnement et comment les applications les exploitent
- Comment exécuter un workflow chimique complet de SQD (diagonalisation quantique par échantillonnage) sur N « » à l’aide du matériel « IBM » via QRMI
Prérequis
- Qiskit primitives (Échantillonneur et estimateur)
- IBM Quantum sessions
- IBM Quantum transpilation
- Diagonalisation quantique par échantillonnage (SQD)
- Connaissances de base des environnements virtuels d' Python et de la chimie quantique
Arrière-plan
Le défi de l'intégration entre l'informatique quantique et le calcul haute performance (HPC)
Les flux de travail en calcul haute performance (HPC) nécessitent souvent une coordination fluide entre les clusters de calcul classiques et les unités de traitement quantique (QPU). Les différents backends et services de matériel quantique proposent des mécanismes d'authentification, des formats de transmission et des API de gestion du cycle de vie des tâches distincts. L'intégration des systèmes d' IBM Quantum s dans les gestionnaires de charges de travail HPC (tels que Slurm) nécessite une interface simple et standardisée pour l'acquisition des ressources, l'exécution des tâches et la gestion des sessions.
Qu'est-ce que le QRMI?
L'interface QRMI (Quantum Resource Management Interface) est une bibliothèque middleware écrite en Rust qui uniformise l'accès au matériel quantique depuis les planificateurs HPC et les applications classiques. Elle expose une API unique et unifiée pour la gestion du cycle de vie :
┌─────────────────────────────────────────────────────────────────┐
│ 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 est publié sous forme de projet open source à l'adresse github.com/qiskit-community/qrmi et est décrit dans l'article de synthèse arXiv:2506.10052.
Choix clés en matière de conception
Le cycle de vie des ressources, et non la compilation des circuits. QRMI gère le cycle de vie « acquisition/soumission/interrogation/libération » et rien d'autre. La compilation, l'optimisation et la transpilation des circuits restent du ressort de la couche applicative (par exemple, Qiskit). Cela permet de conserver une interface minimaliste et modulable.
Modèle de portabilité des fournisseurs. Bien que QRMI fournisse des appels communs de gestion des tâches (acquire, task_start, task_status, task_result, release) sur l'ensemble des backends matériels pris en charge, le changement de fournisseur nécessite également différents cycles de compilation, la construction d'une charge utile spécifique au fournisseur et le décodage des résultats au niveau de la couche applicative.
Format natif de la charge utile « IBM ». Pour les backends « IBM Quantum », QRMI utilise des charges utiles JSON de type « OpenQASM 3 » (QiskitPrimitive) conformes au schéma Qiskit Runtime.
Configuration via des variables d'environnement. Les identifiants et les URL des points de terminaison sont lus à partir des variables d'environnement lors de l'exécution. Dans un cluster HPC, le plugin Slurm QRMI SPANK les configure automatiquement lors de l'envoi d'un travail. Dans un notebook ou lors d'une session interactive, vous les chargez à partir d'un fichier .env . Le code de l'application ne contient jamais d'identifiants ni d'URL de point de terminaison codés en dur.
Intégration d'un planificateur HPC via GRES. Lorsqu'une tâche Slurm sollicite des ressources Quantum via l'interface du plugin QRMI SPANK (#SBATCH --gres=qpu:1 et #SBATCH --qpu=ibm_kingston), le plugin injecte QRMI_JOB_QPU_RESOURCES et QRMI_JOB_QPU_TYPES dans l'environnement de la tâche. Les applications appellent cette interface pour get_job_qpu_resources_and_types() connaître les ressources qui leur ont été attribuées — aucun nom de backend codé en dur n'est nécessaire. QRMIService fournit une interface pour ce modèle à l'intention des utilisateurs de Qiskit.
Les appels d'API principaux
Appel | Fonction |
|---|---|
qrmi.acquire() | Obtient l'accès à la ressource (par exemple, ouvre une session dédiée); renvoie un jeton de verrouillage |
qrmi.target() | Récupérer les capacités du backend (qubits, portes, carte de couplage) au format JSON |
qrmi.task_start(payload) | Soumettre une tâche quantique; renvoie un identifiant de tâche |
qrmi.task_status(job_id) | État de la tâche de sondage (Queued, Running, Completed, Failed) |
qrmi.task_result(job_id) | Récupérer les résultats des tâches terminées sous forme de chaîne JSON brute |
qrmi.task_stop(job_id) | Annuler ou nettoyer une tâche |
qrmi.release(lock) | Libérer le verrou de la ressource (par exemple, fermer la session) |
Contenu de ce tutoriel
Ce tutoriel est divisé en deux parties :
Étapes 1 à 3 (exemples à petite échelle) : Présenter l'API QRMI à l'aide d'une démonstration simple utilisant un circuit à états de Bell sur le matériel d' IBM Quantum, en abordant à la fois l'utilisation directe des primitives de bas niveau ainsi que QRMIService l'intégration SamplerV2 de haut niveau.
Exemple de calcul matériel à grande échelle : un workflow SQD complet pour la molécule N e à une distance de liaison de 1.0 (espace actif à base cc-pVDZ, 26 orbitales spatiales / 52 qubits), exécuté sur le matériel IBM Quantum via QRMI. La méthode SQD combine l'échantillonnage quantique d'un ansatz LUCJ construit à l'aide de ffsim et la récupération de configuration auto-cohérente à l'aide de qiskit-addon-sqd.
Exigences
Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont bien installés.
Python configuration de l'environnement
Des paquets binaires précompilés sont disponibles pour Linux sur PyPI,; ainsi, la version standard pip install fonctionne directement sur les systèmes Linux /HPC.
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"Si vous compilez pip QRMI à partir du code source, assurez-vous de disposer d'une chaîne d'outils Rust récente (Rust ≥ 1.91.1, à installer via rustup.rsrustup ).
Sélectionnez le noyau QRMI IBM dans Jupyter, puis redémarrez-le et exécutez les cellules du notebook dans l'ordre. Les résultats enregistrés proviennent d'une exécution sur le matériel du contributeur; les commandes d'installation ne précisent pas les versions exactes utilisées pour cette exécution.
Justificatifs requis
- IBM Quantum : clé API IAM et service CRN, disponible sur IBM Quantum Platform
Pour une exécution autonome, créez un fichier .env à côté de ce notebook contenant les valeurs suivantes, en remplaçant les espaces réservés aux identifiants. Ce fichier doit rester confidentiel. Si vous choisissez un autre backend, modifiez à la fois son nom et les préfixes des variables d'environnement.
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
Pour une allocation Slurm, utilisez les paramètres de ressources et les identifiants fournis par le cluster. Le bloc-notes conserve les valeurs d'environnement existantes.
Configuration
Importez les dépendances et chargez la configuration des ressources.
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.
Exemples à petite échelle
Les étapes 1 à 3 présentent l'API QRMI à l'aide de circuits simples. Chaque étape correspond à une phase clé du cycle de vie du QRMI sur le matériel « IBM Quantum ».
La charge utile pour ces premières étapes est un petit circuit à état de Bell, choisi pour sa rapidité et son faible coût d'exécution.
Ces exemples font appel au matériel, car ils illustrent l'allocation de ressources à distance et la gestion des tâches. Un simulateur de circuit local ne permet pas de valider l'intégration du service QRMI et du planificateur. L'exécution de ce notebook lance des tâches IBM Quantum et nécessite un accès au backend configuré.
Étape 1 : Associer le problème classique à une ressource quantique
La première étape de tout workflow QRMI consiste à créer un objet QuantumResource et à vérifier qu'il est accessible.
get_target() récupère la description matérielle du backend (nombre de qubits, portes de base, carte de couplage) et la convertit en un objet Target Qiskit, que le transpilateur utilise à l'étape 2.
Étape 2 : Optimiser le problème en vue de son exécution sur un matériel quantique
Avant de valider, utilisez Qiskit pour transcompiler le circuit vers l'architecture du jeu d'instructions (ISA) du backend, à l'aide de l'objet Target récupéré à l'étape 1.
L'exemple crée ensuite un objet Payload.QiskitPrimitivequi encapsule la chaîne de circuits « OpenQASM 3 » ainsi que les métadonnées de la tâche dans le schéma primitif « IBM ».
Étape 3 : Exécution à l'aide des primitives QRMI
Une fois la charge utile créée, l'exemple envoie la tâche et vérifie si elle est terminée. task_start() renvoie immédiatement un identifiant de tâche; task_status() est interrogé jusqu'à ce que le statut ne soit plus Queued/Running. Les résultats sont récupérés sous la forme d'une chaîne JSON brute, puis analysés pour en extraire les échantillons de mesure.
La cellule suivante regroupe les étapes d'acquisition, d'exécution et de nettoyage, de sorte que les échecs survenant après l'acquisition libèrent tout de même une session appartenant au notebook.
# ── 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.
Interface Qiskit de haut niveau : QRMIService et SamplerV2
Le cycle de vie « brut » décrit ci-dessus permet un contrôle explicite sur chaque appel. Pour les workflows Qiskit standard, QRMI fournit une primitive SamplerV2 permettant de mettre en œuvre BaseSamplerV2.
SamplerV2 gère la sérialisation de la charge utile, l'envoi (task_start), l'interrogation et le décodage des résultats. Dans un environnement HPC en mode batch (par exemple, avec Slurm), l'allocation et la libération des ressources sont gérées par le planificateur et le plugin SPANK. Dans une session interactive d’ Python, lorsque l’on utilise des objets API de bas niveau, acquire() et release() peuvent être utilisés pour gérer explicitement des sessions dédiées.
# 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}
Contexte HPC : injection de ressources Slurm
Dans un cluster HPC, les utilisateurs demandent des ressources quantiques en utilisant la syntaxe Slurm GRES ainsi que les options du plugin QRMI SPANK. Le plugin gère automatiquement l'injection des identifiants et des ressources :
#SBATCH --gres=qpu:1
#SBATCH --qpu=ibm_kingston
python my_workflow.py # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already setLe code de l'application détecte les ressources qui lui sont allouées au moment de l'exécution — aucun nom de backend n'est codé en dur :
# 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
Exemple de matériel à grande échelle : SQD sur N
Nous avons ici regroupé tous ces composants pour former un workflow complet de chimie quantique à plus grande échelle, exécuté sur du matériel d’ IBM Quantum s réels via QRMI.
SQD réunit les éléments suivants :
- Échantillonnage quantique d'un modèle LUCJ (Local Unitary Cluster Jastrow) construit à l'aide de
ffsimet initialisé à partir d'amplitudes CCSD - Transpilation tenant compte du matériel et respectant la topologie en treillis « heavy-hex » via
generate_lucj_pass_manager - Exécution de l'échantillonnage sur le matériel « IBM Quantum » géré via et
QRMIServiceQRMISamplerV2 - Post-traitement classique : restauration auto-cohérente de la configuration et diagonalisation itérative des sous-espaces à l'aide de
qiskit-addon-sqd
Nous appliquons la méthode SQD à N , à une distance de liaison de 1.0 , avec un espace actif dérivé de l'ensemble de bases cc-pVDZ (26 orbitales spatiales, correspondant à 52 orbitales spin-orbitales/qubits).
Énergie de référence pour N /cc-pVDZ espace actif (distance de liaison 1.0 ) :
- Énergie de référence (calcul SCI distinct) : − 109.22802922 Ha
Le programme SQD ci-dessous illustre une exécution QRMI de bout en bout réussie sur le matériel d' IBM Quantum. Avec une seule itération LUCJ et 100 000 itérations, le résultat se situe à environ 23.7 kcal/mol au-dessus de l'énergie de référence et n'atteint pas la précision chimique (≤ 1 kcal/mol). Modifier le n_reps nombre de prises de vue ou le nombre d'itérations SQD pourrait améliorer la précision, mais cela nécessite des tests supplémentaires.
Dans la simulation enregistrée, le gestionnaire de passes ffsim a supprimé les interactions de spin opposé et (24, 24) (20, 20) car le moteur de calcul ne pouvait pas les prendre en charge. Les résultats présentés ici utilisent ce circuit ajusté.
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)
Etapes suivantes
Si ce travail vous a paru intéressant, les documents suivants pourraient vous intéresser :
- Tutoriel sur la diagonalisation quantique basée sur des échantillons — le flux de travail complet de chimie SQD sur IBM Quantum Platform, incluant des molécules plus complexes et des ensembles de bases
- Diagonalisation quantique de Krylov par échantillonnage — une méthode apparentée utilisant des circuits d'évolution temporelle pour les modèles de réseaux fermioniques
qiskit-addon-sqddocumentation — référence complète de l'API et tutoriels supplémentaires pour la bibliothèque de post-traitement SQD- Référentiel QRMI GitHub — code source, exemples supplémentaires de back-end (CUDA-Q, C, Lua)
- Document de présentation du QRMI — description technique de l'architecture du QRMI et de son intégration au calcul haute performance (HPC)
- IBM Quantum Compute Guide des sessions de service — lien entre les sessions et le QRMI
acquire/releasecycle de vie pour les backends d’ IBM