Lewati ke konten utama
Proyek ini masih dalam tahap beta. Menemukan bug? Buat isu GitHub. Punya pertanyaan atau ide? Tanya di Discussions โ€” kami sangat menghargai masukan kamu!
Bagaimana kualitas terjemahan di halaman ini?

Jalankan workload kuantum dengan QRMI

Estimasi penggunaan: kurang dari satu menit pada hardware IBM Quantumยฎ untuk bagian SQD. Estimasi ini tidak termasuk waktu antrian dan pemrosesan klasik; runtime dapat bervariasi.

Hasil pembelajaranโ€‹

  1. Peran QRMI sebagai middleware antara scheduler HPC dan hardware IBM Quantum

  2. Cara menggunakan siklus hidup inti QRMI (acquire โ†’ task_start โ†’ task_status โ†’ task_result โ†’ release) terhadap backend IBMยฎ yang sebenarnya

  3. Cara menggunakan wrapper tingkat-tinggi Qiskit SamplerV2 dan QRMIService di atas QRMI

  4. Bagaimana scheduler HPC (Slurm) menyuntikkan resource kuantum melalui variabel lingkungan dan bagaimana aplikasi menggunakannya

  5. Cara menjalankan alur kerja kimia SQD (Sample-based Quantum Diagonalization) lengkap pada N2_2 dengan menggunakan hardware IBM melalui QRMI

Prasyaratโ€‹

Latar belakangโ€‹

Tantangan integrasi kuantum-HPCโ€‹

Alur kerja high-performance computing (HPC) sering membutuhkan koordinasi yang mulus antara klaster komputasi klasik dan quantum processing unit (QPU). Backend dan layanan hardware quantum yang berbeda mengekspos mekanisme autentikasi, format wire, dan API siklus hidup job yang berbeda-beda. Mengintegrasikan sistem IBM Quantum ke dalam workload manager HPC (seperti Slurm) membutuhkan antarmuka yang bersih dan standar untuk akuisisi resource, eksekusi job, dan manajemen session.

Apa itu QRMIโ€‹

Quantum Resource Management Interface (QRMI) adalah library middleware yang ditulis dalam Rust yang menstandardisasi akses ke hardware quantum dari scheduler HPC dan aplikasi klasik. Ia mengekspos satu API siklus hidup yang terpadu:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 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 dipublikasikan sebagai proyek open-source di github.com/qiskit-community/qrmi dan dijelaskan dalam paper overview arXiv:2506.10052.

Pilihan desain utamaโ€‹

Siklus hidup resource, bukan kompilasi circuit. QRMI menangani siklus hidup acquire/submit/poll/release dan tidak ada lagi selain itu. Kompilasi circuit, optimisasi, dan transpilasi tetap berada di layer aplikasi (misalnya, Qiskit). Ini menjaga antarmuka tetap minimal dan dapat dikomposisi.

Model portabilitas vendor. Meskipun QRMI menyediakan panggilan manajemen job umum (acquire, task_start, task_status, task_result, release) di berbagai backend hardware yang didukung, mengganti vendor juga membutuhkan pass kompilasi yang berbeda, konstruksi payload spesifik vendor, dan decoding hasil di layer aplikasi.

Format payload native IBM. Untuk backend IBM Quantum, QRMI menggunakan payload JSON OpenQASM 3 (QiskitPrimitive) yang sesuai dengan skema Qiskit Runtime.

Konfigurasi melalui variabel lingkungan. Kredensial dan URL endpoint dibaca dari variabel lingkungan saat runtime. Di klaster HPC, plugin SPANK Slurm QRMI mengatur ini secara otomatis saat job dikirim. Di notebook atau session interaktif, Anda memuatnya dari file .env. Kode aplikasi tidak pernah berisi kredensial atau URL endpoint yang di-hardcode.

