Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Exécuter des charges de travail quantiques avec QRMI

Estimation d'utilisation : moins d'une minute sur du matériel IBM Quantum® pour la section SQD. Cette estimation exclut le temps de file d'attente et le traitement classique ; le temps d'exécution peut varier.

Objectifs d'apprentissage​

  1. Le rôle que joue QRMI en tant qu'intergiciel entre les ordonnanceurs HPC et le matériel IBM Quantum

  2. Comment utiliser le cycle de vie de base de QRMI (acquire → task_start → task_status → task_result → release) avec un véritable backend IBM®

  3. Comment utiliser les wrappers de plus haut niveau Qiskit SamplerV2 et QRMIService au-dessus de QRMI

  4. Comment les ordonnanceurs HPC (Slurm) injectent des ressources quantiques via des variables d'environnement, et comment les applications les consomment

  5. Comment exécuter un workflow complet de chimie SQD (Sample-based Quantum Diagonalization) sur N2_2 en utilisant du matériel IBM via QRMI

Prérequis​

Contexte​

Le défi de l'intégration quantique-HPC​

Les workflows de 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 matériels quantiques et services exposent des mécanismes d'authentification, des formats de communication et des API de cycle de vie des tâches distincts. L'intégration des systèmes IBM Quantum dans les gestionnaires de charge de travail HPC (comme Slurm) nécessite une interface propre et standard pour l'acquisition des ressources, l'exécution des tâches et la gestion des sessions.

Ce qu'est QRMI​

La Quantum Resource Management Interface (QRMI) est une bibliothèque intergicielle écrite en Rust qui standardise l'accès au matériel quantique depuis les ordonnanceurs HPC et les applications classiques. Elle expose une API de cycle de vie unique et unifiée :

┌─────────────────────────────────────────────────────────────────┐
│ 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é en tant que projet open source sur github.com/qiskit-community/qrmi et est décrit dans l'article de présentation arXiv:2506.10052.

Choix de conception clés​

Cycle de vie des ressources, pas compilation de circuits. QRMI gère le cycle de vie acquire/submit/poll/release et rien d'autre. La compilation, l'optimisation et la transpilation des circuits restent dans la couche applicative (par exemple, Qiskit). Cela permet de garder une interface minimale et composable.

Modèle de portabilité entre 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, changer de fournisseur nécessite également des passes de compilation différentes, une construction de charge utile (payload) spécifique au fournisseur, et un décodage des résultats dans la couche applicative.

Format de charge utile natif IBM. Pour les backends IBM Quantum, QRMI utilise des charges utiles JSON OpenQASM 3 (QiskitPrimitive) conformes au schéma Qiskit Runtime.

Configuration via des variables d'environnement. Les identifiants et les URL de point de terminaison sont lus à partir de variables d'environnement au moment de l'exécution. Dans un cluster HPC, le plugin SPANK QRMI de Slurm les définit automatiquement lors de l'envoi d'une tâche. Dans un notebook ou une session interactive, tu les charges à partir d'un fichier .env. Le code applicatif ne contient jamais d'identifiants ou d'URL de point de terminaison codés en dur.

