انتقل إلى المحتوى الرئيسي
هذا المشروع في مرحلة تجريبية. وجدت خطأ؟ افتح مشكلة على GitHub. لديك سؤال أو فكرة؟ اسأل في Discussions — نحب سماع ملاحظاتك!
كيف جودة الترجمة في هذه الصفحة؟

تشغيل أعباء العمل الكمومية باستخدام QRMI

تقدير الاستخدام: أقل من دقيقة واحدة على عتاد IBM Quantum® لقسم SQD. يستثني هذا التقدير وقت الانتظار في الطابور والمعالجة الكلاسيكية؛ وقد يختلف وقت التشغيل الفعلي.

نتائج التعلّم​

  1. الدور الذي تلعبه QRMI كوسيط برمجي (middleware) بين مجدولات HPC وعتاد IBM Quantum

  2. كيفية استخدام دورة حياة QRMI الأساسية (acquire → task_start → task_status → task_result → release) مقابل خلفية (backend) حقيقية من IBM®

  3. كيفية استخدام غلافي SamplerV2 وQRMIService عالي المستوى من Qiskit فوق QRMI

  4. كيف تحقن مجدولات HPC (مثل Slurm) الموارد الكمومية عبر متغيرات البيئة وكيف تستهلكها التطبيقات

  5. كيفية تشغيل سير عمل كيمياء SQD (التحليل القطري الكمي القائم على العينات - Sample-based Quantum Diagonalization) كامل على N2_2 باستخدام عتاد IBM عبر QRMI

المتطلبات الأساسية​

الخلفية​

تحدي التكامل بين الحوسبة الكمومية وHPC​

غالبًا ما تتطلب مهام سير عمل الحوسبة عالية الأداء (HPC) تنسيقًا سلسًا بين المجموعات الحاسوبية الكلاسيكية ووحدات المعالجة الكمية (QPUs). تُظهر خلفيات الأجهزة الكمية والخدمات المختلفة آليات مصادقة مختلفة، وتنسيقات بيانات مختلفة، وواجهات برمجة تطبيقات مختلفة لدورة حياة المهام. يتطلب دمج أنظمة IBM Quantum في مديري أحمال عمل HPC (مثل Slurm) واجهة نظيفة وموحدة للحصول على الموارد وتنفيذ المهام وإدارة الجلسات.

ما هو QRMI​

واجهة إدارة الموارد الكمية (QRMI) هي مكتبة وسيطة مكتوبة بلغة Rust توحّد الوصول إلى الأجهزة الكمية من مجدولات HPC والتطبيقات الكلاسيكية. وتوفّر واجهة برمجة تطبيقات واحدة موحّدة لدورة الحياة:

┌─────────────────────────────────────────────────────────────────┐
│ 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. يبقى تصريف الدارات (compilation) وتحسينها وتحويلها (transpilation) ضمن طبقة التطبيق (على سبيل المثال، Qiskit). وهذا يجعل الواجهة بسيطة وقابلة للتركيب.

نموذج قابلية النقل بين الموردين. بينما يوفّر QRMI استدعاءات موحّدة لإدارة المهام (acquire، task_start، task_status، task_result، release) عبر خلفيات الأجهزة المدعومة، فإن تغيير المورّد يتطلب أيضًا تمريرات تصريف مختلفة، وبناء حمولة خاص بكل مورّد، وفك تشفير النتائج ضمن طبقة التطبيق.

تنسيق الحمولة الأصلي لـ IBM. بالنسبة لخلفيات IBM Quantum، يستخدم QRMI حمولات 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.

استدعاءات واجهة برمجة التطبيقات الأساسية​

الاستدعاءالغرض
qrmi.acquire()الحصول على صلاحية الوصول إلى المورد (على سبيل المثال، فتح جلسة مخصصة)؛ يُعيد رمز قفل
qrmi.target()استرجاع قدرات الخلفية (الكيوبتات، البوابات، خريطة الاقتران) بصيغة JSON
qrmi.task_start(payload)إرسال مهمة كمية؛ يُعيد معرّف المهمة
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 (أمثلة صغيرة النطاق): تقديم واجهة برمجة تطبيقات QRMI من خلال عرض توضيحي بسيط لدارة حالة بيل (Bell state) على أجهزة IBM Quantum، يغطي الاستخدام المباشر منخفض المستوى للعناصر الأساسية (primitives) بالإضافة إلى التكامل عالي المستوى مع QRMIService و SamplerV2.

مثال على أجهزة واسعة النطاق: سير عمل SQD كامل لجزيء N2_2 عند مسافة رابطة قدرها 1.0 A˚\AA (فضاء نشط بمجموعة أساس cc-pVDZ، 26 مداراً فراغياً / 52 كيوبت)، يُنفَّذ على أجهزة IBM Quantum عبر QRMI. يجمع SQD بين أخذ عينات كمية لمُخمّن LUCJ ansatz المبني باستخدام 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​

تتوفر عجلات ثنائية (binary 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 من المصدر، فتأكد من أن لديك سلسلة أدوات Rust حديثة (Rust ≥ 1.91.1 مثبتة عبر rustup من rustup.rs).

اختر النواة QRMI IBM في Jupyter، ثم أعد تشغيلها ونفّذ خلايا دفتر الملاحظات بالترتيب. المخرجات المحفوظة مأخوذة من تشغيل المساهم على الأجهزة الفعلية؛ ولا تحدد أوامر التثبيت الإصدارات الدقيقة المستخدمة في ذلك التشغيل.

بيانات الاعتماد المطلوبة​

  • IBM Quantum: مفتاح واجهة برمجة تطبيقات IAM ورقم CRN للخدمة من IBM Quantum Platform

للتنفيذ المستقل، أنشئ ملف .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 واجهة برمجة تطبيقات QRMI باستخدام دارات بسيطة. تقابل كل خطوة مرحلة أساسية من دورة حياة QRMI مقابل أجهزة IBM Quantum.

الحمولة لهذه الخطوات الأولية هي دارة صغيرة لحالة بيل (Bell state) اختيرت لتكون سريعة ومنخفضة التكلفة في التشغيل.

تستخدم هذه الأمثلة الأجهزة الفعلية لأنها توضّح تخصيص الموارد عن بُعد وإدارة المهام. لا يتحقق محاكي الدارات المحلي من تكامل خدمة QRMI والمجدول. يؤدي تشغيل دفتر الملاحظات هذا إلى إرسال مهام IBM Quantum ويتطلب الوصول إلى الخلفية المُهيأة.

الخطوة 1: تعيين المسألة الكلاسيكية إلى مورد كمي​

الخطوة الأولى في أي سير عمل QRMI هي إنشاء كائن QuantumResource والتحقق من إمكانية الوصول إليه.

تسترجع get_target() وصف أجهزة الخلفية (عدد الكيوبتات، البوابات الأساسية، خريطة الاقتران) وتُغلّفها ككائن Qiskit Target، والذي يستخدمه المُحوِّل البرمجي في الخطوة 2.

الخطوة 2: تحسين المسألة للتنفيذ على الأجهزة الكمية​

قبل الإرسال، استخدم Qiskit لتصريف الدارة (transpile) إلى معمارية مجموعة التعليمات (ISA) الخاصة بالخلفية، باستخدام كائن Target الذي تم استرجاعه في الخطوة 1.

ثم يبني المثال Payload.QiskitPrimitive، الذي يُغلّف سلسلة دارة OpenQASM 3 وبيانات وصفية للمهمة ضمن مخطط IBM primitive.

الخطوة 3: التنفيذ باستخدام عناصر QRMI الأساسية​

بعد بناء الحمولة، يُرسل المثال المهمة ويستطلع حالتها حتى الاكتمال. تُعيد task_start() معرّف المهمة فورًا؛ ويُستطلع 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 عنصرًا أساسيًا SamplerV2 يُنفّذ BaseSamplerV2.

إدارة دورة الحياة

تتعامل SamplerV2 مع تسلسل الحمولة (serialization)، والإرسال (task_start)، والاستطلاع، وفك تشفير النتائج. في إعداد دفعي لـ HPC (على سبيل المثال، باستخدام Slurm)، يُدار التخصيص والتحرير بواسطة المجدول وإضافة SPANK. أما في جلسة Python تفاعلية تستخدم كائنات واجهة برمجة التطبيقات المباشرة منخفضة المستوى، فيمكن استخدام 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. أخذ عينات كمية لمُخمّن Local Unitary Cluster Jastrow (LUCJ) المبني باستخدام ffsim والمُهيأ من سعات CCSD
  2. تصريف واعٍ بالأجهزة (hardware-aware) يطابق طوبولوجيا الشبكة السداسية الثقيلة (heavy-hex) عبر generate_lucj_pass_manager
  3. تنفيذ أخذ العينات على أجهزة IBM Quantum المُدارة من خلال QRMIService و QRMI SamplerV2
  4. معالجة كلاسيكية لاحقة: استعادة التكوين ذاتية الاتساق والتحليل القطري التكراري للفضاء الفرعي باستخدام qiskit-addon-sqd

نطبق SQD على N2_2 عند مسافة رابطة قدرها 1.0 A˚\AA مع فضاء نشط مشتق من مجموعة الأساس cc-pVDZ (26 مداراً فراغياً، يقابل 52 مدار سبين/كيوبت).

طاقة مرجعية لفضاء N2_2 /cc-pVDZ النشط (مسافة الرابطة 1.0 A˚\AA):

  • الطاقة المرجعية (حساب SCI منفصل): −109.22802922 Ha
دقة التشغيل المحفوظ

يوضّح تشغيل SQD أدناه نجاح تنفيذ QRMI من طرف إلى طرف على أجهزة IBM Quantum. باستخدام تكرار واحد لـ LUCJ و100,000 طلقة (shots)، تنتهي النتيجة عند حوالي 23.7 كيلو كالوري/مول أعلى من الطاقة المرجعية ولا تحقق الدقة الكيميائية (≤ 1 كيلو كالوري/مول). قد يؤدي تغيير n_reps، أو عدد الطلقات، أو عدد تكرارات SQD إلى تحسين الدقة، لكن ذلك يتطلب المزيد من الاختبار.

في التشغيل المحفوظ، أزال مدير التمريرات (pass manager) الخاص بـ ffsim التفاعلات ذات السبين المتعاكس (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

الخطوات التالية​

توصيات

إذا وجدت هذا العمل مثيرًا للاهتمام، فقد تكون مهتمًا بالمواد التالية: