基底状態推定のためのSqDRIFTアルゴリズム
- Python
- C++
Pythonノートブックをダウンロードして、このチュートリアルをご自身で実行してください。ノートブックは、FCIDump入力がまだ存在しない場合にダウンロードします。
使用量の見積もり: Heron r3プロセッサで180秒(注: これはあくまで見積もりです。実際の実行時間は異なる場合があります。)
学習成果
-
トロッター化と比較して、より深さの小さい回路を作成する方法を学びます
-
qDRIFTとSQDを使用した基底状態推定のエンドツーエンドのワークフローを一通り確認します
-
qiskit-fermionsを他のQiskitアドオンと連携させて、このようなワークフローを実装する方法を学びます
前提条件
-
サンプルベース量子対角化 (SQD)の概要を読む
-
サンプルベースKrylov量子対角化 (SKQD)のレッスンを読む
背景
SqDRIFTは、ビット列をサンプリングするためのアンザッツを選ぶ必要性を、ターゲットのハミルトニアンから直接構築される時間発展回路のアンサンブルで置き換える、SKQDの変種です。これは、ハミルトニアンの係数に基づいて、より小さな時間発展演算子をハミルトニアンからサブサンプリングすることで実現され、この手法はqDRIFTトロッター化法として知られています。
どちらの実装も、Qiskit Fermionsを使用して分子ハミルトニアンを表現し、qDRIFTアルゴリズムの回路を構築します。Python実装はフェルミオン・トランスパイラーパスを使用しますが、C++実装は明示的にグループをサンプリングし、それらを量子ビット演算子にマッピングします。
ハミルトニアンを次の形式とします。
ここで、一般性を失うことなく、であり、の最大固有値の絶対値がに等しいことを要求します。符号付きまたは複素数の前因子はすべてに吸収されるため、係数は厳密に正の重みであり、は各項の方向を担います。ここではハミルトニアン中の項の数(またはグループ化後のグループの数)であり、これはハミルトニアンの性質であって、単一の回路にサンプリングされる演算子の数(以下でと表記)とは異なります。
qDRIFTアルゴリズムは、ターゲット時間に対して、以下のように定義される演算子を実現します。ここではの範囲を取り、番目のSqDRIFT回路を表します。
ここでは1回路あたりのサンプリングされた演算子の数であり、はアンサンブル内の回路の数です。積は、すべての個のハミルトニアン項ではなく、回の抽出にわたって取られ、項が復元抽出されるため、同じが単一の内に複数回現れることがあります。
次の量:
は係数のノルムであり、したがって個の各ステップは、どの項が抽出されたかにかかわらず、同じ時間だけ発展します。ステップ角の均一性はqDRIFTの特徴的な性質です。係数は、その項がどれだけ回転されるかではなく、その項がどれだけの頻度で抽出されるかを通じて結果に影響を与えます。インデックスは次の分布からサンプリングされます。
したがって、系列は、この分布から抽出された項インデックスのランダムな列です。は正であり、その和がであるため、これは正規化された確率分布であり、ランダムな抽出に対する結果チャンネルの期待値は、が大きくなるにつれて減少する誤差で、の下での発展を近似します。近似誤差は、項の数ではなくに依存することに注意してください。
(SqDRIFTの論文では、項の数を、系列の長さをと表記していますが、ここでは両者を明確に区別するためにとを使用します。)
このチュートリアルでは、そのようなランダム化された回路のアンサンブルを生成する方法を示します。これらの回路を作成した後、異なる演算子に対してKrylov部分空間を作成する場合と同様に、異なる時間パラメーターを持つそのような複数の演算子からビット列をサンプリングします。これにより、基底状態ベクトルとサンプリングされたビット列との間で、より高いオーバーラップが保証されます。
要件
このチュートリアルを開始する前に、以下がインストールされていることを確認してください
- Python (>=3.10) 仮想環境
- pip>=25.1
qiskit~= 2.5qiskit-fermions==0.1.0 (名前が複数形であることに注意してください)- numpy
- pyscf
qiskit-aerqiskit-ibm-runtimeqiskit-addon-sqd
必要なパッケージはすべて、次のコマンドでインストールできます。
pip install "qiskit~=2.5" "qiskit-fermions==0.1.0" qiskit-aer qiskit-ibm-runtime qiskit-addon-sqd pyscf numpy
セットアップ
# Third-party scientific computing
import numpy as np
# PySCF
from pyscf import tools, ao2mo, fci
# Qiskit core
from qiskit import transpile
from qiskit.primitives import BitArray
# Qiskit Aer
from qiskit_aer import AerSimulator
# IBM Quantum Compute Service
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
# Qiskit Fermions
from qiskit_fermions.operators.library import FCIDump
from qiskit_fermions.operators import FermionOperator
from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
from qiskit_fermions.operators.terms.grouping import (
group_terms_by_electronic_structure,
)
from qiskit_fermions.operators.terms.ordering import canonical_order
from qiskit_fermions.circuit import FermionicCircuit
from qiskit_fermions.circuit.library import Evolution
from qiskit_fermions.transpiler import FermionicPassManager
from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
from qiskit_fermions.transpiler.passes import QDriftTrotterization
from qiskit_fermions.circuit.library import InitializeModes
# Qiskit addon SQD
from qiskit_addon_sqd.fermion import (
diagonalize_fermionic_hamiltonian,
SCIResult,
)
実装
ステップ1: 古典的な入力を量子問題にマッピングする
以下のPythonのステップではAerシミュレーターを使用します。ハードウェアの例はその後に続きます。
FCIDumpの読み込みと準備
このチュートリアルでは、窒素(N2)の電子構造ハミルトニアンを読み込みます。フェルミオン演算子を作成する方法は他にもあります。ドキュメントについては、qiskit_fermions.operators.libraryを参照してください。
このFCIDumpについて。 N2_sto_3g ファイルは、実験的な平衡結合長である1.09 の原子間距離における最小STO-3G基底での窒素分子()を記述しています。そのヘッダーは NORB=10、NELEC=14、MS2=0 を宣言しています。すなわち10個の空間軌道(したがって20個のスピン軌道、Jordan-Wigner変換の下では20個の量子ビット)、スピン一重項の14個の電子、つまり7個の電子と7個の電子です。すべての軌道には対称性ラベル1が与えられており、つまり点群対称性は利用されていません。全空間STO-3Gダンプであるため、凍結される軌道はなく、相関空間は十分に小さいため、次のコードブロックで示すように比較用の厳密なFCI参照エネルギーを古典的に計算できます。
同等のファイルはPySCFで再生成できます。
from pyscf import gto, scf, tools
mol = gto.M(atom="N 0 0 0; N 0 0 1.09", basis="sto-3g", symmetry=False)
mf = scf.RHF(mol).run()
tools.fcidump.from_scf(mf, "N2_sto_3g")
積分は収束したSCF軌道に依存するため、再生成したファイルは軌道の位相や順序において配布されているものと異なる場合がありますが、全エネルギーには影響しません。
ファイルの取得。 FCIDumpはこのGitHubリポジトリにあります。以下のコードを実行すると、このチュートリアルの残りの部分が想定する場所にファイルを取得できます。
まず、pyscfが提供する cisolver を使用して参照エネルギーを取得します。これは、対象としている分子の真の基底状態エネルギーです。このために、まず軌道数と電子数をそれぞれ表す norb と nelec を宣言します。次に、1電子積分と2電子積分をそれぞれ表す h1e と h2e を宣言します。これらはすべて、後でSQDにも使用されます。
import os
from urllib.request import urlopen
# The FCIDump is stored with this tutorial in the Qiskit documentation repository.
FCIDUMP_URL = "https://raw.githubusercontent.com/Qiskit/documentation/main/docs/tutorials/assets/sqdrift/fcidump_files/N2_sto_3g"
FCIDUMP_PATH = "fcidump_files/N2_sto_3g"
if not os.path.exists(FCIDUMP_PATH):
os.makedirs(os.path.dirname(FCIDUMP_PATH), exist_ok=True)
with urlopen(FCIDUMP_URL) as response:
contents = response.read()
with open(FCIDUMP_PATH, "wb") as f:
f.write(contents)
print(f"Downloaded FCIDump to {FCIDUMP_PATH}")
else:
print(f"Using existing FCIDump at {FCIDUMP_PATH}")
name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
ハミルトニアンの読み込み
必要なデータが揃ったら、qiskit-fermions と互換性のある形式でFCIファイルからハミルトニアンを読み込みます
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
qiskit-fermions を用いたフェルミオンのワークフロー
まず、フェルミオン回路に特化したトランスパイラーパスとゲートを提供する qiskit-fermions を使用して、ハミルトニアンをフェルミオン回路モデルにマッピングします。これらは、このワークフローにおいてQiskitの従来のトランスパイラーパスより前に使用されます。
項のグループ化
結果の再現性を確保するため、まず canonical_order を使用して、構造のみに基づいて項を並べ替えます。したがって、canon リスト内の演算子の順序は固定されます。これにより、作成される演算子の再現性が保証されます。これは、後で使用する QDriftTrotterization パスが、qDRIFT演算子を作成するためにランダムなインデックスをサンプリングするためです。
このステップでは、同一の係数を持つ関連項をグループ化することで、電子構造ハミルトニアンに存在する多くの対称性を利用します。これにより、qDRIFTプロトコルがサンプリングする演算子の係数分布は変化しますが、その収束保証には影響しません。重要なのは、対称性によって関連付けられた項をグループ化すると、パウリ項の望ましい打ち消し合いが生じ、それらの作用の下で状態を時間発展させる際の回路の深さ全体が短くなることです。
qiskit-fermions は、このグループ化を行う group_terms_by_electronic_structure 関数を提供しています。
group_terms_by_electronic_structure は正規順序化された項を前提としていることに注意してください。
対角項のフィルタリング
回路の生成に使用するハミルトニアンから対角項を削除し、 個のqDRIFTサンプリングスロットが配置間で存在確率を移動させる項に費やされるようにします。このような項は、次のステップで Evolution ゲートが構築される前の、この時点でハミルトニアンから取り除いておくのが最適です。
問題となる項は、占有数基底において対角である項、すなわち数演算子 の積です。この記述に該当する項には3種類あります。
-
定数エネルギーオフセット、すなわちゼロ個の数演算子の積で、その時間発展は大域的な位相のみに寄与するもの。
-
個々の数演算子 で、その時間発展は単一量子ビットの 回転に帰着するもの。
-
のような高次の積。
これら自体は単独では、占有数配置間で存在確率を移動させることはなく、すでに存在する配置の位相にのみ作用します。しかし、これらは無害というわけではありません。これらの相対位相は、回路の後段で励起項によって生じる干渉に影響を与えるため、これらをフィルタリングすると実際に生成される時間発展が変化し、サンプリング分布が変化する可能性があります。これは、サンプリングされる分布をそのまま維持するステップではなく、励起項へのサンプリングを集中させるために回路生成ステップで行われる意図的な近似です。上記の対称性によるグループ化がqDRIFTの収束保証をそのまま維持するのとは異なり、このフィルタリングは時間発展させる演算子そのものを変化させます。したがって、回路はもはや完全なハミルトニアンの下での時間発展を近似するものではなくなり、qDRIFTの誤差限界は元の演算子ではなくフィルタリングされた演算子に対して適用されます。ここではこれが許容されるのは、回路が配置を提案するためのサンプリングヒューリスティックにすぎないためです。フィルタリングは回路の構築に使用するハミルトニアンにのみ適用され、後で行う古典的な対角化は対角項を含む完全なハミルトニアンを使用するため、エネルギー推定自体から項が失われることはありません。SQDの精度はこの古典的なステップに依存しており、配置がどのように提案されたかにかかわらず、サンプリングされた部分空間内で変分的であり続けます。
filter_diagonal_terms() 関数は、このような項を演算子からその場で(in place)削除します。この関数は、生成モードの多重集合が消滅モードの多重集合と一致するという正規順序化された構造からそれらを識別するため、すでに正規順序化されている演算子に対してのみ有効です。この前提は実行時にはチェックされません。
# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)
print(len(canon.groups))
5060
ハミルトニアンの項をグループ化したので、回路のアンサンブルを生成するために以下のパラメータを決定します。
- 生成する回路の数:
num_circuits - 励起グループの観点での各回路の長さ:
num_exc - 各発展時間を決める係数:
times
フェルミオン回路の作成
次に、各タイムステップに対してフェルミオン回路を作成します。各回路は、先ほど宣言した発展時間を持つ単一のEvolutionゲートで構成されます。発展演算子はハミルトニアンです。後で、これらの回路に対してトランスパイラーパスを実行し、qDRIFT回路を作成します。
アンザッツの準備
InitializeModes クラスを使用してHartree-Fock状態を準備します。窒素の場合、このプロセスは単純に、最初の num_elec_a 個の量子ビットにXゲートを適用し、次に num_elec_b 個の量子ビットに適用するというものです。窒素ではどちらも7に等しくなります。この状態は、窒素の7個の電子と7個の電子を表します。
# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate
init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))
for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)
ステップ2: 量子ハードウェア実行のための問題の最適化
回路が完成したので、まず qiskit-fermions で利用可能なパスを使用してフェルミオンレベルの最適化を行い、続いて選択したバックエンド用に回路をトランスパイルします。これはシミュレーター実験であるため、まずAerSimulator向けにこれを行います。
各グループの重みの計算
このステップでは、ハミルトニアン内の係数に比例した確率で項のqDRIFTサンプリングを確率的に行います。これはqDRIFTトランスパイラーパスが行ってくれます。これにより、ハミルトニアンが長距離結合や2次を超える項を含む場合でも、量子ビット接続性が限られているにもかかわらず、ハードウェア上でより効率的に実行できるより浅い回路を作成できます。 項のグループ化の後、演算子はその重みに基づいてサンプリングされます。各演算子 について、重み は以下のように定義されます。
ステップ1で項をグループ化したため、ここでの各はグループ全体です。はグループの項の平均絶対係数であり、グループ内の各項は、その係数を符号のみに縮約して時間発展されます。
フェルミオンおよびハードウェアネイティブな最適化
generate_preset_jw_pass_manager() 関数は、FermionicCircuit を受け取り、ハードウェア上で実行するためにトランスパイルできる最適化された最終回路を生成する MultiStagePassManager を返します。デフォルトの最適化ステージを、QDriftTrotterization パスを含む FermionicPassManager に置き換えます。
-
QDriftTrotterizationパスは、内部で重み計算とサンプリングを使用して、サンプリングに使用する回路を生成します -
RelabelModesパスは、フェルミオンモードを並べ替えて量子ビット間の接続性を最適化し、ゲートの深さを削減するために使用できる別の最適化パスです。詳細はAPIリファレンスを参照してください
MultiStagePassManager の残りのステージは自動的に実行され、フェルミオンから量子ビットへの完全なマッピングを処理します。
-
F2QLayout: プリセットのパスマネージャーは
TrivialF2QLayoutパスを適用し、 個のフェルミオンビットを 個の量子ビットに自明にマッピングします。 -
F2QSynth: フェルミオンベースの回路命令を量子ビットベースの命令にマッピングするトランスパイルパスです。
qdrift = QDriftTrotterization(num_exc, rng=19)
pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])
sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))
for circ in sqdrift_circuits:
circ.measure_all()
print(len(sqdrift_circuits))
400
フェルミオンレベルの最適化が完了したので、シミュレーターでの実行に向けて回路をトランスパイルできます。
simulator = AerSimulator()
shots = 100
transpiled_circuits = transpile(sqdrift_circuits, simulator)
ステップ3: Qiskitプリミティブを使用した実行
回路が用意できたので、AerSimulator上でQiskitプリミティブを使用してそれらを実行できます。異なる回路からのすべてのカウントを結合します。最後にSQDで後処理を行う前に、それらをブールベクトルに変換します。
print(
f"Executing {len(transpiled_circuits)} circuits with {shots} shots each..."
)
job = simulator.run(transpiled_circuits, shots=shots)
result = job.result()
all_counts = [result.get_counts(i) for i in range(len(transpiled_circuits))]
print(len(all_counts), "length before post processing")
Executing 400 circuits with 100 shots each...
400 length before post processing
ステップ4: 後処理を行い、望ましい古典的な形式で結果を返す
SQDへのビット列の使用
これで、選択されたビット列に対して対角化スキームを実行し、分子の基底状態エネルギーに対応する最小固有値を求めることができます。コールバック関数を作成し、初期占有数を宣言し、パラメータを設定してから、最終的に対角化スキームを実行します。コールバック関数は、各反復で現在の反復回数と現在の固有値推定値を出力するために使用されます。
最後に、基底状態の推定値を得るために、結果として得られたエネルギーに nuclear_repulsion_energy を加えます。
注: 部分空間の次元は、ノイズのないシミュレーターであっても反復ごとに固定されているわけではありません。各サブサンプルは異なる配置の集合を抽出し、復元ステップが反復間でプールを再形成するため、報告される次元はサブサンプルごとに異なります。ノイズのないサンプリングそれ自体は、選択される部分空間の次元を固定するものではありません。しかし、ハードウェアでの実行では、系統的により大きな部分空間が得られる傾向があります。これは、ノイズの多いショットが粒子数対称性を破り、配置の復元がそれらを追加の基底ベクトルに変換するためです。そのため、ハードウェアのセクションでは、ビット列を刈り込む(プルーニングする)ための別のステップも導入します。
combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count
bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")
# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5
initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)
computed_energy = result.energy + nuclear_repulsion_energy
print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")
energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")
40000
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)
Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64767025226178
Subspace dimension: 5538
Subsample 1
Energy: -107.64772799119115
Subspace dimension: 5670
Subsample 2
Energy: -107.64765512281548
Subspace dimension: 5767
Iteration 2
Subsample 0
Energy: -107.64795948524682
Subspace dimension: 6080
Subsample 1
Energy: -107.64806617355072
Subspace dimension: 6300
Subsample 2
Energy: -107.64802260640258
Subspace dimension: 6308
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999464 0.99999643 0.99584631 0.99332984 0.96684652 0.96686712
0.99301927 0.0373282 0.0373266 0.00944508]
Orbital occupancies (beta): [0.99999462 0.99999643 0.9958261 0.99332349 0.96684268 0.96686737
0.99302145 0.03733536 0.03733399 0.0094585 ]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6480661736 Ha
Error: 1.1811817564e-04 Ha
ハードウェアの例
この例では20量子ビット(10個の空間軌道)を使用します。この選択は、素早く実行できるべきチュートリアルのための便宜的なものであり、この手法における厳格な上限ではありません。
古典的なステップのコストは、量子ビット数によって直接決まるものではありません。SQDはサンプリングされた配置が張る部分空間に射影されたハミルトニアンを対角化するため、古典的なコストを左右するのは、その選択された部分空間の次元です。これはここでは samples_per_batch、num_batches、および回路が実際に生成する異なる配置の数によって決まり、それに加えて射影されたハミルトニアンを適用するために必要な疎な線形代数の計算も影響します。完全なCI空間は軌道と電子の数に応じて組み合わせ論的に増大しますが、選択される部分空間はそのうちの小さく調整可能な一部分であり、その大きさは直接制御できます。その結果、量子ビット数と古典的な難しさはある程度独立に変化させることができます。より広い軌道空間を適度な部分空間にサンプリングする方が、非常に大きな部分空間で対角化されるより小さな系よりも安価になる場合があります。
したがって実際には、実行可能な系のサイズは、望む精度に必要な部分空間の次元と、固有値ソルバーが利用できるメモリおよびコア数に依存します。一般に、より大きな軌道空間では化学的精度に到達するためにより大きな部分空間が必要となり、これが最終的に分散リソースを必要とする動機となります。このステップをスケールアウトする方法についてはqiskit-addon-sqd-hpcを参照してください。固定のカットオフを想定するのではなく、実際的なアプローチは、報告される部分空間の次元と反復にわたるエネルギーの収束を観察し、エネルギーの改善が止まるか利用可能なメモリを使い尽くすまで部分空間のサイズを増やすことです。
注: ハードウェアのノイズによるサンプリング誤差のため、ハードウェアでの実行で対角化のために作成される部分空間は、シミュレーターを使用した場合よりも大きくなります。これにより対角化したい部分空間の次元は大きくなりますが、SQDのノイズに対する頑健性により、このワークフローは依然として正確な答えを与えます。
偽のビット列の刈り込み
ここでは、追加のステップを実行するかどうかを選択できます。回路の実行からすべてのビット列が得られたら、SQDを実行する前に無効なビット列をフィルタリングして除去することも、刈り込みを行わずに進めることもできます。刈り込みを省略することは、一般にハードウェア実行では望ましいとされます。これは、対称性が破れたショットを配置の復元に利用できる状態のまま残しておくことで、それらを有効な配置に修復し、それらのショットを単に破棄するのではなく部分空間を広げることができるためです。
窒素は7個の電子と7個の電子しか持てないため、出力の前半と後半においてそれぞれ1の数が7個より多いか少ないビット列は破棄できます。ビット列が有効かどうかをチェックし、無効であれば破棄する関数を定義します。偽のビット列をフィルタリングして除去した後、残りは対角化スキームに送られます。以下の PRUNE フラグを使用して、この2つの動作を切り替えます。
刈り込みは、回路の数、発展時間の集合、対角項のフィルタリングと並んで、最終的な部分空間を形作るいくつかの選択肢のうちの一つに過ぎないことに留意してください。刈り込みを行った実行と行わなかった実行を比較することは、他のすべてが固定されている場合にのみ有意義です。C++タブでは、配置回復ではなく事後選択を行い、他のパラメータも異なることから、この点についてより詳しく議論されています。
name = "fcidump_files/N2_sto_3g"
fcidump = tools.fcidump.read(name)
# Extract metadata from the FCIDump header
norb = fcidump["NORB"] # number of spatial orbitals
nelec = fcidump["NELEC"] # total number of electrons
e_nuc = fcidump["ECORE"] # nuclear repulsion / core energy
ms2 = fcidump["MS2"] # 2S (spin)
num_elec_a = (nelec + ms2) // 2 # alpha electrons
num_elec_b = (nelec - ms2) // 2 # beta electrons
# Reconstruct full 4-index ERIs from the FCIDump (stored in 8-fold symmetry)
h1e = fcidump["H1"] # shape (norb, norb)
h2e = ao2mo.restore( # shape (norb, norb, norb, norb)
1, fcidump["H2"], norb
)
cisolver = fci.direct_spin1.FCI()
cisolver.max_cycle = 200
cisolver.conv_tol = 1e-12
e_fci, _ = cisolver.kernel(
h1e,
h2e,
norb,
(num_elec_a, num_elec_b),
ecore=e_nuc, # adds nuclear repulsion to the final energy
)
reference_energy = e_fci
print(f"Reference FCI Energy = {reference_energy:.10f} Ha")
nuclear_repulsion_energy = fcidump["ECORE"]
print(f"Nuclear Repulsion Energy = {nuclear_repulsion_energy:.10f} Ha")
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb
# Apply automatic grouping
canon = canonical_order(hamiltonian.normal_ordered().simplify(atol=1e-16))
exit_code = group_terms_by_electronic_structure(
canon, num_modes, two_body_physicist_order=False
)
filter_diagonal_terms(canon)
print(len(canon.groups))
# SqDRIFT parameters
times = [1.0, 10.0] # Total evolution times used for the subspace creation
num_exc = 10 # Number of excitation groups per circuit
num_circuits = 200 # Number of circuits to generate
init_circuits = []
hf_gate = InitializeModes.from_hartree_fock(norb, (num_elec_a, num_elec_b))
for time in times:
evo_gate = Evolution(num_modes, canon, time)
circ = FermionicCircuit(num_modes)
circ.append(hf_gate, circ.modes)
circ.append(evo_gate, circ.modes)
init_circuits.append(circ)
# Calculate weights for sampling (one per group)
qdrift = QDriftTrotterization(num_exc, rng=19)
pm = generate_preset_jw_pass_manager()
pm.optimization = FermionicPassManager([qdrift])
sqdrift_circuits = []
for circ in init_circuits:
sqdrift_circuits += (pm.run(circ) for _ in range(num_circuits))
for circ in sqdrift_circuits:
circ.measure_all()
print(len(sqdrift_circuits))
# This example assumes you have saved your IBM Quantum Platform account locally.
service = QiskitRuntimeService(channel="ibm_quantum_platform")
# Select backend (choose based on qubit requirements)
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=2 * norb,
)
print(f"Selected backend: {backend.name} ({backend.num_qubits} qubits)")
# Transpile for hardware
transpiled_circuits = transpile(
sqdrift_circuits,
backend=backend,
optimization_level=3,
seed_transpiler=42,
)
shots = 100
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_SQDRIFT"]
job = sampler.run(transpiled_circuits, shots=shots)
result = job.result()
# Extract counts from SamplerV2 results
all_counts = [pub_result.data.meas.get_counts() for pub_result in result]
# Set to True to filter out bitstrings that violate electron-number conservation
PRUNE = False
def is_valid_bitstring(
bitstring: str, norb: int, nelec: tuple[int, int]
) -> bool:
n_alpha, n_beta = nelec
return (
len(bitstring) == 2 * norb
and bitstring[norb:].count("1") == n_alpha
and bitstring[:norb].count("1") == n_beta
)
if PRUNE:
all_counts_filtered = []
for counts in all_counts:
filtered_count = {}
for key in counts:
if not is_valid_bitstring(key, norb, (num_elec_a, num_elec_b)):
continue
elif key not in filtered_count.keys():
filtered_count[key] = counts[key]
else:
filtered_count[key] += counts[key]
all_counts_filtered.append(filtered_count)
all_counts = all_counts_filtered
combined_counts = {}
for counts in all_counts:
for bitstring, count in counts.items():
combined_counts[bitstring] = combined_counts.get(bitstring, 0) + count
bit_array = BitArray.from_counts(combined_counts)
print(bit_array.num_shots)
print("Electron configuration:")
print(f" Total electrons: {nelec}")
print(f" Alpha electrons: {num_elec_a}")
print(f" Beta electrons: {num_elec_b}")
print(f" Number of orbitals: {norb}")
print(f" Number of spin orbitals (qubits): {2*norb}")
print(f"Integral shapes: h1e={h1e.shape}, h2e={h2e.shape}")
# SQD parameters
samples_per_batch = 300
num_batches = 3
max_iterations = 5
initial_occupancies = (
np.array([1] * num_elec_a + [0] * (norb - num_elec_a)), # alpha
np.array([1] * num_elec_b + [0] * (norb - num_elec_b)), # beta
)
result_history = []
def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy + nuclear_repulsion_energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
# Run SQD with configuration recovery
print("\nRunning SQD with configuration recovery...")
result = diagonalize_fermionic_hamiltonian(
h1e,
h2e,
bit_array,
samples_per_batch=samples_per_batch,
norb=norb,
nelec=(num_elec_a, num_elec_b),
num_batches=num_batches,
energy_tol=1e-3,
occupancies_tol=1e-3,
max_iterations=max_iterations,
initial_occupancies=initial_occupancies,
seed=42,
callback=callback,
)
computed_energy = result.energy + nuclear_repulsion_energy
print("FINAL SQD RESULTS")
print(f"Orbital occupancies (alpha): {result.orbital_occupancies[0]}")
print(f"Orbital occupancies (beta): {result.orbital_occupancies[1]}")
energy_error = abs(computed_energy - reference_energy)
print(f"Reference Energy: {reference_energy:.10f} Ha")
print(f"Computed Energy: {computed_energy:.10f} Ha")
print(f"Error: {energy_error:.10e} Ha")
Reference FCI Energy = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha
5060
400
Selected backend: ibm_aachen (156 qubits)
40000
Electron configuration:
Total electrons: 14
Alpha electrons: 7
Beta electrons: 7
Number of orbitals: 10
Number of spin orbitals (qubits): 20
Integral shapes: h1e=(10, 10), h2e=(10, 10, 10, 10)
Running SQD with configuration recovery...
Iteration 1
Subsample 0
Energy: -107.64593072647523
Subspace dimension: 7221
Subsample 1
Energy: -107.6458270048177
Subspace dimension: 7209
Subsample 2
Energy: -107.64007673117075
Subspace dimension: 7138
Iteration 2
Subsample 0
Energy: -107.64757372124944
Subspace dimension: 9009
Subsample 1
Energy: -107.64674060104392
Subspace dimension: 8245
Subsample 2
Energy: -107.64731360491942
Subspace dimension: 8178
Iteration 3
Subsample 0
Energy: -107.64765518770588
Subspace dimension: 8835
Subsample 1
Energy: -107.64767975712016
Subspace dimension: 8649
Subsample 2
Energy: -107.64761634415606
Subspace dimension: 8648
FINAL SQD RESULTS
Orbital occupancies (alpha): [0.99999504 0.9999964 0.99590318 0.9932359 0.96697158 0.96696295
0.99298797 0.03728154 0.03728186 0.00938359]
Orbital occupancies (beta): [0.9999946 0.99999641 0.99590413 0.99323077 0.96697361 0.96696174
0.99298424 0.03728121 0.03728169 0.00939159]
Reference Energy: -107.6481842917 Ha
Computed Energy: -107.6476797571 Ha
Error: 5.0453460619e-04 Ha
C++のソース、CMakeプロジェクト、およびビルド手順は、Qiskitドキュメントのリポジトリのdocs/tutorials/assets/sqdrift/cppディレクトリにあります。FCIDump入力は、隣接するfcidump_filesディレクトリにあります。このチュートリアルをご自身でビルドして実行するには、リポジトリをクローンするか、両方のディレクトリをダウンロードしてください。
使用量の見積もり: Heron r3プロセッサで180秒(注: これはあくまで見積もりです。実際の実行時間は異なる場合があります。)
学習成果 (C++)
-
トロッター化と比較して、より深さの小さい回路を作成する方法を学びます
-
qDRIFTとSQDを使用した基底状態推定のエンドツーエンドのワークフローを一通り確認します
-
qiskit-fermionsを他のQiskitアドオンと連携させて、このようなワークフローを実装する方法を学びます
前提条件 (C++)
-
サンプルベース量子対角化 (SQD)の概要を読む
-
サンプルベースKrylov量子対角化 (SKQD)のレッスンを読む
背景 (C++)
SqDRIFTは、ビット列をサンプリングするためのアンザッツを選ぶ必要性を、ターゲットのハミルトニアンから直接構築される時間発展回路のアンサンブルで置き換える、SKQDの変種です。これは、ハミルトニアンの係数に基づいて、より小さな時間発展演算子をハミルトニアンからサブサンプリングすることで実現され、この手法はqDRIFTトロッター化法として知られています。
どちらの実装も、Qiskit Fermionsを使用して分子ハミルトニアンを表現し、qDRIFTアルゴリズムの回路を構築します。Python実装はフェルミオン・トランスパイラーパスを使用しますが、C++実装は明示的にグループをサンプリングし、それらを量子ビット演算子にマッピングします。
ハミルトニアンを次の形式とします。
ここで、一般性を失うことなく、であり、の最大固有値の絶対値がに等しいことを要求します。符号付きまたは複素数の前因子はすべてに吸収されるため、係数は厳密に正の重みであり、は各項の方向を担います。ここではハミルトニアン中の項の数(またはグループ化後のグループの数)であり、これはハミルトニアンの性質であって、単一の回路にサンプリングされる演算子の数(以下でと表記)とは異なります。
qDRIFTアルゴリズムは、ターゲット時間に対して、以下のように定義される演算子を実現します。ここではの範囲を取り、番目のSqDRIFT回路を表します。
ここでは1回路あたりのサンプリングされた演算子の数であり、はアンサンブル内の回路の数です。積は、すべての個のハミルトニアン項ではなく、回の抽出にわたって取られ、項が復元抽出されるため、同じが単一の内に複数回現れることがあります。
次の量:
は係数のノルムであり、したがって個の各ステップは、どの項が抽出されたかにかかわらず、同じ時間だけ発展します。ステップ角の均一性はqDRIFTの特徴的な性質です。係数は、その項がどれだけ回転されるかではなく、その項がどれだけの頻度で抽出されるかを通じて結果に影響を与えます。インデックスは次の分布からサンプリングされます。
したがって、系列は、この分布から抽出された項インデックスのランダムな列です。は正であり、その和がであるため、これは正規化された確率分布であり、ランダムな抽出に対する結果チャンネルの期待値は、が大きくなるにつれて減少する誤差で、の下での発展を近似します。近似誤差は、項の数ではなくに依存することに注意してください。
(SqDRIFTの論文では、項の数を、系列の長さをと表記していますが、ここでは両者を明確に区別するためにとを使用します。)
このチュートリアルでは、そのようなランダム化された回路のアンサンブルを生成する方法を示します。これらの回路を作成した後、異なる演算子に対してKrylov部分空間を作成する場合と同様に、異なる時間パラメーターを持つそのような複数の演算子からビット列をサンプリングします。これにより、基底状態ベクトルとサンプリングされたビット列との間で、より高いオーバーラップが保証されます。
要件 (C++)
この実装はIBM Quantum®ハードウェア上で動作し、古典的な対角化には外部のSBD実行ファイルを使用します。Pythonシミュレーターの例は含まれていません。
C++のビルド手順に従って、コンパイラ、CMake、Rust、Boost、およびQiskitの依存関係をインストールします。Rustはrustupでインストールしてください。Qiskitの依存関係は独自のRustバージョンを固定しており、rustupがそれを自動的にダウンロードしますが、HomebrewやrinuxのパッケージマネージャーからインストールしたRustは古すぎる場合があります。最後のSBDステップでは、さらにMPI、OpenMP、BLAS/LAPACKが必要です。この実装は現在Windowsをサポートしていません。
リポジトリから完全なC++ソースとCMakeプロジェクトを使用してください。例をダウンロードする際は、隣接するfcidump_filesディレクトリも保持してください。ドキュメントのリポジトリのチェックアウトから、次のコマンドでビルドします。最初のビルドではすべての依存関係をダウンロードしてコンパイルするため、数分かかります。
cd docs/tutorials/assets/sqdrift/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ビルドにより、SqDRIFT実行ファイルはbuild/ではなく、docs/tutorials/assets/sqdrift/cpp(プロジェクトのルート)に書き出されます。../fcidump_files/N2_sto_3gを見つけられるように、プロジェクトのルートから./SqDRIFTを実行してください。このタブ内の他のすべてのコマンドも、特に指示がない限りプロジェクトのルートから実行します。./SqDRIFTが共有ライブラリの欠落エラーで失敗する場合は、ビルドガイドにあるプラットフォーム固有のライブラリパスの手順に従ってください。macOSでは通常、それらなしで実行ファイルが動作します。
セットアップ(C++)
以下のスニペットは、完全なSqDRIFT.cppプログラムからの抜粋です。そのファイルを単一のプログラムとしてビルドして実行してください。
SqDRIFTを実行する前に、IBM Quantum APIトークンがIBM Cloud Platform用に設定されていることを確認してください。
qiskit-ibm-runtimeがインストールされたPythonがある場合、正しい形式の設定ファイルを作成する最も簡単な方法は次のとおりです。
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum_platform", token="YOUR_IBM_QUANTUM_TOKEN_HERE")
これにより、正しい形式で~/.qiskit/qiskit-ibm.json(Linux/macOS)または%USERPROFILE%\.qiskit\qiskit-ibm.json(Windows)が書き込まれます。ファイルを手動で作成する場合は次のようにします。
{
"default-ibm-quantum-platform": {
"channel": "ibm_quantum_platform",
"token": "YOUR_IBM_QUANTUM_TOKEN_HERE"
}
}
APIキーはIBM Quantum Platformから取得してください。
注意: 設定ファイルが存在しない、または形式が正しくない場合、明確なエラーではなく、起動時に分かりにくいRustのunwrap()パニックが発生します。問題が発生した場合は、キー名(default-ibm-quantum-platform)とチャネルの値(ibm_quantum_platform)を再確認してください。
実装(C++)
ステップ1: 古典的な入力を量子問題にマッピングする (C++)
分子ハミルトニアンを読み込む
char filename[] = "../fcidump_files/N2_sto_3g";
QfFCIDump* fcidump = qf_fcidump_from_file(filename);
if (fcidump == nullptr) {
std::cerr << "Failed to load FCIDump!" << std::endl;
return 1;
}
uint32_t norb = qf_fcidump_norb(fcidump);
uint32_t nelec = qf_fcidump_nelec(fcidump);
uint32_t n_alpha = nelec / 2;
uint32_t n_beta = nelec - n_alpha;
uint32_t num_modes = 2 * norb;
uint32_t num_qubits = num_modes;
std::cout << "✓ Loaded N2 molecule (" << norb << " orbitals, "
<< num_qubits << " qubits, " << nelec << " electrons)" << std::endl;
Output:
✓ Loaded N2 molecule (10 orbitals, 20 qubits, 14 electrons)
FCIDumpファイルから分子のハミルトニアンを読み込みます。量子ビット数は軌道数の2倍(スピンアップとスピンダウン)に等しくなります。
ハミルトニアンを作成して正規順序化する
// Create and normal order Hamiltonian
QfFermionOperator* hamiltonian = qf_ferm_op_from_fcidump(fcidump);
QfFermionOperator* normal_ordered = qf_ferm_op_normal_ordered(hamiltonian, nullptr);
FCIDumpのデータをフェルミオン演算子に変換し、正規順序化を適用して演算子の構造を単純化します。
電子構造で項をグループ化する
// Group terms by electronic structure
QfExitCode exit_code = qf_ferm_op_group_terms_by_electronic_structure(
normal_ordered, num_modes, false
);
if (exit_code != QfExitCode_Success) {
std::cerr << "Failed to group terms!" << std::endl;
return 1;
}
uint32_t num_groups = qf_ferm_op_num_groups(normal_ordered);
std::cout << "Grouped into " << num_groups << " groups" << std::endl;
// Split into group operators
QfFermionOperator** group_ops = new QfFermionOperator*[num_groups];
qf_ferm_op_split_out_groups(normal_ordered, nullptr, 0, group_ops);
Output:
Grouped into 1902 groups
発展回路を構築する前に、関連するハミルトニアンの項を電子構造でグループ化します。
サンプリング重みを計算し、正規化して量子ビット演算子にマッピングする
ここでは順序が重要です。 正規化はすべての係数をその場で単位の大きさに再スケーリングするため、サンプリング重みを先に計算しておく必要があります。正規化を先に実行すると、すべてのがちょうど1.0と読み取られ、各グループの重みは項の単なる個数に帰着してしまいます。その場合、サンプリング分布は物理的な係数ではなくグループのサイズによって決まることになります。
各グループの重みは、その項の絶対係数の平均です。PythonタブのQDriftTrotterizationパスは、グループ化されたハミルトニアンに対して同じ重みを使用します。代わりに合計を使用すると、がグループのサイズに比例してしまい、その結果、発展時間が歪んでしまいます。
// Calculate sampling weights BEFORE normalization.
std::vector<double> weights(num_groups);
double total_weight = 0.0;
for (uint32_t i = 0; i < num_groups; i++) {
QkComplex64* coeffs;
uint64_t num_terms;
qf_ferm_op_get_coeffs(group_ops[i], &coeffs, &num_terms);
double abs_sum = 0.0;
for (uint64_t j = 0; j < num_terms; j++) {
abs_sum += std::sqrt(coeffs[j].re * coeffs[j].re +
coeffs[j].im * coeffs[j].im);
}
weights[i] = (num_terms > 0) ? (abs_sum / static_cast<double>(num_terms)) : 0.0;
total_weight += weights[i];
}
std::cout << "Total weight (λ): " << total_weight << std::endl;
// Normalize the evolution operators term-by-term, now that the sampling
// weights above have already captured the original magnitudes.
for (uint32_t i = 0; i < num_groups; i++) {
QkComplex64* coeffs;
uint64_t num_terms;
qf_ferm_op_get_coeffs(group_ops[i], &coeffs, &num_terms);
for (uint64_t j = 0; j < num_terms; j++) {
const double magnitude = std::sqrt(coeffs[j].re * coeffs[j].re +
coeffs[j].im * coeffs[j].im);
if (magnitude > 0.0) {
coeffs[j].re /= magnitude;
coeffs[j].im /= magnitude;
}
}
}
QkObs** qubit_ops = new QkObs*[num_groups];
for (uint32_t i = 0; i < num_groups; i++) {
QfExitCode jw_exit = qf_ferm_op_jordan_wigner(group_ops[i], num_qubits, &qubit_ops[i]);
if (jw_exit != QfExitCode_Success) {
std::cerr << "Failed to map group " << i << std::endl;
return 1;
}
}
std::cout << "Mapped all " << num_groups << " normalized groups to qubit operators" << std::endl;
Output:
Total weight (λ): 337.261
Mapped all 1902 normalized groups to qubit operators
重みは、元の係数から読み取った、グループの項にわたる平均絶対係数です。その後、正規化によって、Jordan-Wigner変換の前に各フェルミオン項が単位の大きさに再スケーリングされ、発展した演算子には位相と符号のみが保持されます。これはqDRIFTの正規化された項の発展と一致しています。係数の大きさはを通じてサンプリング確率を決定し、回路の発展では正規化された項を使用するため、大きな係数が二重に数えられることはありません。
演算子セットをサンプリングする
// SqDRIFT Sampling: Create operator sets
const int num_circuits = 100; // Number of circuits to create
const int ops_per_circuit = 10; // Operators per circuit
const double time_step = 1; // Time step for evolution
std::cout << "\n Generating " << num_circuits << " circuits with "
<< ops_per_circuit << " operators each..." << std::endl;
std::mt19937 gen(42);
std::discrete_distribution<> dist(weights.begin(), weights.end());
// Store sampled operator indices
std::vector<std::vector<int>> operator_sets(num_circuits);
for (int i = 0; i < num_circuits; i++) {
operator_sets[i].resize(ops_per_circuit);
for (int j = 0; j < ops_per_circuit; j++) {
operator_sets[i][j] = dist(gen);
}
}
Output:
Generating 100 circuits with 10 operators each...
重み付き分布からサンプリングした10個のグループをそれぞれ含む、100個のランダムな回路の演算子セットを抽出します。次のステップでは、これらのセットをqDRIFT回路のアンサンブルに変換します。
ステップ2: qDRIFT回路を構築する(C++)
C++版では、このステップでサンプリングされた演算子セットから回路を構築します。ターゲットのバックエンド向けのトランスパイルは、各回路を送信する直前のステップ3で行われます。
// Create Suzuki-Trotter circuits by composing all operators
std::vector<Qiskit::circuit::QuantumCircuit> circuits;
circuits.reserve(num_circuits);
const uint32_t trotter_order = 1;
const uint32_t trotter_reps = 1;
const bool preserve_order = false;
const bool insert_barriers = false;
// Gate name mapping is static — compute once before all loops
auto name_map = Qiskit::circuit::get_standard_gate_name_mapping();
for (int circuit_idx = 0; circuit_idx < num_circuits; circuit_idx++) {
// Create initial circuit with state preparation using C++ API
Qiskit::circuit::QuantumCircuit qc(num_qubits, num_qubits);
// State preparation: apply X gates to spin-up qubits 0..n_alpha-1
// and spin-down qubits norb..norb+n_beta-1 (derived from FCIDump NELEC)
for (uint32_t q = 0; q < n_alpha && q < num_qubits; q++) {
qc.x(q);
}
for (uint32_t q = norb; q < norb + n_beta && q < num_qubits; q++) {
qc.x(q);
}
// For each operator in this circuit's set
for (int op_idx = 0; op_idx < ops_per_circuit; op_idx++) {
int group_idx = operator_sets[circuit_idx][op_idx];
double evolution_time = (total_weight * time_step) / ops_per_circuit;
// Create Suzuki-Trotter evolution circuit
QkCircuit* evolution_circuit = qk_circuit_library_suzuki_trotter(
qubit_ops[group_idx], trotter_order, trotter_reps,
evolution_time, preserve_order, insert_barriers
);
if (evolution_circuit != nullptr) {
// Get number of instructions
uint32_t num_ops = qk_circuit_num_instructions(evolution_circuit);
// Manually append each instruction
for (uint32_t i = 0; i < num_ops; i++) {
QkCircuitInstruction inst;
qk_circuit_get_instruction(evolution_circuit, i, &inst);
// Prepare qubit vector
std::vector<uint32_t> qubits(inst.num_qubits);
for (uint32_t j = 0; j < inst.num_qubits; j++) {
qubits[j] = inst.qubits[j];
}
// Get operation kind
QkOperationKind kind = qk_circuit_instruction_kind(evolution_circuit, i);
// Get mutable pointer to circuit
QkCircuit* mutable_circuit = qc.get_rust_circuit().get();
// Append based on operation type
if (kind == QkOperationKind_Gate) {
// Convert gate name string to QkGate enum
std::string gate_name(inst.name);
auto gate_it = name_map.find(gate_name);
if (gate_it != name_map.end()) {
QkGate gate_enum = gate_it->second.gate_map();
qk_circuit_parameterized_gate(
mutable_circuit,
gate_enum, // Use QkGate enum, not string
qubits.data(),
inst.params
);
} else {
std::cerr << "Unknown gate in evolution circuit: " << gate_name << std::endl;
qk_circuit_instruction_clear(&inst);
qk_circuit_free(evolution_circuit);
return 1;
}
} else if (kind == QkOperationKind_Barrier) {
qk_circuit_barrier(
mutable_circuit,
qubits.data(),
inst.num_qubits
);
} else if (kind == QkOperationKind_Reset) {
qk_circuit_reset(
mutable_circuit,
qubits[0]
);
}
qk_circuit_instruction_clear(&inst);
}
qk_circuit_free(evolution_circuit);
}
}
circuits.push_back(qc);
// Display progress
if ((circuit_idx + 1) % 10 == 0) {
std::cout << " Created " << (circuit_idx + 1) << "/"
<< num_circuits << " circuits" << std::endl;
}
}
std::cout << "✓ Created all " << num_circuits << " Suzuki-Trotter circuits" << std::endl;
Output:
Created 10/100 circuits
Created 20/100 circuits
Created 30/100 circuits
Created 40/100 circuits
Created 50/100 circuits
Created 60/100 circuits
Created 70/100 circuits
Created 80/100 circuits
Created 90/100 circuits
Created 100/100 circuits
✓ Created all 100 Suzuki-Trotter circuits
完全なプログラムはその後、回路の統計情報と回路0の演算子インデックスを出力しますが、ここでは省略しています。
サンプリングされた各演算子に対して、時間のSuzuki-Trotter発展回路を作成します。ここで、は回路ごとにサンプリングされる演算子の数、は時間ステップです。サンプリングされた各グループはマッピングの前に項ごとに正規化されているため、この発展では回転中に各項の位相と符号のみが適用されます。大きさはサンプリング分布を通じてのみ寄与します。Hartree-Fock初期状態は、量子ビット0〜6および10〜16にXゲートを適用して準備されます(各スピンセクターに7個の電子)。各発展回路の命令は、C++ APIを使用してメイン回路に手動で追加されます。
ステップ3: Qiskitプリミティブを使用した実行 (C++)
std::cout << "\n Connecting to IBM Quantum Cloud..." << std::endl;
// Initialize IBM Quantum Compute Service using qiskit-cpp
Qiskit::service::QiskitRuntimeService service;
std::cout << "✓ Connected to IBM Quantum" << std::endl;
// Get backend
const std::string backend_name = "ibm_fez";
auto backend = service.backend(backend_name);
if (backend.name().empty()) {
std::cerr << "Backend " << backend_name
<< " is not available to this account." << std::endl;
return 1;
}
std::cout << "✓ Selected backend: " << backend.name() << std::endl;
// Create sampler primitive
const int32_t shots = 100; // Number of shots per circuit
Qiskit::primitives::BackendSamplerV2 sampler(backend, shots);
std::cout << "\n Submitting " << num_circuits << " circuits ("
<< shots << " shots each)..." << std::endl;
// Submit one job per circuit.
std::vector<std::shared_ptr<Qiskit::primitives::BasePrimitiveJob>> jobs;
for (int i = 0; i < num_circuits; i++) {
for (uint32_t q = 0; q < num_qubits; q++) {
circuits[i].measure(q, q);
}
Qiskit::circuit::QuantumCircuit transpiled_qc =
Qiskit::compiler::transpile(circuits[i], backend, 2, 1.0, 42);
std::vector<Qiskit::primitives::SamplerPub> pubs;
pubs.push_back(Qiskit::primitives::SamplerPub(transpiled_qc, shots));
jobs.push_back(sampler.run(pubs));
}
std::cout << "\n⏳ Waiting for " << num_circuits << " jobs to complete..." << std::endl;
// Collect results — poll each job and extract bitstrings
std::vector<boost::dynamic_bitset<>> all_bitstrings;
for (int i = 0; i < num_circuits; i++) {
while (!jobs[i]->in_final_state()) {
sleep(5);
}
if (jobs[i]->done()) {
auto result = jobs[i]->result();
if (result.size() > 0) {
auto& pub_result = result[0];
auto& bit_array = pub_result.data();
for (size_t s = 0; s < bit_array.num_shots(); s++) {
auto sample = bit_array[s].to_string();
boost::dynamic_bitset<> bs(num_qubits);
for (uint32_t bit = 0; bit < num_qubits; bit++) {
bs[bit] = (sample[num_qubits - 1 - bit] == '1');
}
all_bitstrings.push_back(bs);
}
}
} else {
std::cout << " Circuit " << (i+1) << " did not complete successfully" << std::endl;
}
}
std::cout << "\n✓ Collected " << all_bitstrings.size()
<< " total bitstrings from all circuits" << std::endl;
IBM Quantumに接続し、名前でバックエンドを選択し、そのバックエンド向けに各回路をトランスパイルして、回路ごとに1つのジョブを送信します。具体的には、20量子ビットのレジスタ上で各100ショットの100個の個別のSampler jobです。その後、各ジョブが完了するまでポーリングし、result[0]からビット列を収集します。すべてのジョブがQPU時間を消費し、個別にキューで待機するため、このステップは単一のバッチジョブよりもはるかに時間がかかることを想定してください。
このページで引用しているハードウェアの結果は、backend_nameをデフォルトのibm_fezから変更して、ibm_pittsburghで実行したもの(100回路 × 100ショット)です。保存したサンプルは、ステップ4に示す修正済みの行列式ビット順序で後処理されました。ハードウェアノイズは実行ごとに異なるため、カウントと最終的なエネルギーはお使いの環境では異なります。
Output:
Connecting to IBM Quantum Cloud...
✓ Connected to IBM Quantum
✓ Selected backend: ibm_pittsburgh
Submitting 100 circuits (100 shots each)...
⏳ Waiting for 100 jobs to complete...
✓ Collected 10000 total bitstrings from all circuits
バックエンドの変更: 別のデバイスを対象にするには、SqDRIFT.cppのbackend_nameを変更してください。service.backends()で、お使いのアカウントが利用できるものを一覧表示できます。
ステップ4: 後処理を行い、望ましい古典的な形式で結果を返す (C++)
ビット列を事後選択してCI文字列を書き込む
// Postselect bitstrings by Hamming weight (derived from FCIDump NELEC)
std::vector<double> bitstring_weights(all_bitstrings.size(), 1.0);
auto [filtered_bitstrings, filtered_weights] = Qiskit::addon::sqd::postselect_bitstrings(
all_bitstrings,
bitstring_weights,
Qiskit::addon::sqd::MatchesRightLeftHamming<uint32_t>(n_alpha, n_beta)
);
std::ignore = filtered_weights;
std::cout << "✓ Postselected " << filtered_bitstrings.size()
<< " bitstrings with Hamming weight ("
<< n_alpha << "," << n_beta << ")" << std::endl;
// Convert bitstrings to CI strings using SQD addon
auto ci_strings = Qiskit::addon::sqd::bitstrings_to_ci_strings_symmetrize_spin(
filtered_bitstrings,
std::nullopt // No dimension limit
);
std::cout << "✓ Generated " << ci_strings.size() << " CI strings" << std::endl;
// Write CI strings to file for SBD
std::ofstream alpha_file("alphadets_from_sqd.txt");
for (const auto& ci_string : ci_strings) {
std::string bitstr;
// SBD expects the highest orbital index first (bit zero on the right).
boost::to_string(ci_string, bitstr);
alpha_file << bitstr << "\n";
}
alpha_file.close();
std::cout << "✓ Wrote " << ci_strings.size()
<< " CI strings to alphadets_from_sqd.txt" << std::endl;
std::cout << " Ready for SBD diagonalization!" << std::endl;
Output:
✓ Postselected 1137 bitstrings with Hamming weight (7,7)
✓ Generated 49 CI strings
✓ Wrote 49 CI strings to alphadets_from_sqd.txt
Ready for SBD diagonalization!
正しいハミング重み(FCIDumpのNELECフィールドから導出されたn_alpha個のスピンアップ電子とn_beta個のスピンダウン電子)を持つビット列を事後選択し、スピン対称化を使用してそれらをConfiguration Interaction (CI)文字列に変換し、後続のSelected Basis Diagonalizationのためにalphadets_from_sqd.txtに書き込みます。
リソースをクリーンアップする
// Cleanup
// circuits vector will be automatically cleaned up (RAII)
// Free qubit operators
for (uint32_t i = 0; i < num_groups; i++) {
qk_obs_free(qubit_ops[i]);
}
delete[] qubit_ops;
// Free fermionic operators
for (uint32_t i = 0; i < num_groups; i++) {
qf_ferm_op_free(group_ops[i]);
}
delete[] group_ops;
qf_ferm_op_free(normal_ordered);
qf_ferm_op_free(hamiltonian);
qf_fcidump_free(fcidump);
return 0;
割り当てられたすべてのメモリとリソースを適切に解放します。C++のstd::vector<QuantumCircuit>はRAIIによって自動的にクリーンアップされます。量子ビット演算子、フェルミオン演算子、およびFCIDumpデータは手動で解放します。
ハミルトニアンを射影する基底が作成できたので、対角化の処理に進んで基底状態の推定値を得ることができます。プロジェクトのルートであるdocs/tutorials/assets/sqdrift/cppから、次のコマンドを実行してください。
SBDのdiagバイナリをビルドする
CMakeはSBDをソースのみとしてdeps/sbd/に取得します(メインのビルドでは意図的にそのコンパイルをスキップします)。SBDアプリは通常のMakefileを使用し、SBD自体がMPI、OpenMP、BLAS/LAPACKを必要とします—そのため、それらがインストールされ、アプリのConfigurationファイルがお使いのツールチェーンに一致するまでは、単なるmakeでは不十分です。
Makefileは、同じディレクトリにあるConfigurationファイルからコンパイラとリンカのフラグを読み取ります。上流で提供されているバージョンは、特定のmacOS/Homebrewのレイアウト向けに調整されており、標準的なマシンではそのままではビルドできません—特に、-I/opt/homebrew/opt/llvm/includeにはomp.hが含まれておらず(それはバージョン固有のclangリソースディレクトリにあります)、Appleのclang++は-fopenmpを完全に拒否します。
clang++: error: unsupported option '-fopenmp'
以下の手順は、macOS(Apple Silicon、Darwin 25.6、Open MPI 5.x + Homebrew LLVM)で動作確認済みです。
macOS(Apple Silicon) — テスト済み
# 1. Install MPI and an OpenMP-capable compiler.
# Apple clang cannot do OpenMP, so Homebrew's LLVM provides both
# clang++ and libomp. BLAS/LAPACK come from Apple's Accelerate
# framework, which is already part of macOS — no openblas needed.
brew install open-mpi llvm
次に、deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization/Configurationを次の内容に置き換えます。
# Path to the SBD library
SBD_PATH=../..
# MPI C++ compiler. OMPI_CXX points Open MPI's wrapper at Homebrew's clang++,
# which (unlike Apple clang) supports -fopenmp.
CCCOM=OMPI_CXX=/opt/homebrew/opt/llvm/bin/clang++ mpicxx
# Build flags. omp.h lives in a version-specific clang resource dir, so let
# Homebrew's clang++ find it itself rather than hardcoding an -I path.
CCFLAGS= -std=c++17 -fopenmp -O3
# Link flags: OpenMP runtime from Homebrew LLVM, BLAS/LAPACK from the
# Accelerate framework that ships with macOS (no openblas install needed).
SYSLIB= -L/opt/homebrew/opt/llvm/lib -lomp -framework Accelerate
# 2. Build
cd deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization
make
cd ../../../..
ビルドが成功すると、2行のコンパイル/リンク行が出力され、Makefileの隣にdiagバイナリが生成されます。ld: warning: ignoring duplicate libraries: '-lomp'という警告が1つ出ますが、問題ありません。
Intel macOS: Homebrewのプレフィックスは/opt/homebrewではなく/usr/localであるため、前述の両方のパスでそれに置き換えてください。
Linux(Ubuntu/Debian) — 未テスト
以下の前提条件で、MPI、OpenMP、BLAS/LAPACKを用意できます。GCCは-fopenmpをネイティブにサポートしているため、コンパイラの上書きは不要です。これはLinuxでは検証されていません。
sudo apt install -y libopenmpi-dev libomp-dev libblas-dev liblapack-dev
SBD_PATH=../..
CCCOM=mpicxx
CCFLAGS= -std=c++17 -fopenmp -O3
SYSLIB= -llapack -lblas
SqDRIFTを実行してCI文字列を生成する
すでにステップ1〜4で./SqDRIFTを実行している場合、alphadets_from_sqd.txtはプロジェクトのルートにすでに存在するため、このステップは省略できます。そうでない場合は、プロジェクトのルートから実行してください。
./SqDRIFT
# Writes alphadets_from_sqd.txt in the project root
対角化を実行する
cd deps/sbd/apps/chemistry_tpb_selected_basis_diagonalization
# The FCIDump lives in the shared sqdrift/fcidump_files/ directory, one level
# above this tutorial's project root, hence five levels up from here.
ln -sf ../../../../../fcidump_files/N2_sto_3g fcidump.txt
# alphadets_from_sqd.txt is written by ./SqDRIFT into the project root itself,
# which is four levels up.
ln -sf ../../../../alphadets_from_sqd.txt alphadets.txt
./diag \
--fcidump fcidump.txt \
--adetfile alphadets.txt \
--method 0 \
--iteration 100 \
--block 10 \
--tolerance 1e-8 \
| tee ../../../../sbd_output.txt
cd ../../../..
パラメータ:
--fcidump: ハミルトニアンを含むFCIDumpファイルへのパス--adetfile: アルファ行列式ファイル(SqDRIFTからのCI文字列)へのパス--method 0: Davidson対角化法--iteration 100: Davidson反復の最大回数--block 10: Davidsonアルゴリズムのブロックサイズ--tolerance 1e-8: エネルギーの収束許容値
Elapsed time for helper construction 0.001512 (sec)
Elapsed time for init 1e-06 (sec)
Davidson iteration 0.0 (tol=0.1857493937033264): -107.493531425221
Davidson iteration 0.1 (tol=0.0455699976495391): -107.5098150493379 -105.93368397386
Davidson iteration 0.2 (tol=0.009291923190816917): -107.5108356028703 -106.1245977311231 -105.6823366883346
Davidson iteration 0.3 (tol=0.001488674426419356): -107.5108711084406 -106.3219246016818 -105.7376912083761 -105.4392974334491
Davidson iteration 0.4 (tol=0.0003188697654737439): -107.5108722237947 -106.5935152393201 -106.1108017566682 -105.4932621297472
Davidson iteration 0.5 (tol=3.080273116687244e-05): -107.5108722587463 -106.7245930484005 -106.2313070085243 -105.6423237790271
Davidson iteration 0.6 (tol=2.981303155550973e-06): -107.5108722590151 -106.8008116251279 -106.330411722324 -105.69489630447
Davidson iteration 0.7 (tol=4.565075388900609e-07): -107.5108722590186 -106.8090569201597 -106.3345525819528 -106.0578325555555
Davidson iteration 0.8 (tol=4.870689101320012e-08): -107.5108722590186 -106.8098939189322 -106.3973756614146 -106.1617813840531
Davidson iteration 0.9 (tol=6.224330513865847e-09): -107.5108722590187 -106.8159869231416 -106.4315891597535 -106.2222021964013
Elapsed time for davidson 0.031276 (sec)
Elapsed time for diagonalization 0.031286 (sec)
Elapsed time for mult 0.002511 (sec)
Energy = -107.5108722590187
Elapsed time for measurement 4.6e-05 (sec)
Sample-based diagonalization: Energy = -107.5108722590187
Sample-based diagonalization: density = [1.999989340109303,1.99999662423008,1.999199121034164,1.981735389885314,1.999825046204946,1.999847567448985,1.995347419300126,0.01136512984757364,0.009844780833945199,0.002849581105572152
Sample-based diagonalization: carryover bitstrings = [], size = 0
Energy = -107.5108722590187の行は、サンプリングされた部分空間における基底状態の推定値です。
Pythonの例との違い
2つの例は、誤った対称性を持つビット列の扱いだけでなく、複数の点で同時に異なるため、同一のエネルギーが得られることは期待されません。
| C++タブ | Pythonタブ | |
|---|---|---|
| 誤った対称性を持つビット列 | ハミング重みに基づくpostselect_bitstringsで破棄 | 反復的な配置回復で修復 |
| ループ構造 | 1パスのみ: サンプリング → 事後選択 → 1回対角化 | 外側のループ: 対角化、軌道占有数の読み取り、配置の回復、再対角化 |
| 回路数 | 100回路(num_circuits = 100) | 400回路(発展時間ごとに200) |
| 発展時間 | 単一の(time_step = 1) | 2つの時間、times = [1.0, 10.0] |
| 対角項 | サンプリングされた演算子に保持 | filter_diagonal_termsで除去され、サンプリング分布と回路が変化 |
| 項の順序 | グループ化のみ | 再現性のため、グループ化の前にcanonical_orderを適用 |
| 対角化ソルバー | SBD(diag、外部のMPI/OpenMPバイナリ) | qiskit-addon-sqdのプロセス内ソルバー |
| バックエンド | 名前で固定(backend_name = "ibm_fez"。このページの実行ではibm_pittsburghを使用) | 十分な量子ビット数を持つleast_busyの稼働中バックエンド |
| トランスパイル | 各回路を個別に、最適化レベル2 | すべての回路をまとめて、最適化レベル3 |
| ジョブの送信 | 100ジョブ、各1回路 | 400回路すべてを含む1つのSampler job |
| シミュレーター実行 | なし、ハードウェアのみ | ハードウェア実行の前にノイズなしシミュレーターの例 |
どちらのバージョンも、同じハミルトニアンを使用し、その項を電子構造でグループ化し、各グループを項の平均絶対係数で重み付けし、Hartree-Fock状態から開始し、回路ごとに10個のグループと100ショットをサンプリングします。
それぞれの違いは、サンプリングされる部分空間、ひいてはエネルギーに、独立して影響します。より大きなアンサンブルと、2番目のより長い発展時間は、どちらもPythonの例が探索する配置を広げます。複数の時間でサンプリングすることで、SqDRIFTは低エネルギー空間に対するKrylov的なカバレッジを得ます。対角項をフィルタリングすると、サンプラーが抽出できる演算子とその重みが変わるため、2つの実行は異なる分布をサンプリングすることになります。
事後選択は厳格なフィルターであり、粒子数が誤っているショットは完全に破棄されます。上記の実行では、それによりデータの大半が破棄され、10000ショットのうち(7,7)ハミング重みのチェックを通過したのは1137ショット(約11%)で、わずか49個の異なる行列式にまとまりました。配置回復では、代わりに以前の対角化で得られた平均軌道占有数を使用してビットを反転させ、そのようなショットを修復し、事後選択で破棄されるものを再利用します。このフィードバックにより、本質的に反復的になります。qiskit-addon-sqd-hpcのrecover_configurationsは、対角化が実行された後にのみ存在するavg_occupancies引数を必要とするためです。
このC++の実行では、49個の行列式からなる部分空間と、-107.5109 Haのエネルギーが得られます。10軌道に7個の電子を配置する行列式の完全な空間は、わずか個の文字列であるため、120個すべてを同じdiagバイナリに与えると、厳密なFCIエネルギー-107.6482 Haが得られます。したがって、この実行は、120個の行列式のうち49個にまたがりながら、厳密値より約0.1373 Ha高い位置にあります。SQDは選択された部分空間において変分的であるため、小さい部分空間ではエネルギーが上がるだけであり、不完全な部分空間が、この差の最も直接的な解釈となります。
この厳密なFCIによる確認は、例が小さいからこそ可能です。これはSqDRIFTのワークフローの一部ではなく、SQDが実際に必要となる規模のシステムでは、そのような参照値は存在しません。ここでは、この実行が空間のどれだけを回復したかを定量化するためだけに用いています。
Pythonの例との比較は統制されたものではないため、差異を配置回復がないことだけに帰すべきではありません。回路数、単一の発展時間、対角項のフィルタリング、ハードウェアノイズがすべて同時に異なり、そのすべてが部分空間を形作っている点に注意してください。
この差を縮めるために、このチュートリアルを終えた後に試せる次のステップとして、shots、num_circuits、またはops_per_circuitを増やすこと、複数のtime_stepでサンプリングすること、そしてQiskit::addon::sqd::recover_configurationsの上に回復ループを追加することが挙げられます。
次のステップ
-
フェルミオン格子模型のサンプルベースKrylov量子対角化 — 変分アンサッツの代わりに時間発展回路を使用する関連チュートリアルです。
-
化学ハミルトニアンのサンプルベース量子対角化 — 量子化学シミュレーションのために、局所ユニタリークラスターJastrow (LUCJ)回路を構築する方法に関するチュートリアルです。
-
SqDRIFTの論文 — このチュートリアルの基になっている文献です。(この論文で議論されている最適化の一部は現在開発中であり、このチュートリアルは、使用しているライブラリの進化に基づいて今後変更される可能性があります。)