Intégration à l'ordonnanceur HPC via GRES. Lorsqu'une tâche Slurm demande des ressources quantiques à l'aide de l'interface du plugin SPANK QRMI (#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 get_job_qpu_resources_and_types() pour découvrir les ressources allouées — aucun nom de backend codé en dur n'est nécessaire. QRMIService encapsule ce schéma pour les utilisateurs de Qiskit.

Les appels de l'API principale​

AppelObjectif
qrmi.acquire()Acquiert l'accès à la ressource (par exemple, ouvre une session dédiée) ; renvoie un jeton de verrouillage
qrmi.target()Récupère les capacités du backend (qubits, portes, carte de couplage) au format JSON
qrmi.task_start(payload)Soumet une tâche quantique ; renvoie un ID de tâche
qrmi.task_status(job_id)Interroge l'état de la tâche (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Récupère les résultats de la tâche terminée sous forme de chaîne JSON brute
qrmi.task_stop(job_id)Annule ou nettoie une tâche
qrmi.release(lock)Libère le verrou de ressource (par exemple, ferme la session)

Ce que couvre ce tutoriel​

Ce tutoriel est structuré en deux parties :

Étapes 1 à 3 (exemples à petite échelle) : présentent l'API QRMI avec une démonstration simple d'un circuit d'état de Bell sur du matériel IBM Quantum, couvrant l'utilisation directe des primitives de bas niveau ainsi que l'intégration de haut niveau QRMIService et SamplerV2.

Exemple matériel à grande échelle : un workflow SQD complet pour la molécule N2_2 à une distance de liaison de 1.0 A˚\AA (espace actif de base cc-pVDZ, 26 orbitales spatiales / 52 qubits), exécuté sur du matériel IBM Quantum via QRMI. SQD combine un échantillonnage quantique d'un ansatz LUCJ construit avec ffsim et une récupération de configuration auto-cohérente avec qiskit-addon-sqd.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"dotenv": "python-dotenv", "ffsim": "ffsim", "matplotlib": "matplotlib", "numpy": "numpy", "pyscf": "pyscf", "qiskit": "qiskit", "qiskit_addon_sqd": "qiskit-addon-sqd", "qrmi": "qrmi"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")

Configuration requise​

Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants.

Configuration de l'environnement Python​

Des wheels binaires précompilées sont disponibles pour Linux sur PyPI, donc la commande 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"
Plateformes sans wheels précompilées

Si pip compile QRMI à partir des sources, assure-toi de disposer d'une chaîne d'outils Rust moderne (Rust ≥ 1.91.1 installée via rustup depuis rustup.rs).

Sélectionne le noyau QRMI IBM dans Jupyter, puis redémarre-le et exécute les cellules du notebook dans l'ordre. Les sorties enregistrées proviennent de l'exécution matérielle du contributeur ; les commandes d'installation ne précisent pas les versions exactes utilisées pour cette exécution.

Identifiants requis​

Pour une exécution autonome, crée un fichier .env à côté de ce notebook avec les valeurs suivantes, en remplaçant les espaces réservés pour les identifiants. Garde ce fichier privé. Si tu sélectionnes un backend différent, mets à jour à 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, utilise les paramètres de ressources et les identifiants fournis par le cluster. Le notebook préserve les valeurs d'environnement existantes.

Configuration​

Importe les dépendances et charge 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.")
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 essentielle du cycle de vie QRMI sur du matériel IBM Quantum.

La charge utile de ces étapes initiales est un petit circuit d'état de Bell choisi pour être rapide et peu coûteux à exécuter.

Ces exemples utilisent du matériel réel car ils démontrent l'allocation de ressources distantes et la gestion des tâches. Un simulateur de circuit local ne permet pas de valider l'intégration du service QRMI et de l'ordonnanceur. L'exécution de ce notebook soumet 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 l'encapsule dans un objet Qiskit Target, que le transpileur utilise à l'étape 2.

Étape 2 : optimiser le problème pour l'exécution sur du matériel quantique​

Avant la soumission, utilise Qiskit pour transpiler 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 construit ensuite un Payload.QiskitPrimitive, qui encapsule la chaîne de circuit OpenQASM 3 et les métadonnées de la tâche dans le schéma de primitive IBM.

Étape 3 : exécuter à l'aide des primitives QRMI​

Une fois la charge utile construite, l'exemple soumet la tâche et interroge son état jusqu'à son achèvement. task_start() renvoie immédiatement un ID 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 forme de chaîne JSON brute puis analysés pour extraire les échantillons de mesure.

La cellule suivante regroupe l'acquisition, l'exécution et le nettoyage afin que les échecs survenant après l'acquisition libèrent quand même une session détenue par le 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.")
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 plus haut niveau : QRMIService et SamplerV2​

Le cycle de vie brut ci-dessus offre un contrôle explicite sur chaque appel. Pour les workflows Qiskit standards, QRMI fournit une primitive SamplerV2 implémentant BaseSamplerV2.

Gestion du cycle de vie

SamplerV2 gère la sérialisation de la charge utile, la soumission (task_start), l'interrogation d'état et le décodage des résultats. Dans un contexte de traitement par lots HPC (par exemple avec Slurm), l'allocation et la libération sont gérées par l'ordonnanceur et le plugin SPANK. Dans une session Python interactive utilisant directement les objets d'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()}")
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 à l'aide de la syntaxe GRES de Slurm associée aux options du plugin SPANK QRMI. 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 set

Le code applicatif découvre ses ressources allouées au moment de l'exécution — aucun nom de backend 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()}"
)
Resources allocated by scheduler:
ibm_kingston (ibm-quantum-compute-service)

QRMIService found: ibm_kingston accessible=True

Exemple matériel à grande échelle : SQD sur N2_2​

Nous rassemblons ici tous les composants en un workflow complet de chimie quantique à plus grande échelle, exécuté sur du matériel IBM Quantum réel via QRMI.

SQD combine les éléments suivants :

  1. Échantillonnage quantique d'un ansatz Local Unitary Cluster Jastrow (LUCJ) construit avec ffsim et initialisé à partir des amplitudes CCSD
  2. Transpilation adaptée au matériel correspondant à la topologie en treillis heavy-hex via generate_lucj_pass_manager
  3. Exécution de l'échantillonnage sur du matériel IBM Quantum géré via QRMIService et SamplerV2 de QRMI
  4. Post-traitement classique : récupération de configuration auto-cohérente et diagonalisation itérative de sous-espace à l'aide de qiskit-addon-sqd

Nous appliquons SQD à N2_2 à une distance de liaison de 1,0 A˚\AA avec un espace actif dérivé de l'ensemble de base cc-pVDZ (26 orbitales spatiales, correspondant à 52 spin-orbitales/qubits).

Énergie de référence pour l'espace actif N2_2 /cc-pVDZ (distance de liaison 1.0 A˚\AA) :

  • Énergie de référence (calcul SCI séparé) : −109.22802922 Ha
Précision de l'exécution enregistrée

L'exécution SQD ci-dessous démontre une exécution QRMI de bout en bout réussie sur du matériel IBM Quantum. Avec une seule répétition LUCJ et 100 000 shots, 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 n_reps, le nombre de shots, ou le nombre d'itérations SQD pourrait améliorer la précision, mais cela nécessite des tests supplémentaires.

Dans l'exécution enregistrée, le gestionnaire de passes ffsim a supprimé les interactions de spins opposés (24, 24) et (20, 20) car le backend ne pouvait pas les prendre en charge. Les résultats rapportés 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()
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

Prochaines étapes​

Recommandations

Si tu as trouvé ce travail intéressant, les ressources suivantes pourraient t'intéresser :