הרצת עומסי עבודה קוונטיים עם QRMI
הערכת שימוש: פחות מדקה אחת על חומרת IBM Quantum® עבור חלק ה-SQD. הערכה זו אינה כוללת זמן המתנה בתור ועיבוד קלאסי; זמן הריצה עשוי להשתנות.
תוצרי למידה
-
התפקיד ש-QRMI ממלא כתוכנת ביניים (middleware) בין מתזמני HPC לחומרת IBM Quantum
-
כיצד להשתמש במחזור החיים (lifecycle) המרכזי של QRMI (
acquire→task_start→task_status→task_result→release) מול backend אמיתי של IBM® -
כיצד להשתמש במעטפות (wrappers) ברמה גבוהה יותר של Qiskit,
SamplerV2ו-QRMIService, מעל QRMI -
כיצד מתזמני HPC (Slurm) מזריקים משאבים קוונטיים באמצעות משתני סביבה וכיצד יישומים צורכים אותם
-
כיצד להריץ זרימת עבודה כימית מלאה מסוג SQD (דיאגונליזציה קוונטית מבוססת-דגימה) על N תוך שימוש בחומרת IBM דרך QRMI
דרישות מוקדמות
-
היכרות בסיסית עם סביבות וירטואליות של Python וכימיה קוונטית
רקע
אתגר האינטגרציה בין קוונטי ל-HPC
תהליכי עבודה של מחשוב בעל ביצועים גבוהים (HPC) דורשים לעיתים קרובות תיאום חלק בין אשכולות מחשוב קלאסיים לבין יחידות עיבוד קוונטי (QPUs). Backend-ים קוונטיים ושירותים שונים חושפים מנגנוני אימות, פורמטים של העברת נתונים (wire formats) וממשקי API שונים למחזור חיי העבודה. שילוב מערכות IBM Quantum במנהלי עומסי עבודה של HPC (כגון Slurm) מחייב ממשק אחיד וסטנדרטי לרכישת משאבים, הרצת עבודות (jobs) וניהול sessions.
מה זה QRMI
Quantum Resource Management Interface (QRMI) היא ספריית middleware הכתובה ב-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 בלבד. קומפילציה, אופטימיזציה ו-transpilation של מעגלים נותרים בשכבת האפליקציה (למשל, Qiskit). כך הממשק נשאר מינימלי וניתן להרכבה.
מודל ניידות בין ספקים. בעוד ש-QRMI מספק קריאות ניהול עבודה משותפות (acquire, task_start, task_status, task_result, release) עבור כל ה-Backend-ים הנתמכים, מעבר בין ספקים דורש גם passes שונים של קומפילציה, בניית payload ספציפית לספק, ופענוח תוצאות בשכבת האפליקציה.
פורמט payload מקורי של IBM. עבור Backend-ים של IBM Quantum, QRMI משתמש ב-payloads מסוג OpenQASM 3 JSON (QiskitPrimitive) התואמים לסכימת Qiskit Runtime.
תצורה באמצעות משתני סביבה. פרטי הזדהות וכתובות URL של נקודות קצה נקראים ממשתני סביבה בזמן ריצה. באשכול HPC, plugin ה-SPANK של Slurm עבור QRMI מגדיר אותם אוטומטית כאשר עבודה נשלחת. ב-notebook או ב-session אינטראקטיבי, טוענים אותם מקובץ .env. קוד האפליקציה לעולם לא מכיל פרטי הזדהות או כתובות URL מקודדים בקוד.
שילוב מתזמן HPC באמצעות GRES. כאשר עבודת Slurm מבקשת משאבים קוונטיים באמצעות ממשק plugin ה-SPANK של QRMI (#SBATCH --gres=qpu:1 וגם #SBATCH --qpu=ibm_kingston), ה-plugin מזריק את QRMI_JOB_QPU_RESOURCES ואת QRMI_JOB_QPU_TYPES לסביבת העבודה. אפליקציות קוראות ל-get_job_qpu_resources_and_types() כדי לגלות אילו משאבים הוקצו — ללא צורך בשמות Backend מקודדים בקוד. QRMIService עוטף את התבנית הזו עבור משתמשי Qiskit.
קריאות ה-API המרכזיות
| קריאה | מטרה |
|---|---|
qrmi.acquire() | רכישת גישה למשאב (למשל, פתיחת session ייעודי); מחזירה lock token |
qrmi.target() | אחזור יכולות ה-Backend (qubits, שערים, מפת צימוד) כ-JSON |
qrmi.task_start(payload) | הגשת עבודה קוונטית; מחזירה מזהה עבודה (job ID) |
qrmi.task_status(job_id) | בדיקת סטטוס עבודה (Queued, Running, Completed, Failed) |
qrmi.task_result(job_id) | אחזור תוצאות עבודה שהושלמה כמחרוזת JSON גולמית |
qrmi.task_stop(job_id) | ביטול או ניקוי עבודה |
qrmi.release(lock) | שחרור נעילת המשאב (למשל, סגירת ה-session) |
מה מדריך זה מכסה
מדריך זה בנוי משני חלקים:
שלבים 1–3 (דוגמאות בקנה מידה קטן): מציגים את ה-API של QRMI באמצעות הדגמה פשוטה של מעגל במצב Bell על חומרת IBM Quantum, וכוללים שימוש ישיר ברמת primitive נמוכה וגם שילוב ברמה גבוהה עם QRMIService ו-SamplerV2.
דוגמת חומרה בקנה מידה גדול: תהליך עבודה מלא של SQD עבור המולקולה N במרחק קשר של 1.0 (מרחב פעיל של בסיס cc-pVDZ, 26 מסלולים מרחביים / 52 qubits), המבוצע על חומרת IBM Quantum דרך QRMI. SQD משלב דגימה קוונטית של ansatz מסוג LUCJ הבנוי באמצעות ffsim ושחזור תצורה עצמי-עקבי עם 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
קיימים wheels בינאריים מוכנים מראש עבור 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"
אם pip בונה את QRMI מהמקור, ודאו שיש לכם toolchain מודרני של Rust (Rust ≥ 1.91.1 מותקן באמצעות rustup מתוך rustup.rs).
בחרו את ה-kernel QRMI IBM ב-Jupyter, לאחר מכן הפעילו אותו מחדש והריצו את תאי ה-notebook לפי הסדר. הפלטים השמורים מקורם בהרצת החומרה של התורם/ת; פקודות ההתקנה אינן מציינות את הגרסאות המדויקות ששימשו באותה הרצה.
פרטי הזדהות נדרשים
- IBM Quantum: מפתח API של IAM ו-Service CRN מתוך IBM Quantum Platform
להרצה עצמאית, צרו קובץ .env לצד ה-notebook הזה עם הערכים הבאים, תוך החלפת placeholders פרטי ההזדהות. שמרו על קובץ זה פרטי. אם תבחרו Backend אחר, עדכנו גם את שמו וגם את קידומות משתני הסביבה.
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, השתמשו בהגדרות המשאבים ובפרטי ההזדהות שסופקו על-ידי האשכול. ה-notebook שומר על ערכי הסביבה הקיימים.
הגדרה
ייבאו את התלויות וטענו את תצורת המשאב.
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 ואת שילוב המתזמן. הרצת ה-notebook הזה שולחת עבודות IBM Quantum ומחייבת גישה ל-Backend המוגדר.
שלב 1: מיפוי הבעיה הקלאסית למשאב קוונטי
השלב הראשון בכל תהליך עבודה של QRMI הוא יצירת אובייקט QuantumResource ואימות שהוא נגיש.
get_target() מאחזרת את תיאור החומרה של ה-Backend (מספר qubits, שערים בסיסיים, מפת צימוד) ואורזת אותו כאובייקט Target של Qiskit, שבו משתמש ה-Transpiler בשלב 2.
שלב 2: אופטימיזציה של הבעיה להרצה על חומרה קוונטית
לפני ההגשה, השתמשו ב-Qiskit כדי לבצע transpile למעגל אל ארכיטקטורת סט ההוראות (ISA) של ה-Backend, באמצעות אובייקט ה-Target שאוחזר בשלב 1.
לאחר מכן הדוגמה בונה Payload.QiskitPrimitive, שעוטף את מחרוזת המעגל ב-OpenQASM 3 ואת המטא-דאטה של העבודה בתוך סכימת ה-primitive של IBM.
שלב 3: הרצה באמצעות primitives של QRMI
לאחר בניית ה-payload, הדוגמה מגישה את העבודה ובודקת (polling) עד להשלמתה. task_start() מחזירה מזהה עבודה (job ID) מיידית; task_status() נבדקת שוב ושוב עד שהסטטוס אינו עוד Queued/Running. התוצאות מאוחזרות כמחרוזת JSON גולמית ומנותחות כדי לחלץ דגימות מדידה.
התא הבא שומר על רכישה, הרצה וניקוי יחד, כך שכשלים לאחר הרכישה עדיין ישחררו session בבעלות ה-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.
ממשק Qiskit ברמה גבוהה יותר: QRMIService ו-SamplerV2
מחזור החיים הגולמי שלמעלה מספק שליטה מפורשת על כל קריאה. עבור תהליכי עבודה סטנדרטיים של Qiskit, QRMI מספק primitive מסוג SamplerV2 שמיישם את BaseSamplerV2.
SamplerV2 מטפל בסריאליזציה של ה-payload, בהגשה (task_start), בבדיקת סטטוס (polling) ובפענוח תוצאות. בסביבת batch של HPC (למשל, עם Slurm), ההקצאה והשחרור מנוהלים על-ידי המתזמן ו-plugin ה-SPANK. ב-session אינטראקטיבי של Python המשתמש באובייקטי API ישירים ברמה נמוכה, ניתן להשתמש ב-acquire() וב-release() כדי לנהל באופן מפורש sessions ייעודיים.
# 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, משתמשים מבקשים משאבים קוונטיים באמצעות תחביר GRES של Slurm יחד עם אפשרויות ה-plugin של SPANK עבור QRMI. ה-plugin מטפל בהזרקת פרטי הזדהות ומשאבים באופן אוטומטי:
#SBATCH --gres=qpu:1
#SBATCH --qpu=ibm_kingston
python my_workflow.py # QRMI_JOB_QPU_RESOURCES and QRMI_JOB_QPU_TYPES are already set
קוד האפליקציה מגלה את המשאבים שהוקצו לו בזמן ריצה — ללא שמות Backend מקודדים בקוד:
# 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 על N
כאן אנו מחברים את כל הרכיבים יחד לתהליך עבודה מלא של כימיה קוונטית בקנה מידה גדול יותר, המבוצע על חומרת IBM Quantum אמיתית דרך QRMI.
SQD משלב את הרכיבים הבאים:
- דגימה קוונטית של ansatz מסוג Local Unitary Cluster Jastrow (LUCJ), הבנוי באמצעות
ffsimומאותחל מאמפליטודות CCSD - transpilation מודע-חומרה, התואם לטופולוגיית הרשת heavy-hex, באמצעות
generate_lucj_pass_manager - הרצת דגימה על חומרת IBM Quantum המנוהלת דרך
QRMIServiceו-SamplerV2של QRMI - עיבוד קלאסי לאחר-ריצה: שחזור תצורה עצמי-עקבי ודיאגונליזציה איטרטיבית של תת-מרחב באמצעות
qiskit-addon-sqd
אנו מיישמים את SQD על N במרחק קשר של 1.0 עם מרחב פעיל הנגזר מסט הבסיס cc-pVDZ (26 מסלולים מרחביים, המתאימים ל-52 spin-orbitals/qubits).
אנרגיית ייחוס עבור מרחב פעיל N /cc-pVDZ (מרחק קשר 1.0 ):
- אנרגיית ייחוס (חישוב SCI נפרד): −109.22802922 Ha
הרצת ה-SQD שלהלן מדגימה הרצת QRMI מוצלחת מקצה לקצה על חומרת IBM Quantum. עם חזרה אחת של LUCJ ו-100,000 shots, התוצאה מסתיימת בכ-23.7 קק״ל/מול מעל אנרגיית הייחוס ואינה משיגה דיוק כימי (≤ 1 קק״ל/מול). שינוי n_reps, מספר ה-shots, או מספר האיטרציות של SQD עשוי לשפר את הדיוק, אך דורש בדיקות נוספות.
בהרצה השמורה, ה-pass manager של ffsim הסיר את האינטראקציות מנוגדות-הספין (24, 24) ו-(20, 20) מכיוון שה-Backend לא הצליח להכיל אותן. התוצאות המדווחות משתמשות במעגל המותאם הזה.
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)

צעדים הבאים
אם מצאתם עבודה זו מעניינת, ייתכן שתתעניינו בחומרים הבאים:
-
מדריך לדיאגונליזציה קוונטית מבוססת-דגימה — תהליך העבודה המלא של כימיה קוונטית מבוססת SQD ב-IBM Quantum Platform, כולל מולקולות וסטי בסיס גדולים יותר
-
דיאגונליזציה קוונטית של קריילוב מבוססת-דגימה — שיטה קשורה המשתמשת במעגלים של אבולוציית זמן עבור מודלים פרמיוניים על סריג
-
תיעוד
qiskit-addon-sqd— מדריך API מלא ומדריכים נוספים לספריית העיבוד שלאחר הדגימה של SQD -
מאגר GitHub של QRMI — קוד מקור, דוגמאות נוספות ל-Backend (CUDA-Q, C, Lua)
-
מאמר הסקירה של QRMI — תיאור טכני של ארכיטקטורת QRMI והשילוב עם HPC
-
מדריך ה-sessions של IBM Quantum Compute Service — כיצד sessions קשורים למחזור החיים של
acquire/releaseב-QRMI עבור Backend-ים של IBM