Rulează sarcini cuantice cu QRMI
Estimare de utilizare: sub un minut pe hardware IBM Quantum® pentru secțiunea SQD. Această estimare exclude timpul de așteptare în coadă și procesarea clasică; timpul de execuție poate varia.
Rezultate ale învățării
-
Rolul pe care QRMI îl joacă ca middleware între planificatorii HPC și hardware-ul IBM Quantum
-
Cum să folosești ciclul de viață de bază al QRMI (
acquire→task_start→task_status→task_result→release) pe un backend IBM® real -
Cum să folosești wrapper-ele Qiskit de nivel superior
SamplerV2șiQRMIServicepeste QRMI -
Cum planificatorii HPC (Slurm) injectează resurse cuantice prin variabile de mediu și cum le consumă aplicațiile
-
Cum să rulezi un flux complet de chimie SQD (Sample-based Quantum Diagonalization) pe N folosind hardware IBM prin QRMI
Condiții prealabile
-
Familiaritate de bază cu mediile virtuale Python și chimia cuantică
Context
Provocarea integrării quantum-HPC
Fluxurile de lucru de calcul de înaltă performanță (HPC) necesită adesea o coordonare fără sudură între clusterele de calcul clasic și unitățile de procesare cuantică (QPU-uri). Diferite backend-uri hardware cuantice și servicii expun mecanisme de autentificare, formate de transmisie și API-uri de ciclu de viață al joburilor distincte. Integrarea sistemelor IBM Quantum în managerii de sarcini de lucru HPC (cum ar fi Slurm) necesită o interfață curată și standard pentru achiziția resurselor, execuția joburilor și gestionarea sesiunilor.
Ce este QRMI
Quantum Resource Management Interface (QRMI) este o bibliotecă middleware scrisă în Rust care standardizează accesul la hardware-ul cuantic din planificatoarele HPC și aplicațiile clasice. Aceasta expune un singur API unificat de ciclu de viață:
┌─────────────────────────────────────────────────────────────────┐
│ 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 este publicat ca proiect open-source la github.com/qiskit-community/qrmi și este descris în lucrarea de prezentare arXiv:2506.10052.
Alegeri de proiectare cheie
Ciclul de viață al resurselor, nu compilarea circuitelor. QRMI gestionează ciclul de viață acquire/submit/poll/release și nimic altceva. Compilarea, optimizarea și transpilarea circuitelor rămân în stratul aplicației (de exemplu, Qiskit). Acest lucru păstrează interfața minimală și compozabilă.
Model de portabilitate între furnizori. Deși QRMI oferă apeluri comune de gestionare a joburilor (acquire, task_start, task_status, task_result, release) pentru backend-urile hardware acceptate, schimbarea furnizorilor necesită de asemenea treceri de compilare diferite, construirea de payload-uri specifice furnizorului și decodarea rezultatelor în stratul aplicației.
Format nativ de payload IBM. Pentru backend-urile IBM Quantum, QRMI utilizează payload-uri JSON OpenQASM 3 (QiskitPrimitive) conforme cu schema Qiskit Runtime.
Configurare prin variabile de mediu. Datele de acreditare și URL-urile punctelor finale sunt citite din variabile de mediu la momentul execuției. Într-un cluster HPC, plugin-ul Slurm QRMI SPANK le setează automat atunci când jobul este lansat de scheduler. Într-un notebook sau o sesiune interactivă, le încarci dintr-un fișier .env. Codul aplicației nu conține niciodată date de acreditare sau URL-uri ale punctelor finale codificate direct.
Integrare cu schedulerul HPC prin GRES. Atunci când un job Slurm solicită resurse cuantice utilizând interfața plugin-ului QRMI SPANK (#SBATCH --gres=qpu:1 și #SBATCH --qpu=ibm_kingston), plugin-ul injectează QRMI_JOB_QPU_RESOURCES și QRMI_JOB_QPU_TYPES în mediul jobului. Aplicațiile apelează get_job_qpu_resources_and_types() pentru a descoperi ce resurse au fost alocate — fără a fi necesare nume de backend codificate direct. QRMIService încapsulează acest tipar pentru utilizatorii Qiskit.
Apelurile API de bază
| Call | Purpose |
|---|---|
qrmi.acquire() | Achiziționează accesul la resursă (de exemplu, deschide o sesiune dedicată); returnează un token de blocare |
qrmi.target() | Preia capacitățile backend-ului (qubiți, porți, hartă de cuplare) ca JSON |
qrmi.task_start(payload) | Trimite un job cuantic; returnează un ID de job |
qrmi.task_status(job_id) | Interoghează starea jobului (Queued, Running, Completed, Failed) |
qrmi.task_result(job_id) | Preia rezultatele jobului finalizat ca un șir JSON brut |
qrmi.task_stop(job_id) | Anulează sau curăță un job |
qrmi.release(lock) | Eliberează blocarea resursei (de exemplu, închide sesiunea) |
Ce acoperă acest tutorial
Acest tutorial este structurat în două părți:
Pașii 1–3 (exemple la scară mică): Introduc API-ul QRMI printr-o demonstrație simplă a unui circuit de stare Bell pe hardware IBM Quantum, acoperind atât utilizarea directă a primitivelor de nivel scăzut, cât și integrarea de nivel înalt QRMIService și SamplerV2.
Exemplu hardware la scară mare: Un flux de lucru SQD complet pentru molecula N la o distanță de legătură de 1.0 (spațiu activ bazat pe cc-pVDZ, 26 orbitali spațiali / 52 qubiți), executat pe hardware IBM Quantum prin QRMI. SQD combină eșantionarea cuantică a unui ansatz LUCJ construit cu ffsim și recuperarea de configurație auto-consistentă cu 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")
Cerințe
Înainte de a începe acest tutorial, asigură-te că ai instalate următoarele.
Configurarea mediului Python
Sunt disponibile wheel-uri binare pre-construite pentru Linux pe PyPI, astfel încât pip install standard funcționează direct pe sistemele 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"
Dacă pip construiește QRMI din sursă, asigură-te că ai un toolchain Rust modern (Rust ≥ 1.91.1 instalat prin rustup de la rustup.rs).
Selectează kernelul QRMI IBM în Jupyter, apoi repornește-l și rulează celulele notebook-ului în ordine. Ieșirile salvate provin din rularea pe hardware a contributorului; comenzile de instalare nu specifică versiunile exacte utilizate pentru acea rulare.
Date de acreditare necesare
- IBM Quantum: cheie API IAM și CRN-ul serviciului de la IBM Quantum Platform
Pentru execuție de sine stătătoare, creează un fișier .env lângă acest notebook cu următoarele valori, înlocuind substituenții pentru datele de acreditare. Păstrează acest fișier privat. Dacă selectezi un backend diferit, actualizează atât numele acestuia, cât și prefixele variabilelor de mediu.
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
Pentru o alocare Slurm, folosește setările de resurse și datele de acreditare furnizate de cluster. Notebook-ul păstrează valorile de mediu existente.
Configurare
Importă dependențele și încarcă configurația resursei.
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.
Exemple la scară mică
Pașii 1–3 introduc API-ul QRMI folosind circuite simple. Fiecare pas corespunde unei faze de bază a ciclului de viață QRMI pe hardware IBM Quantum.
Payload-ul pentru acești pași inițiali este un circuit mic de stare Bell, ales pentru a fi rapid și ieftin de rulat.
Aceste exemple utilizează hardware deoarece demonstrează alocarea de resurse la distanță și gestionarea joburilor. Un simulator local de circuite nu validează serviciul QRMI și integrarea cu schedulerul. Rularea acestui notebook trimite joburi IBM Quantum și necesită acces la backend-ul configurat.
Pasul 1: Corelează problema clasică cu o resursă cuantică
Primul pas în orice flux de lucru QRMI este crearea unui obiect QuantumResource și verificarea faptului că este accesibil.
get_target() preia descrierea hardware a backend-ului (numărul de qubiți, porțile de bază, harta de cuplare) și o împachetează ca obiect Qiskit Target, pe care transpiler-ul îl folosește la Pasul 2.
Pasul 2: Optimizează problema pentru execuția pe hardware cuantic
Înainte de trimitere, folosește Qiskit pentru a transpila circuitul la arhitectura setului de instrucțiuni (ISA) a backend-ului, utilizând obiectul Target preluat la Pasul 1.
Exemplul construiește apoi un Payload.QiskitPrimitive, care încapsulează șirul de circuit OpenQASM 3 și metadatele jobului în schema primitivă IBM.
Pasul 3: Execută folosind primitivele QRMI
Odată construit payload-ul, exemplul trimite jobul și interoghează pentru finalizare. task_start() returnează imediat un ID de job; task_status() este interogat până când starea nu mai este Queued/Running. Rezultatele sunt preluate ca un șir JSON brut și analizate pentru a extrage eșantioanele de măsurare.
Următoarea celulă păstrează achiziția, execuția și curățarea împreună, astfel încât eșecurile ulterioare achiziției să elibereze totuși o sesiune deținută de 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.
Interfață Qiskit de nivel mai înalt: QRMIService și SamplerV2
Ciclul de viață brut de mai sus oferă control explicit asupra fiecărui apel. Pentru fluxurile de lucru Qiskit standard, QRMI oferă o primitivă SamplerV2 care implementează BaseSamplerV2.
SamplerV2 gestionează serializarea payload-ului, trimiterea (task_start), interogarea și decodarea rezultatelor. Într-un context de batch HPC (de exemplu, cu Slurm), alocarea și eliberarea sunt gestionate de scheduler și de plugin-ul SPANK. Într-o sesiune Python interactivă care utilizează direct obiecte API de nivel scăzut, acquire() și release() pot fi folosite pentru a gestiona explicit sesiuni dedicate.
# 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}
Context HPC: injectarea resurselor Slurm
Într-un cluster HPC, utilizatorii solicită resurse cuantice folosind sintaxa Slurm GRES împreună cu opțiunile plugin-ului QRMI SPANK. Plugin-ul gestionează automat injectarea datelor de acreditare și a resurselor:
#SBATCH --gres=qpu:1
#SBATCH --qpu=ibm_kingston
python my_workflow.py # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already set
Codul aplicației descoperă resursele alocate la momentul execuției — fără nume de backend codificate direct:
# 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
Exemplu hardware la scară mare: SQD pe N
Aici punem toate componentele împreună într-un flux de lucru complet de chimie cuantică la o scară mai mare, executat pe hardware IBM Quantum real prin QRMI.
SQD combină următoarele:
- Eșantionare cuantică a unui ansatz Local Unitary Cluster Jastrow (LUCJ) construit folosind
ffsimși inițializat din amplitudinile CCSD - Transpilare adaptată hardware-ului, conformă cu topologia rețelei heavy-hex prin
generate_lucj_pass_manager - Execuție de eșantionare pe hardware IBM Quantum gestionată prin
QRMIServiceși QRMISamplerV2 - Post-procesare clasică: recuperare de configurație auto-consistentă și diagonalizare iterativă a subspațiului folosind
qiskit-addon-sqd
Aplicăm SQD la N la o distanță de legătură de 1.0 cu un spațiu activ derivat din setul de bază cc-pVDZ (26 orbitali spațiali, corespunzând la 52 de spin-orbitali/qubiți).
Energia de referință pentru spațiul activ N /cc-pVDZ (distanță de legătură 1.0 ):
- Energia de referință (calcul SCI separat): −109.22802922 Ha
Rularea SQD de mai jos demonstrează o execuție QRMI de tip end-to-end reușită pe hardware IBM Quantum. Cu o singură repetiție LUCJ și 100.000 de shot-uri, rezultatul se încheie la aproximativ 23,7 kcal/mol peste energia de referință și nu atinge acuratețea chimică (≤ 1 kcal/mol). Modificarea n_reps, a numărului de shot-uri sau a numărului de iterații SQD ar putea îmbunătăți acuratețea, dar necesită testări suplimentare.
În rularea salvată, pass manager-ul ffsim a eliminat interacțiunile de spin opus (24, 24) și (20, 20) deoarece backend-ul nu le putea acomoda. Rezultatele raportate folosesc acest circuit ajustat.
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)

Pașii următori
Dacă ai găsit această lucrare interesantă, s-ar putea să fii interesat de următoarele materiale:
-
Tutorial de diagonalizare cuantică bazată pe eșantioane — fluxul complet de lucru de chimie SQD pe IBM Quantum Platform, inclusiv molecule și seturi de bază mai mari
-
Diagonalizare cuantică Krylov bazată pe eșantioane — o metodă înrudită care folosește circuite de evoluție temporală pentru modele de rețea fermionice
-
qiskit-addon-sqddocumentation — referință API completă și tutoriale suplimentare pentru biblioteca de post-procesare SQD -
Repozitoriul GitHub QRMI — cod sursă, exemple suplimentare de backend (CUDA-Q, C, Lua)
-
Lucrarea de prezentare QRMI — descriere tehnică a arhitecturii QRMI și a integrării HPC
-
Ghidul sesiunilor IBM Quantum Compute Service — cum se raportează sesiunile la ciclul de viață
acquire/releaseal QRMI pentru backend-urile IBM