ข้ามไปยังเนื้อหาหลัก
โครงการนี้อยู่ในช่วงเบต้า พบข้อผิดพลาด? สร้าง GitHub Issue มีคำถามหรือไอเดีย? ถามใน Discussions — ยินดีรับฟังความคิดเห็น!
คุณภาพการแปลในหน้านี้เป็นอย่างไร?

รัน workload ควอนตัมด้วย QRMI

ประมาณการเวลาใช้งาน: น้อยกว่าหนึ่งนาทีบนฮาร์ดแวร์ IBM Quantum® สำหรับส่วน SQD การประมาณการนี้ไม่รวมเวลารอคิวและการประมวลผลแบบคลาสสิก เวลาที่ใช้จริงอาจแตกต่างกันไป

ผลลัพธ์การเรียนรู้​

  1. บทบาทของ QRMI ในฐานะ middleware ระหว่าง HPC scheduler และฮาร์ดแวร์ IBM Quantum

  2. วิธีใช้วงจรชีวิตหลักของ QRMI (acquire → task_start → task_status → task_result → release) กับ backend ของ IBM® จริง

  3. วิธีใช้ wrapper ระดับสูงของ Qiskit SamplerV2 และ QRMIService บน QRMI

  4. วิธีที่ HPC scheduler (Slurm) ฉีดทรัพยากรควอนตัมผ่าน environment variable และวิธีที่แอปพลิเคชันนำไปใช้

  5. วิธีรัน workflow เคมี SQD (Sample-based Quantum Diagonalization) แบบสมบูรณ์บน N2_2 โดยใช้ฮาร์ดแวร์ของ IBM ผ่าน QRMI

ข้อกำหนดเบื้องต้น​

ความเป็นมา​

ความท้าทายในการผสานรวมควอนตัมกับ HPC​

เวิร์กโฟลว์การประมวลผลสมรรถนะสูง (HPC) มักต้องการการประสานงานที่ราบรื่นระหว่างคลัสเตอร์คอมพิวเตอร์แบบคลาสสิกและหน่วยประมวลผลควอนตัม (QPU) แบ็กเอนด์และบริการฮาร์ดแวร์ควอนตัมที่แตกต่างกันมีกลไกการยืนยันตัวตน รูปแบบข้อมูล และ API วงจรชีวิตของงานที่แตกต่างกัน การผสานรวมระบบ IBM Quantum เข้ากับตัวจัดการงาน HPC (เช่น Slurm) ต้องการอินเทอร์เฟซที่สะอาดและเป็นมาตรฐานสำหรับการจัดหาทรัพยากร การดำเนินงาน และการจัดการเซสชัน

QRMI คืออะไร​

Quantum Resource Management Interface (QRMI) เป็นไลบรารีมิดเดิลแวร์ที่เขียนด้วย Rust ซึ่งกำหนดมาตรฐานการเข้าถึงฮาร์ดแวร์ควอนตัมจากตัวจัดตารางงาน HPC และแอปพลิเคชันแบบคลาสสิก โดยมี API วงจรชีวิตแบบรวมเป็นหนึ่งเดียวให้ใช้:

┌─────────────────────────────────────────────────────────────────┐
│ 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 เผยแพร่เป็นโปรเจกต์โอเพนซอร์สที่ github.com/qiskit-community/qrmi และมีคำอธิบายในเปเปอร์ภาพรวม arXiv:2506.10052

ตัวเลือกการออกแบบหลัก​

วงจรชีวิตของทรัพยากร ไม่ใช่การคอมไพล์วงจร QRMI จัดการวงจรชีวิต acquire/submit/poll/release และไม่มีอะไรมากไปกว่านั้น การคอมไพล์วงจร การปรับให้เหมาะสม และการทรานสไพล์ยังคงอยู่ในชั้นแอปพลิเคชัน (เช่น Qiskit) ซึ่งทำให้อินเทอร์เฟซนี้เรียบง่ายและประกอบเข้าด้วยกันได้

โมเดลความสามารถในการพกพาข้ามผู้ผลิต ในขณะที่ QRMI มีการเรียกจัดการงานทั่วไป (acquire, task_start, task_status, task_result, release) ข้ามแบ็กเอนด์ฮาร์ดแวร์ที่รองรับ การเปลี่ยนผู้ผลิตยังต้องการพาสการคอมไพล์ที่แตกต่างกัน การสร้าง payload เฉพาะผู้ผลิต และการถอดรหัสผลลัพธ์ในชั้นแอปพลิเคชัน

รูปแบบ payload ดั้งเดิมของ IBM สำหรับแบ็กเอนด์ IBM Quantum QRMI ใช้ payload แบบ OpenQASM 3 JSON (QiskitPrimitive) ที่สอดคล้องกับสคีมา Qiskit Runtime

การกำหนดค่าผ่านตัวแปรสภาพแวดล้อม ข้อมูลรับรองและ URL ปลายทางถูกอ่านจากตัวแปรสภาพแวดล้อมขณะรันไทม์ ในคลัสเตอร์ HPC ปลั๊กอิน Slurm QRMI SPANK จะตั้งค่าสิ่งเหล่านี้โดยอัตโนมัติเมื่อมีการส่งงาน ในโน้ตบุ๊กหรือเซสชันแบบโต้ตอบ คุณโหลดค่าเหล่านี้จากไฟล์ .env โค้ดแอปพลิเคชันไม่มีข้อมูลรับรองหรือ URL ปลายทางที่ฝังไว้ตายตัว

การผสานรวมตัวจัดตารางงาน HPC ผ่าน GRES เมื่องาน Slurm ร้องขอทรัพยากรควอนตัมโดยใช้อินเทอร์เฟซปลั๊กอิน QRMI SPANK (#SBATCH --gres=qpu:1 และ #SBATCH --qpu=ibm_kingston) ปลั๊กอินจะแทรก QRMI_JOB_QPU_RESOURCES และ QRMI_JOB_QPU_TYPES เข้าไปในสภาพแวดล้อมของงาน แอปพลิเคชันเรียก get_job_qpu_resources_and_types() เพื่อค้นหาว่าทรัพยากรใดถูกจัดสรร — ไม่จำเป็นต้องมีชื่อแบ็กเอนด์ที่ฝังไว้ตายตัว QRMIService ห่อรูปแบบนี้สำหรับผู้ใช้ Qiskit

การเรียก API หลัก​

การเรียกวัตถุประสงค์
qrmi.acquire()ขอสิทธิ์เข้าถึงทรัพยากร (เช่น เปิดเซสชันเฉพาะ) ส่งคืนโทเค็นล็อก
qrmi.target()ดึงความสามารถของแบ็กเอนด์ (คิวบิต เกต แผนที่การเชื่อมต่อ) เป็น JSON
qrmi.task_start(payload)ส่งงานควอนตัม ส่งคืน ID งาน
qrmi.task_status(job_id)ตรวจสอบสถานะงาน (Queued, Running, Completed, Failed)
qrmi.task_result(job_id)ดึงผลลัพธ์งานที่เสร็จสมบูรณ์เป็นสตริง JSON ดิบ
qrmi.task_stop(job_id)ยกเลิกหรือล้างงาน
qrmi.release(lock)ปลดล็อกทรัพยากร (เช่น ปิดเซสชัน)

เนื้อหาที่บทเรียนนี้ครอบคลุม​

บทเรียนนี้แบ่งออกเป็นสองส่วน:

ขั้นตอนที่ 1-3 (ตัวอย่างขนาดเล็ก): แนะนำ API ของ QRMI ด้วยการสาธิตวงจรสถานะ Bell แบบง่ายบนฮาร์ดแวร์ IBM Quantum ครอบคลุมการใช้งาน primitive ระดับต่ำโดยตรง รวมถึงการผสานรวม QRMIService และ SamplerV2 ระดับสูง

ตัวอย่างฮาร์ดแวร์ขนาดใหญ่: เวิร์กโฟลว์ SQD แบบสมบูรณ์สำหรับโมเลกุล N2_2 ที่ระยะพันธะ 1.0 A˚\AA (active space ของฐาน cc-pVDZ, 26 spatial orbitals / 52 คิวบิต) ดำเนินการบนฮาร์ดแวร์ IBM Quantum ผ่าน QRMI SQD ผสมผสานการสุ่มตัวอย่างควอนตัมของ ansatz แบบ LUCJ ที่สร้างด้วย ffsim และการกู้คืนคอนฟิกูเรชันแบบ self-consistent ด้วย 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")

ข้อกำหนด​

ก่อนเริ่มบทเรียนนี้ ตรวจสอบให้แน่ใจว่าคุณติดตั้งสิ่งต่อไปนี้แล้ว

การตั้งค่าสภาพแวดล้อม Python​

มี binary wheel ที่สร้างไว้ล่วงหน้าสำหรับ Linux บน PyPI ดังนั้นคำสั่ง pip install มาตรฐานทำงานได้โดยตรงบนระบบ 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"
แพลตฟอร์มที่ไม่มี wheel สร้างไว้ล่วงหน้า

หาก pip สร้าง QRMI จากซอร์สโค้ด ตรวจสอบให้แน่ใจว่าคุณมีชุดเครื่องมือ Rust รุ่นทันสมัย (Rust ≥ 1.91.1 ติดตั้งผ่าน rustup จาก rustup.rs)

เลือกเคอร์เนล QRMI IBM ใน Jupyter จากนั้นรีสตาร์ทและรันเซลล์โน้ตบุ๊กตามลำดับ ผลลัพธ์ที่บันทึกไว้มาจากการรันฮาร์ดแวร์ของผู้สนับสนุน คำสั่งติดตั้งไม่ได้ระบุเวอร์ชันที่แน่นอนที่ใช้ในการรันนั้น

ข้อมูลรับรองที่จำเป็น​

สำหรับการรันแบบสแตนด์อโลน ให้สร้างไฟล์ .env ไว้ข้างโน้ตบุ๊กนี้โดยใส่ค่าต่อไปนี้ แทนที่ตัวยึดตำแหน่งข้อมูลรับรอง เก็บไฟล์นี้เป็นส่วนตัว หากคุณเลือกแบ็กเอนด์อื่น ให้อัปเดตทั้งชื่อของมันและคำนำหน้าตัวแปรสภาพแวดล้อม

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

สำหรับการจัดสรร Slurm ให้ใช้การตั้งค่าทรัพยากรและข้อมูลรับรองที่คลัสเตอร์จัดเตรียมให้ โน้ตบุ๊กจะรักษาค่าตัวแปรสภาพแวดล้อมที่มีอยู่เดิมไว้

การตั้งค่า​

นำเข้าดีเพนเดนซีและโหลดการกำหนดค่าทรัพยากร

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.

ตัวอย่างขนาดเล็ก​

ขั้นตอนที่ 1-3 แนะนำ API ของ QRMI โดยใช้วงจรแบบง่าย แต่ละขั้นตอนสอดคล้องกับเฟสหลักของวงจรชีวิต QRMI ต่อฮาร์ดแวร์ IBM Quantum

payload สำหรับขั้นตอนเริ่มต้นเหล่านี้เป็นวงจรสถานะ Bell ขนาดเล็กที่เลือกให้รันได้เร็วและมีต้นทุนต่ำ

ตัวอย่างเหล่านี้ใช้ฮาร์ดแวร์เพราะแสดงให้เห็นถึงการจัดสรรทรัพยากรระยะไกลและการจัดการงาน ตัวจำลองวงจรแบบโลคัลไม่สามารถตรวจสอบบริการ QRMI และการผสานรวมตัวจัดตารางงานได้ การรันโน้ตบุ๊กนี้จะส่งงาน IBM Quantum และต้องมีสิทธิ์เข้าถึงแบ็กเอนด์ที่กำหนดค่าไว้

ขั้นตอนที่ 1: จับคู่ปัญหาแบบคลาสสิกกับทรัพยากรควอนตัม​

ขั้นตอนแรกในเวิร์กโฟลว์ QRMI ใด ๆ คือการสร้างออบเจกต์ QuantumResource และตรวจสอบว่าสามารถเข้าถึงได้

get_target() ดึงคำอธิบายฮาร์ดแวร์ของแบ็กเอนด์ (จำนวนคิวบิต เกตพื้นฐาน แผนที่การเชื่อมต่อ) และบรรจุเป็นออบเจกต์ Target ของ Qiskit ซึ่งทรานสไพเลอร์ใช้ในขั้นตอนที่ 2

ขั้นตอนที่ 2: ปรับปัญหาให้เหมาะกับการรันบนฮาร์ดแวร์ควอนตัม​

ก่อนส่งงาน ใช้ Qiskit ทรานสไพล์วงจรไปยังสถาปัตยกรรมชุดคำสั่ง (ISA) ของแบ็กเอนด์ โดยใช้ออบเจกต์ Target ที่ดึงมาในขั้นตอนที่ 1

จากนั้นตัวอย่างจะสร้าง Payload.QiskitPrimitive ซึ่งห่อสตริงวงจร OpenQASM 3 และเมทาดาทาของงานเข้าไปในสคีมา primitive ของ IBM

ขั้นตอนที่ 3: ดำเนินการโดยใช้ QRMI primitive​

เมื่อสร้าง payload แล้ว ตัวอย่างจะส่งงานและตรวจสอบสถานะจนกว่าจะเสร็จสมบูรณ์ task_start() ส่งคืน ID งานทันที task_status() จะถูกตรวจสอบซ้ำจนกว่าสถานะจะไม่ใช่ Queued/Running อีกต่อไป ผลลัพธ์ถูกดึงมาเป็นสตริง JSON ดิบและแยกวิเคราะห์เพื่อดึงตัวอย่างการวัด

เซลล์ต่อไปนี้จะรวมการจัดหา การดำเนินการ และการล้างข้อมูลไว้ด้วยกัน เพื่อให้ความล้มเหลวหลังการจัดหายังคงปลดปล่อยเซสชันที่โน้ตบุ๊กเป็นเจ้าของ

# ── 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 ระดับสูง: QRMIService และ SamplerV2​

การเรียกวงจรชีวิตระดับต่ำข้างต้นช่วยให้ควบคุมทุกการเรียกได้โดยตรง สำหรับเวิร์กโฟลว์ Qiskit มาตรฐาน QRMI มี primitive SamplerV2 ที่นำ BaseSamplerV2 ไปใช้งาน

การจัดการวงจรชีวิต

SamplerV2 จัดการการซีเรียลไลซ์ payload การส่งงาน (task_start) การตรวจสอบสถานะ และการถอดรหัสผลลัพธ์ ในการตั้งค่าแบบแบตช์ของ HPC (เช่น กับ Slurm) การจัดสรรและการปลดปล่อยได้รับการจัดการโดยตัวจัดตารางงานและปลั๊กอิน SPANK ในเซสชัน Python แบบโต้ตอบที่ใช้ออบเจกต์ API ระดับต่ำโดยตรง acquire() และ release() สามารถใช้เพื่อจัดการเซสชันเฉพาะได้อย่างชัดเจน

# 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: การแทรกทรัพยากร Slurm​

ในคลัสเตอร์ HPC ผู้ใช้ร้องขอทรัพยากรควอนตัมโดยใช้ไวยากรณ์ Slurm GRES ร่วมกับตัวเลือกปลั๊กอิน QRMI SPANK ปลั๊กอินจัดการการแทรกข้อมูลรับรองและทรัพยากรโดยอัตโนมัติ:

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

โค้ดแอปพลิเคชันค้นหาทรัพยากรที่จัดสรรให้ในขณะรันไทม์ — ไม่จำเป็นต้องมีชื่อแบ็กเอนด์ที่ฝังไว้ตายตัว:

# 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

ตัวอย่างฮาร์ดแวร์ขนาดใหญ่: SQD บน N2_2​

ที่นี่เรานำองค์ประกอบทั้งหมดมารวมกันเป็นเวิร์กโฟลว์เคมีควอนตัมแบบสมบูรณ์ในขนาดที่ใหญ่ขึ้น ดำเนินการบนฮาร์ดแวร์ IBM Quantum จริงผ่าน QRMI

SQD ผสมผสานสิ่งต่อไปนี้เข้าด้วยกัน:

  1. การสุ่มตัวอย่างควอนตัมของ ansatz แบบ Local Unitary Cluster Jastrow (LUCJ) ที่สร้างโดยใช้ ffsim และเริ่มต้นจากแอมพลิจูด CCSD
  2. การทรานสไพล์ที่คำนึงถึงฮาร์ดแวร์ให้ตรงกับโทโพโลยีแลตทิซ heavy-hex ผ่าน generate_lucj_pass_manager
  3. การดำเนินการสุ่มตัวอย่างบนฮาร์ดแวร์ IBM Quantum ที่จัดการผ่าน QRMIService และ QRMI SamplerV2
  4. การประมวลผลภายหลังแบบคลาสสิก: การกู้คืนคอนฟิกูเรชันแบบ self-consistent และการทำ diagonalization แบบ subspace ซ้ำโดยใช้ qiskit-addon-sqd

เราใช้ SQD กับ N2_2 ที่ระยะพันธะ 1.0 A˚\AA พร้อม active space ที่มาจากชุดฐาน cc-pVDZ (26 spatial orbitals ซึ่งสอดคล้องกับ 52 spin-orbitals/คิวบิต)

พลังงานอ้างอิงสำหรับ active space N2_2 /cc-pVDZ (ระยะพันธะ 1.0 A˚\AA):

  • พลังงานอ้างอิง (การคำนวณ SCI แยกต่างหาก): −109.22802922 Ha
ความแม่นยำของการรันที่บันทึกไว้

การรัน SQD ด้านล่างแสดงให้เห็นถึงการดำเนินการ QRMI แบบครบวงจรที่สำเร็จบนฮาร์ดแวร์ IBM Quantum ด้วยการทำซ้ำ LUCJ เพียงครั้งเดียวและ 100,000 shots ผลลัพธ์จบลงที่ประมาณ 23.7 kcal/mol เหนือพลังงานอ้างอิง และไม่บรรลุความแม่นยำทางเคมี (≤ 1 kcal/mol) การเปลี่ยน n_reps จำนวน shot หรือจำนวนรอบการวนซ้ำของ SQD อาจปรับปรุงความแม่นยำได้ แต่ต้องมีการทดสอบเพิ่มเติม

ในการรันที่บันทึกไว้ ตัวจัดการพาส ffsim ได้ลบปฏิสัมพันธ์แบบ opposite-spin (24, 24) และ (20, 20) ออก เนื่องจากแบ็กเอนด์ไม่สามารถรองรับได้ ผลลัพธ์ที่รายงานใช้วงจรที่ปรับแล้วนี้

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

ขั้นตอนถัดไป​

คำแนะนำ

หากคุณพบว่างานนี้น่าสนใจ คุณอาจสนใจเนื้อหาต่อไปนี้: