QRMIで量子ワークロードを実行する
使用量の目安: SQDセクションではIBM Quantum®ハードウェアで1分未満。この見積もりにはキュー時間や古典処理は含まれておらず、実行時間は変動する場合があります。
学習成果
-
HPCスケジューラーとIBM Quantumハードウェアの間のミドルウェアとしてQRMIが果たす役割
-
実際のIBM®バックエンドに対して、QRMIのコアライフサイクル(
acquire→task_start→task_status→task_result→release)を使用する方法 -
QRMIの上に構築された、より高レベルのQiskitの
SamplerV2およびQRMIServiceラッパーを使用する方法 -
HPCスケジューラー(Slurm)が環境変数を介して量子リソースを注入する方法、およびアプリケーションがそれらをどのように利用するか
-
QRMIを介してIBMハードウェアを使用し、Nに対して完全なSQD(Sample-based Quantum Diagonalization)化学ワークフローを実行する方法
前提条件
-
Pythonの仮想環境と量子化学に関する基本的な知識
背景
量子とHPCの統合における課題
高性能コンピューティング(HPC)ワークフローでは、多くの場合、古典コンピューティング・クラスターと量子処理ユニット(QPU)のシームレスな連携が必要です。量子ハードウェアのバックエンドやサービスによって、認証メカニズム、通信フォーマット、ジョブ・ライフサイクルAPIが異なります。IBM QuantumシステムをHPCワークロード・マネージャー(Slurmなど)に統合するには、リソース取得、ジョブ実行、セッション管理のためのクリーンで標準的なインターフェースが必要です。
QRMIとは
**Quantum Resource Management Interface(QRMI)**は、HPCスケジューラーや古典アプリケーションから量子ハードウェアへのアクセスを標準化する、Rustで書かれたミドルウェア・ライブラリです。単一の統合されたライフサイクル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)を提供しますが、ベンダーを変更するには、アプリケーション層で異なるコンパイル・パス、ベンダー固有のペイロード構築、結果のデコードも必要になります。
IBMネイティブのペイロード形式。 IBM Quantumバックエンドの場合、QRMIはQiskit Runtimeスキーマに準拠したOpenQASM 3 JSONペイロード(QiskitPrimitive)を使用します。
環境変数による設定。 認証情報とエンドポイントURLは、実行時に環境変数から読み込まれます。HPCクラスターでは、Slurm QRMI SPANKプラグインがジョブのディスパッチ時にこれらを自動的に設定します。ノートブックやインタラクティブ・セッションでは、.envファイルからこれらを読み込みます。アプリケーション・コードには、認証情報やエンドポイントURLがハードコードされることはありません。
GRESによるHPCスケジューラー統合。 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) | リソース・ロックを解放する(例えば、セッションを閉じる) |
このチュートリアルの内容
このチュートリアルは2つの部分で構成されています。
ステップ1〜3(小規模な例): IBM Quantumハードウェア上でのシンプルなベル状態回路のデモンストレーションを通じてQRMI APIを紹介し、低レベルのプリミティブの直接的な使用方法と、高レベルのQRMIServiceおよびSamplerV2の統合の両方を取り上げます。
大規模ハードウェアの例: 結合距離1.0 のN分子に対する完全なSQDワークフロー(cc-pVDZ基底の活性空間、26個の空間軌道/52量子ビット)を、QRMIを介してIBM Quantumハードウェア上で実行します。SQDは、ffsimで構築されたLUCJアンザッツの量子サンプリングと、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環境のセットアップ
PyPIにはLinux用のビルド済みバイナリ・ホイールが用意されているため、標準の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ツールチェーン(rustup.rsからrustup経由でインストールされたRust ≥ 1.91.1)があることを確認してください。
JupyterでQRMI IBMカーネルを選択し、再起動してからノートブックのセルを順番に実行してください。保存されている出力は、貢献者によるハードウェア実行の結果です。インストール・コマンドには、その実行で使用された正確なバージョンは指定されていません。
必要な認証情報
- IBM Quantum: IBM Quantum PlatformのIAM APIキーとサービスCRN
スタンドアロン実行の場合は、このノートブックと同じ場所に.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 APIを紹介します。各ステップは、IBM Quantumハードウェアに対するQRMIライフサイクルの中核的なフェーズに対応しています。
これらの最初のステップのペイロードは、高速かつ低コストで実行できるように選ばれた小さなベル状態回路です。
これらの例では、リモート・リソースの割り当てとジョブ管理を実演するためにハードウェアを使用します。ローカルの回路シミュレーターでは、QRMIサービスとスケジューラーの統合を検証できません。このノートブックを実行すると、IBM Quantumジョブが送信されるため、設定されたバックエンドへのアクセスが必要です。
ステップ1: 古典的な問題を量子リソースにマッピングする
QRMIワークフローの最初のステップは、QuantumResourceオブジェクトを作成し、それがアクセス可能であることを確認することです。
get_target()は、バックエンドのハードウェア記述(量子ビット数、基底ゲート、結合マップ)を取得し、それをQiskitのTargetオブジェクトとしてパッケージ化します。これはステップ2でトランスパイラが使用します。
ステップ2: 量子ハードウェアでの実行に向けて問題を最適化する
送信する前に、ステップ1で取得したTargetオブジェクトを使用して、Qiskitで回路をバックエンドの命令セット・アーキテクチャ(ISA)にトランスパイルします。
次に、この例ではPayload.QiskitPrimitiveを構築します。これは、OpenQASM 3回路の文字列とジョブのメタデータをIBMプリミティブ・スキーマにラップします。
ステップ3: QRMIプリミティブを使用して実行する
ペイロードが構築されたら、この例ではジョブを送信し、完了をポーリングします。task_start()はジョブIDを即座に返し、task_status()はステータスがQueued/Runningでなくなるまでポーリングされます。結果は生のJSON文字列として取得され、パースされて測定サンプルが抽出されます。
以下のセルでは、取得後に失敗が発生してもノートブックが所有するセッションが解放されるように、取得、実行、クリーンアップを1つにまとめています。
# ── 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はBaseSamplerV2を実装したSamplerV2プリミティブを提供します。
SamplerV2は、ペイロードのシリアル化、送信(task_start)、ポーリング、結果のデコードを処理します。HPCバッチ環境(例えばSlurm)では、割り当てと解放はスケジューラーとSPANKプラグインによって管理されます。低レベルのAPIオブジェクトを直接使用するインタラクティブな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
大規模なハードウェアの例: NへのSQDの適用
ここでは、すべてのコンポーネントを組み合わせて、より大規模な完全な量子化学ワークフローを構築し、QRMIを介して実際のIBM Quantumハードウェア上で実行します。
SQDは以下を組み合わせています:
ffsimを使用して構築され、CCSD振幅から初期化されたLocal Unitary Cluster Jastrow (LUCJ)アンザッツの量子サンプリングgenerate_lucj_pass_managerによるheavy-hex格子トポロジーに合わせたハードウェア対応のトランスパイルQRMIServiceとQRMIのSamplerV2を通じて管理されるIBM Quantumハードウェアでのサンプリング実行- 古典的な後処理:
qiskit-addon-sqdを使用した自己無撞着な配置回復と反復部分空間対角化
cc-pVDZ基底関数系から導出された活性空間(26個の空間軌道、52個のスピン軌道/量子ビットに相当)を用いて、結合距離1.0 のNにSQDを適用します。
N/cc-pVDZ活性空間の参照エネルギー(結合距離1.0 ):
- 参照エネルギー(別途SCI計算による): −109.22802922 Ha
以下のSQD実行は、IBM Quantumハードウェア上でのQRMIのエンドツーエンド実行が成功したことを示しています。LUCJの繰り返し回数を1回、ショット数を100,000として実行した結果は、参照エネルギーより約23.7 kcal/mol高くなっており、化学的精度(≤ 1 kcal/mol)には達していません。n_reps、ショット数、またはSQDの反復回数を変更することで精度が向上する可能性がありますが、さらなる検証が必要です。
保存された実行では、バックエンドが対応できなかったため、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)

次のステップ
この内容に興味があれば、以下の資料も参考になるでしょう。
-
サンプルベース量子対角化(SQD)チュートリアル — IBM Quantum Platform上での完全なSQD化学ワークフロー。より大きな分子や基底関数系を含む
-
Sample-based Krylov quantum diagonalization — フェルミオン格子モデルの時間発展回路を使用する関連手法
-
qiskit-addon-sqddocumentation — SQD後処理ライブラリの完全なAPIリファレンスと追加チュートリアル -
QRMI GitHubリポジトリ — ソースコード、追加のバックエンド例(CUDA-Q、C、Lua)
-
QRMI概要論文 — QRMIアーキテクチャとHPC統合の技術的な説明
-
IBM Quantum Compute Serviceセッションガイド — IBMバックエンド向けのQRMIの
acquire/releaseライフサイクルとセッションの関係