Skip to main content
IBM Quantum Platform

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

  1. Le rôle joué par QRMI en tant que couche intermédiaire entre les planificateurs HPC et le matériel d' IBM Quantum
  2. Comment utiliser le cycle de vie de base du QRMI (acquire → task_start → task_status → task_result → release) avec un backend « real- IBM® »
  3. Comment utiliser les couches de haut niveau de Qiskit et SamplerV2 les wrappers QRMIService s'appuyant sur QRMI
  4. Comment les planificateurs HPC (Slurm) intègrent les ressources quantiques via des variables d'environnement et comment les applications les exploitent
  5. Comment exécuter un workflow chimique complet de SQD (diagonalisation quantique par échantillonnage) sur N « 2_2 » à l’aide du matériel « IBM » via QRMI

Prérequis


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 2_2 e à une distance de liaison de 1.0 A˚\AA (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"
Plateformes sans roues intégrées

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

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.

Gestion du cycle de vie

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 set

Le 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 2_2

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 :

  1. Échantillonnage quantique d'un modèle LUCJ (Local Unitary Cluster Jastrow) construit à l'aide de ffsim et initialisé à partir d'amplitudes CCSD
  2. Transpilation tenant compte du matériel et respectant la topologie en treillis « heavy-hex » via generate_lucj_pass_manager
  3. Exécution de l'échantillonnage sur le matériel « IBM Quantum » géré via et QRMIService QRMI SamplerV2
  4. 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 2_2, à une distance de liaison de 1.0 A˚\AA, 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 2_2 /cc-pVDZ espace actif (distance de liaison 1.0 A˚\AA ) :

  • Énergie de référence (calcul SCI distinct) : − 109.22802922 Ha
Précision de la session enregistrée

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)
Output of the previous code cell

Etapes suivantes

Recommandations

Si ce travail vous a paru intéressant, les documents suivants pourraient vous intéresser :

Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.