Integrasi scheduler HPC melalui GRES. Ketika job Slurm meminta resource quantum menggunakan antarmuka plugin SPANK QRMI (#SBATCH --gres=qpu:1 dan #SBATCH --qpu=ibm_kingston), plugin menyisipkan QRMI_JOB_QPU_RESOURCES dan QRMI_JOB_QPU_TYPES ke dalam lingkungan job. Aplikasi memanggil get_job_qpu_resources_and_types() untuk menemukan resource mana yang dialokasikan โ€” tidak perlu nama backend yang di-hardcode. QRMIService membungkus pola ini untuk pengguna Qiskit.

Panggilan API intiโ€‹

PanggilanTujuan
qrmi.acquire()Mengakuisisi akses ke resource (misalnya, membuka session khusus); mengembalikan token lock
qrmi.target()Mengambil kapabilitas backend (qubit, gate, coupling map) sebagai JSON
qrmi.task_start(payload)Mengirimkan job quantum; mengembalikan ID job
qrmi.task_status(job_id)Melakukan poll status job (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)Mengambil hasil job yang selesai sebagai string JSON mentah
qrmi.task_stop(job_id)Membatalkan atau membersihkan job
qrmi.release(lock)Melepaskan lock resource (misalnya, menutup session)

Yang dibahas tutorial iniโ€‹

Tutorial ini terstruktur dalam dua bagian:

Langkah 1โ€“3 (contoh skala kecil): Memperkenalkan API QRMI dengan demonstrasi circuit Bell state sederhana pada hardware IBM Quantum, mencakup penggunaan primitive tingkat rendah secara langsung serta integrasi QRMIService dan SamplerV2 tingkat tinggi.

Contoh hardware skala besar: Alur kerja SQD lengkap untuk molekul N2_2 pada jarak ikatan 1,0 Aหš\AA (active space basis cc-pVDZ, 26 orbital spasial / 52 qubit), dijalankan pada hardware IBM Quantum melalui QRMI. SQD menggabungkan sampling quantum dari ansatz LUCJ yang dibangun dengan ffsim dan pemulihan konfigurasi self-consistent dengan 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")

Persyaratanโ€‹

Sebelum memulai tutorial ini, pastikan Anda telah menginstal hal-hal berikut.

Pengaturan lingkungan Pythonโ€‹

Wheel biner yang sudah dibuild tersedia untuk Linux di PyPI, sehingga pip install standar langsung berfungsi pada sistem 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"
Platform tanpa wheel yang sudah dibuild

Jika pip membangun QRMI dari source, pastikan Anda memiliki toolchain Rust modern (Rust โ‰ฅ 1.91.1 diinstal melalui rustup dari rustup.rs).

Pilih kernel QRMI IBM di Jupyter, lalu restart dan jalankan sel-sel notebook secara berurutan. Output yang tersimpan berasal dari jalankan hardware kontributor; perintah instalasi tidak menyebutkan versi persis yang digunakan untuk jalankan tersebut.

Kredensial yang dibutuhkanโ€‹

Untuk eksekusi standalone, buat file .env di samping notebook ini dengan nilai-nilai berikut, mengganti placeholder kredensial. Jaga file ini tetap privat. Jika Anda memilih backend yang berbeda, perbarui baik namanya maupun prefiks variabel lingkungannya.

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

Untuk alokasi Slurm, gunakan pengaturan resource dan kredensial yang disediakan oleh klaster. Notebook mempertahankan nilai lingkungan yang sudah ada.

Pengaturanโ€‹

Impor dependensi dan muat konfigurasi resource.

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.

Contoh skala kecilโ€‹

Langkah 1โ€“3 memperkenalkan API QRMI dengan menggunakan circuit sederhana. Setiap langkah memetakan ke fase inti dari siklus hidup QRMI terhadap hardware IBM Quantum.

Payload untuk langkah-langkah awal ini adalah circuit Bell state kecil yang dipilih agar cepat dan murah untuk dijalankan.

Contoh-contoh ini menggunakan hardware karena mendemonstrasikan alokasi resource jarak jauh dan manajemen job. Simulator circuit lokal tidak memvalidasi layanan QRMI dan integrasi scheduler. Menjalankan notebook ini mengirimkan job IBM Quantum dan membutuhkan akses ke backend yang dikonfigurasi.

Langkah 1: Memetakan masalah klasik ke resource quantumโ€‹

Langkah pertama dalam alur kerja QRMI apa pun adalah membuat objek QuantumResource dan memverifikasi bahwa itu dapat diakses.

get_target() mengambil deskripsi hardware backend (jumlah qubit, basis gate, coupling map) dan mengemasnya sebagai objek Target Qiskit, yang digunakan transpiler pada Langkah 2.

Langkah 2: Mengoptimalkan masalah untuk eksekusi hardware quantumโ€‹

Sebelum pengiriman, gunakan Qiskit untuk mentranspilasi circuit ke instruction set architecture (ISA) backend, menggunakan objek Target yang diambil pada Langkah 1.

Contoh ini kemudian membangun Payload.QiskitPrimitive, yang membungkus string circuit OpenQASM 3 dan metadata job ke dalam skema primitive IBM.

Langkah 3: Mengeksekusi menggunakan primitive QRMIโ€‹

Dengan payload yang sudah dibangun, contoh ini mengirimkan job dan melakukan poll hingga selesai. task_start() langsung mengembalikan ID job; task_status() di-poll hingga statusnya bukan lagi Queued/Running. Hasil diambil sebagai string JSON mentah dan diparsing untuk mengekstrak sampel pengukuran.

Sel berikut menyatukan akuisisi, eksekusi, dan pembersihan sehingga kegagalan setelah akuisisi tetap melepaskan session milik 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.

Antarmuka Qiskit tingkat lebih tinggi: QRMIService dan SamplerV2โ€‹

Siklus hidup mentah di atas menyediakan kontrol eksplisit atas setiap panggilan. Untuk alur kerja Qiskit standar, QRMI menyediakan primitive SamplerV2 yang mengimplementasikan BaseSamplerV2.

Manajemen siklus hidup

SamplerV2 menangani serialisasi payload, pengiriman (task_start), polling, dan decoding hasil. Dalam pengaturan batch HPC (misalnya, dengan Slurm), alokasi dan pelepasan dikelola oleh scheduler dan plugin SPANK. Dalam session Python interaktif menggunakan objek API tingkat rendah secara langsung, acquire() dan release() dapat digunakan untuk mengelola session khusus secara eksplisit.

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

Konteks HPC: injeksi resource Slurmโ€‹

Dalam klaster HPC, pengguna meminta resource quantum menggunakan sintaks GRES Slurm bersama dengan opsi plugin SPANK QRMI. Plugin menangani injeksi kredensial dan resource secara otomatis:

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

Kode aplikasi menemukan resource yang dialokasikan padanya saat runtime โ€” tanpa nama backend yang di-hardcode:

# 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

Contoh hardware skala besar: SQD pada N2_2โ€‹

Di sini kita menyatukan semua komponen menjadi alur kerja kimia kuantum yang lengkap pada skala yang lebih besar, dijalankan pada hardware IBM Quantum nyata melalui QRMI.

SQD menggabungkan hal-hal berikut:

  1. Sampling quantum dari ansatz Local Unitary Cluster Jastrow (LUCJ) yang dibangun menggunakan ffsim dan diinisialisasi dari amplitudo CCSD
  2. Transpilasi yang sadar hardware yang cocok dengan topologi lattice heavy-hex melalui generate_lucj_pass_manager
  3. Eksekusi sampling pada hardware IBM Quantum yang dikelola melalui QRMIService dan SamplerV2 QRMI
  4. Pasca-pemrosesan klasik: pemulihan konfigurasi self-consistent dan diagonalisasi subspace iteratif menggunakan qiskit-addon-sqd

Kita menerapkan SQD pada N2_2 pada jarak ikatan 1,0 Aหš\AA dengan active space yang diturunkan dari basis set cc-pVDZ (26 orbital spasial, setara dengan 52 spin-orbital/qubit).

Energi referensi untuk active space N2_2 /cc-pVDZ (jarak ikatan 1,0 Aหš\AA):

  • Energi referensi (perhitungan SCI terpisah): โˆ’109,22802922 Ha
Akurasi hasil eksekusi yang tersimpan

Eksekusi SQD di bawah ini mendemonstrasikan eksekusi QRMI end-to-end yang berhasil pada hardware IBM Quantum. Dengan satu pengulangan LUCJ dan 100.000 shot, hasilnya berada sekitar 23,7 kcal/mol di atas energi referensi dan tidak mencapai akurasi kimia (โ‰ค 1 kcal/mol). Mengubah n_reps, jumlah shot, atau jumlah iterasi SQD mungkin dapat meningkatkan akurasi, tetapi membutuhkan pengujian lebih lanjut.

Dalam jalankan yang tersimpan, pass manager ffsim menghapus interaksi spin-berlawanan (24, 24) dan (20, 20) karena backend tidak dapat mengakomodasinya. Hasil yang dilaporkan menggunakan circuit yang telah disesuaikan ini.

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

Langkah selanjutnyaโ€‹

Rekomendasi

Jika Anda merasa karya ini menarik, Anda mungkin tertarik dengan materi berikut: