Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Führe Quanten-Workloads mit QRMI aus

Geschätzte Nutzungsdauer: unter einer Minute auf IBM Quantum®-Hardware für den SQD-Abschnitt. Diese Schätzung schließt Warteschlangenzeit und klassische Verarbeitung aus; die Laufzeit kann variieren.

Lernziele​

  1. Die Rolle, die QRMI als Middleware zwischen HPC-Schedulern und IBM Quantum-Hardware spielt

  2. Wie du den zentralen QRMI-Lebenszyklus (acquire → task_start → task_status → task_result → release) mit einem echten IBM®-Backend verwendest

  3. Wie du die übergeordneten Qiskit-Wrapper SamplerV2 und QRMIService auf Basis von QRMI verwendest

  4. Wie HPC-Scheduler (Slurm) Quantenressourcen über Umgebungsvariablen einspeisen und wie Anwendungen diese nutzen

  5. Wie du einen vollständigen SQD-(Sample-based Quantum Diagonalization)-Chemie-Workflow an N2_2 mithilfe von IBM-Hardware über QRMI ausführst

Voraussetzungen​

Hintergrund​

Die Herausforderung der Quanten-HPC-Integration​

High-Performance-Computing-(HPC)-Workflows erfordern oft eine nahtlose Koordination zwischen klassischen Rechenclustern und Quantenprozessoren (QPUs). Unterschiedliche Quantenhardware-Backends und -Dienste bieten jeweils eigene Authentifizierungsmechanismen, Übertragungsformate und APIs für den Job-Lebenszyklus. Die Integration von IBM-Quantum-Systemen in HPC-Workload-Manager (wie Slurm) erfordert eine saubere, standardisierte Schnittstelle für die Ressourcenzuweisung, Jobausführung und Session-Verwaltung.

Was QRMI ist​

Die Quantum Resource Management Interface (QRMI) ist eine in Rust geschriebene Middleware-Bibliothek, die den Zugriff auf Quantenhardware von HPC-Schedulern und klassischen Anwendungen aus standardisiert. Sie stellt eine einzige einheitliche Lifecycle-API bereit:

┌─────────────────────────────────────────────────────────────────┐
│ 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 wird als Open-Source-Projekt unter github.com/qiskit-community/qrmi veröffentlicht und ist im Übersichtspapier arXiv:2506.10052 beschrieben.

Wichtige Designentscheidungen​

Ressourcen-Lebenszyklus, nicht Circuit-Kompilierung. QRMI übernimmt den Lebenszyklus aus Acquire/Submit/Poll/Release und nichts anderes. Circuit-Kompilierung, -Optimierung und -Transpilierung verbleiben in der Anwendungsschicht (zum Beispiel Qiskit). Dies hält die Schnittstelle minimal und komponierbar.

Anbieter-Portabilitätsmodell. Während QRMI über alle unterstützten Hardware-Backends hinweg gemeinsame Aufrufe für das Job-Management bereitstellt (acquire, task_start, task_status, task_result, release), erfordert ein Anbieterwechsel dennoch unterschiedliche Kompilierungsschritte, anbieterspezifische Payload-Konstruktion und Ergebnisdekodierung in der Anwendungsschicht.

Natives IBM-Payload-Format. Für IBM-Quantum-Backends verwendet QRMI OpenQASM-3-JSON-Payloads (QiskitPrimitive), die dem Qiskit Runtime-Schema entsprechen.

Konfiguration über Umgebungsvariablen. Zugangsdaten und Endpunkt-URLs werden zur Laufzeit aus Umgebungsvariablen gelesen. In einem HPC-Cluster setzt das Slurm-QRMI-SPANK-Plugin diese automatisch, wenn ein Job gestartet wird. In einem Notebook oder einer interaktiven Session lädst du sie aus einer .env-Datei. Der Anwendungscode enthält niemals fest codierte Zugangsdaten oder Endpunkt-URLs.

HPC-Scheduler-Integration über GRES. Wenn ein Slurm-Job über die QRMI-SPANK-Plugin-Schnittstelle Quantenressourcen anfordert (#SBATCH --gres=qpu:1 und #SBATCH --qpu=ibm_kingston), fügt das Plugin QRMI_JOB_QPU_RESOURCES und QRMI_JOB_QPU_TYPES in die Job-Umgebung ein. Anwendungen rufen get_job_qpu_resources_and_types() auf, um herauszufinden, welche Ressourcen zugewiesen wurden — ohne dass fest codierte Backend-Namen erforderlich sind. QRMIService kapselt dieses Muster für Qiskit-Nutzer.

Die zentralen API-Aufrufe​

CallZweck
qrmi.acquire()Zugriff auf die Ressource erlangen (öffnet zum Beispiel eine dedizierte Session); gibt ein Lock-Token zurück
qrmi.target()Ruft die Backend-Fähigkeiten (Qubits, Gates, Coupling Map) als JSON ab
qrmi.task_start(payload)Reicht einen Quantenjob ein; gibt eine Job-ID zurück
qrmi.task_status(job_id)Fragt den Job-Status ab (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Ruft die Ergebnisse eines abgeschlossenen Jobs als rohen JSON-String ab
qrmi.task_stop(job_id)Bricht einen Job ab oder räumt ihn auf
qrmi.release(lock)Gibt die Ressourcensperre frei (schließt zum Beispiel die Session)

Was dieses Tutorial behandelt​

Dieses Tutorial ist in zwei Teile gegliedert:

Schritte 1–3 (Beispiele im kleinen Maßstab): Führen die QRMI-API anhand einer einfachen Bell-Zustand-Circuit-Demonstration auf IBM-Quantum-Hardware ein und decken sowohl die direkte Low-Level-Primitive-Nutzung als auch die High-Level-Integration von QRMIService und SamplerV2 ab.

Hardware-Beispiel im großen Maßstab: Ein vollständiger SQD-Workflow für das N2_2-Molekül bei einem Bindungsabstand von 1,0 A˚\AA (cc-pVDZ-Basis-Aktivraum, 26 räumliche Orbitale / 52 Qubits), ausgeführt auf IBM-Quantum-Hardware über QRMI. SQD kombiniert Quanten-Sampling eines mit ffsim konstruierten LUCJ-Ansatzes mit selbstkonsistenter Konfigurationswiederherstellung mittels 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")

Anforderungen​

Bevor du mit diesem Tutorial beginnst, stelle sicher, dass Folgendes installiert ist.

Einrichtung der Python-Umgebung​

Für Linux sind auf PyPI vorgefertigte Binär-Wheels verfügbar, sodass der Standardbefehl pip install auf Linux-/HPC-Systemen direkt funktioniert.

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"
Plattformen ohne vorgefertigte Wheels

Wenn pip QRMI aus dem Quellcode baut, stelle sicher, dass du eine moderne Rust-Toolchain hast (Rust ≥ 1.91.1, installiert über rustup von rustup.rs).

Wähle in Jupyter den Kernel QRMI IBM aus, starte ihn dann neu und führe die Notebook-Zellen der Reihe nach aus. Die gespeicherten Ausgaben stammen aus dem Hardware-Lauf des Beitragenden; die Installationsbefehle geben nicht die genauen Versionen an, die für diesen Lauf verwendet wurden.

Erforderliche Zugangsdaten​

Erstelle für eine eigenständige Ausführung eine .env-Datei neben diesem Notebook mit den folgenden Werten und ersetze dabei die Platzhalter für die Zugangsdaten. Halte diese Datei privat. Wenn du ein anderes Backend auswählst, aktualisiere sowohl dessen Namen als auch die Präfixe der Umgebungsvariablen.

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

Verwende bei einer Slurm-Zuweisung die vom Cluster bereitgestellten Ressourceneinstellungen und Zugangsdaten. Das Notebook behält vorhandene Umgebungswerte bei.

Setup​

Importiere die Abhängigkeiten und lade die Ressourcenkonfiguration.

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.

Beispiele im kleinen Maßstab​

Die Schritte 1–3 führen die QRMI-API anhand einfacher Circuits ein. Jeder Schritt entspricht einer zentralen Phase des QRMI-Lifecycle bei IBM-Quantum-Hardware.

Der Payload für diese ersten Schritte ist ein kleiner Bell-Zustand-Circuit, der so gewählt wurde, dass er schnell und kostengünstig auszuführen ist.

Diese Beispiele verwenden Hardware, da sie die Remote-Ressourcenzuweisung und das Job-Management demonstrieren. Ein lokaler Circuit-Simulator validiert die QRMI-Service- und Scheduler-Integration nicht. Das Ausführen dieses Notebooks reicht IBM-Quantum-Jobs ein und erfordert Zugriff auf das konfigurierte Backend.

Schritt 1: Das klassische Problem einer Quantenressource zuordnen​

Der erste Schritt in jedem QRMI-Workflow besteht darin, ein QuantumResource-Objekt zu erstellen und zu überprüfen, dass es zugänglich ist.

get_target() ruft die Hardwarebeschreibung des Backends (Anzahl der Qubits, Basis-Gates, Coupling Map) ab und verpackt sie als Qiskit-Target-Objekt, das der Transpiler in Schritt 2 verwendet.

Schritt 2: Das Problem für die Ausführung auf Quantenhardware optimieren​

Verwende vor der Einreichung Qiskit, um den Circuit mithilfe des in Schritt 1 abgerufenen Target-Objekts in die Befehlssatzarchitektur (ISA) des Backends zu transpilieren.

Das Beispiel erstellt anschließend ein Payload.QiskitPrimitive, das den OpenQASM-3-Circuit-String und die Job-Metadaten in das IBM-Primitive-Schema verpackt.

Schritt 3: Ausführung mit QRMI-Primitives​

Nachdem der Payload erstellt wurde, reicht das Beispiel den Job ein und fragt wiederholt ab, bis er abgeschlossen ist. task_start() gibt sofort eine Job-ID zurück; task_status() wird abgefragt, bis der Status nicht mehr Queued/Running ist. Die Ergebnisse werden als roher JSON-String abgerufen und geparst, um Messwerte zu extrahieren.

Die folgende Zelle fasst Belegung, Ausführung und Aufräumen der Ressource zusammen, damit auch bei Fehlern nach der Belegung eine vom Notebook geöffnete Session freigegeben wird.

# ── 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.

Qiskit-Schnittstelle auf höherer Ebene: QRMIService und SamplerV2​

Der oben beschriebene Roh-Lifecycle bietet explizite Kontrolle über jeden Aufruf. Für Standard-Qiskit-Workflows stellt QRMI ein SamplerV2-Primitive bereit, das BaseSamplerV2 implementiert.

Lifecycle-Verwaltung

SamplerV2 übernimmt Payload-Serialisierung, Einreichung (task_start), Polling und Ergebnisdekodierung. In einer HPC-Batch-Umgebung (zum Beispiel mit Slurm) werden Zuweisung und Freigabe vom Scheduler und dem SPANK-Plugin verwaltet. In einer interaktiven Python-Session mit direkten Low-Level-API-Objekten können acquire() und release() verwendet werden, um dedizierte Sessions explizit zu verwalten.

# 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}

HPC-Kontext: Slurm-Ressourceninjektion​

In einem HPC-Cluster fordern Nutzer Quantenressourcen mithilfe der Slurm-GRES-Syntax zusammen mit den Optionen des QRMI-SPANK-Plugins an. Das Plugin übernimmt die Injektion von Zugangsdaten und Ressourcen automatisch:

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

Der Anwendungscode ermittelt seine zugewiesenen Ressourcen zur Laufzeit — ohne fest codierte Backend-Namen:

# 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

Hardware-Beispiel im großen Maßstab: SQD für N2_2​

Hier fügen wir alle Komponenten zu einem vollständigen Quantenchemie-Workflow in größerem Maßstab zusammen, der auf echter IBM-Quantum-Hardware über QRMI ausgeführt wird.

SQD kombiniert Folgendes:

  1. Quanten-Sampling eines mit ffsim konstruierten Local-Unitary-Cluster-Jastrow-(LUCJ)-Ansatzes, initialisiert aus CCSD-Amplituden
  2. Hardware-bewusste Transpilierung passend zur Heavy-Hex-Gitter-Topologie über generate_lucj_pass_manager
  3. Sampling-Ausführung auf IBM-Quantum-Hardware, verwaltet über QRMIService und QRMI SamplerV2
  4. Klassische Nachbearbeitung: selbstkonsistente Konfigurationswiederherstellung und iterative Unterraum-Diagonalisierung mittels qiskit-addon-sqd

Wir wenden SQD auf N2_2 bei einem Bindungsabstand von 1,0 A˚\AA mit einem aus dem cc-pVDZ-Basissatz abgeleiteten Aktivraum an (26 räumliche Orbitale, entsprechend 52 Spinorbitalen/Qubits).

Referenzenergie für den N2_2/cc-pVDZ-Aktivraum (Bindungsabstand 1,0 A˚\AA):

  • Referenzenergie (separate SCI-Berechnung): −109,22802922 Ha
Genauigkeit des gespeicherten Laufs

Der unten gezeigte SQD-Lauf demonstriert eine erfolgreiche End-to-End-QRMI-Ausführung auf IBM-Quantum-Hardware. Mit einer einzigen LUCJ-Wiederholung und 100.000 Shots liegt das Ergebnis etwa 23,7 kcal/mol über der Referenzenergie und erreicht keine chemische Genauigkeit (≤ 1 kcal/mol). Eine Änderung von n_reps, der Shot-Anzahl oder der Anzahl der SQD-Iterationen könnte die Genauigkeit verbessern, erfordert jedoch weitere Tests.

Im gespeicherten Lauf hat der ffsim-Pass-Manager die Interaktionen mit entgegengesetztem Spin (24, 24) und (20, 20) entfernt, da das Backend sie nicht verarbeiten konnte. Die berichteten Ergebnisse verwenden diesen angepassten Circuit.

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

Nächste Schritte​

Empfehlungen

Wenn du diese Arbeit interessant fandest, könnten dich folgende Materialien interessieren: