Skip to main content
IBM Quantum Platform

SqDRIFT 基底状態推定のためのアルゴリズム

推定所要時間:Heron r3 プロセッサで 180 秒(注:これはあくまで推定値です。 (実行時間は状況によって異なる場合があります。)

C++版をお探しですか?

このチュートリアルでは、 Python を使用しています。 C++による実装(ソースコードおよびビルド手順を含む)については、「 C++ SqDRIFT チュートリアル 」を参照してください。


学習成果

  • トロッター化に比べて、奥行きが浅い回路を作成する方法について学びましょう
  • qDRIFT とSQDを用いた基底状態推定のエンドツーエンドのワークフローを順を追って解説します
  • 他のQiskitアドオンと組み合わせて qiskit-fermions 、このようなワークフローを実装する方法について学びましょう

このチュートリアルは、教育目的で Python ノートブックとして提供されています。


前提条件


背景

SqDRIFT これはSKQDの変種であり、ビット列をサンプリングするためのアンザッツを選択する必要性を、対象のハミルトニアンから直接構築された時間発展回路の集合体に置き換えたものである。 これは、ハミルトニアンの係数に基づいて、より小さな時間発展演算子をハミルトニアンからサブサンプリングすることで実現され、これは「 qDRIFT トロッター化法」として知られている。

このチュートリアルでは、 Qiskit Fermions を利用して、 qDRIFT アルゴリズム向けのより自然なフェルミオン回路を作成します。その後、フェルミオン向けのレイアウトおよび合成処理を適用し、最終的に回路を従来のQiskitパイプラインに組み込んでハードウェア上で実行します。

ハミルトニアンを次のような形とする:

H=∑i=1NcihiH = \sum_{i=1}^{N} c_i h_i

ここで、一般性を失うことなく、 ci>0c_i > 0 を満たし、かつ hih_i の最大固有値の絶対値が 11 に等しいものと仮定する。符号付きまたは複素数の前因子はいずれも hih_i に吸収されるため、係数 cic_i は厳密に正の重みとなり、 hih_i は各項の方向を表す。 ここで、 NN はハミルトニアンの項の数(あるいは、グループ分け後のグループの数)であり、これはハミルトニアンの性質である。これは、以下で nn と表記される、単一の回路にサンプリングされる演算子の数とは異なる。

qDRIFT アルゴリズムは、目標時間 tt に対して、ある演算子 VkV_k を実現する。ここで、 kk は 1⋯K1 \cdots K から始まり、次のように定義される kthk_{th}SqDRIFT 回路を表す:

Vk=∏j=1ne−ihkjλt/nV_k = \prod_{j=1}^{n} e^{-i h_{k_j} \lambda t / n }

ここで、 nn は回路あたりのサンプリングされた演算子の数、 KK はアンサンブル内の回路の数である。 この製品は、すべての NN ハミルトニアン項ではなく、 nn の抽出結果に基づいて動作します。また、項は重複ありで抽出されるため、単一の VkV_k 内に同じ hih_i が複数回出現する可能性があります。

数量:

λ=∑i=1Nci\lambda = \sum_{i=1}^{N} c_i

は係数の L1L_1 ノルムであるため、 nn の各ステップは、どの項が抽出されたかに関係なく、同じ時間 λt/n\lambda t / n だけ進行する。 ステップ角の均一性は、 qDRIFT: の特徴であり、係数は、その項がどれだけ回転するかではなく、 その項がどれだけ頻繁に引き出されるかによって結果に影響を与えます。 各指標は、以下の分布からサンプリングされます:

P[ki]=ciλP[k_i] = \frac{c_i}{\lambda}

したがって、 (k1,…,kn)(k_1, \ldots, k_n) という数列は、この分布から抽出された項の添字のランダムな列である。 cic_i は正の値であり、その和は λ\lambda となるため、これは正規化された確率分布であり、ランダムな抽出に基づく結果のチャネルの期待値は、 HH の下での進化を近似する。この誤差は、 nn が大きくなるにつれて減少する。 なお、近似誤差は、項の数 NN ではなく、 λ\lambda に依存することに注意してください。

(『 SqDRIFT 』の論文では、項の数を N\mathcal{N}、数列の長さを NN と表記しているが、ここでは両者を明確に区別するために、 NN および nn という表記を用いる。)

このチュートリアルでは、このようなランダム化された回路のアンサンブルを生成する方法について説明します。 これらの回路を作成した後、異なる演算子に対してクリロフ部分空間を作成する場合と同様に、異なる時間パラメータを持つ複数のそのような演算子からビット列をサンプリングする。 これにより、基底状態ベクトルとサンプリングされたビット列との間の重なりがより高くなる。


要件

このチュートリアルを始める前に、以下のものがインストールされていることを確認してください

  • Python ( 3.10 以上)の仮想環境
  • pip>=25.1
  • qiskit ≈ 2.5
  • qiskit-fermions==0.1.0 (この名称は複数形であることに注意してください)
  • numpy
  • pyscf
  • qiskit-aer
  • qiskit-ibm-runtime
  • qiskit-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:古典的な入力を量子問題に写像する

FCIDump の読み込みと準備

このチュートリアルでは、窒素の電子構造ハミルトニアンを読み込みます( N2 )。 フェルミオン演算子を作る方法は他にもあります。 のドキュメントを参照してください qiskit_fermions.operators.library。

このFCIDumpについて。 このファイルは、最小 STO-3G 基底関数系における窒素分子( N2N_2 )について記述 N2_sto_3g しており、原子間距離は 1.09A˚\AA に設定されている。これは、実験的に得られた平衡結合長である。 そのヘッダーには NORB=10、 NELEC=14、、およびが宣言されている MS2=0。すなわち、10個の空間軌道(したがって、20個のスピン軌道、およびジョーダン・ウィグナー表現の下では20個の量子ビット)、スピンシングレット状態にある14個の電子、つまり7個の α\alpha 電子と7個の β\beta 電子である。 すべての軌道には対称性ラベル「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軌道に依存するため、再生成されたファイルは、軌道の位相や順序において出荷時のファイルと異なる場合がありますが、総エネルギーには影響しません。

ファイルの取得。 この GitHub リポジトリで FCIDump を見つけてください。 以下のセルを実行すると、チュートリアルの残りの部分で想定されている場所にそのデータを取得できます。

まず、pyscf が提供する cisolver を使用して、基準エネルギーを求めます。 これが、我々が扱っている分子の真の基底状態エネルギーである。 このため、まず nelec、それぞれ軌道数と電子数を表す norb と を定義する。 次に、それぞれ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 = "assets/sqdrift/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}")

Output:

Using existing FCIDump at assets/sqdrift/fcidump_files/N2_sto_3g
name = "assets/sqdrift/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")

Output:

Parsing assets/sqdrift/fcidump_files/N2_sto_3g
Reference FCI Energy  = -107.6481842917 Ha
Nuclear Repulsion Energy = 23.7887003074 Ha

ハミルトニアンの読み込み

必要なデータが準備できたので、FCIファイルから、以下の形式と互換性のあるハミルトニアンを読み込みます。 qiskit-fermions

fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb

フェルミオンを用いたワークフロー qiskit-fermions

まず、トランスパイラーのパスやフェルミオン回路特有のゲートを提供する を用いて qiskit-fermions、ハミルトニアンをフェルミオン回路モデルにマッピングします。 これらは、このワークフローにおいて、Qiskitの従来のトランスパイラーによる処理が行われる前に使用されます。

項目のグループ化

結果の再現性を確保するため、まず を用いて、項をその構造のみに基づいて並べ canonical_order 替えます。 したがって、リスト canon 内の演算子の順序は固定されています。 これにより、作成された演算子の再現性が確保されます。というのも、今後使用する QDriftTrotterization passが、ランダムなインデックスをサンプリングして qDRIFT 演算子を作成するからです。

このステップでは、電子構造ハミルトニアンに存在する多くの対称性を活用し、係数が同じである関連する項をグループ化します。 そうすることで、 qDRIFT プロトコルがサンプリングを行う演算子係数の分布は変化しますが、その収束保証には影響しません。 重要なことに、対称性によって関連する項をグループ化することで、パウリ項が好都合に相殺され、それらの作用下で状態を時間発展させる際の回路深度が全体的に短くなる。

qiskit-fermions このグループ化を自動的に行ってくれる関 group_terms_by_electronic_structure 数が用意されています。

なお、この式では、項が通常の順序で並んでいることを前提 group_terms_by_electronic_structure としていることに注意してください。

対角項のフィルタリング

回路を生成するために使用するハミルトニアンから対角成分を除去することで、 nn qDRIFT サンプリングスロットが、構成間の状態分布を変化させる項に割り当てられるようにします。 こうした項は、次のステップでゲート Evolution が構築される前のこの段階で、ハミルトニアンから除外しておくのが最善である。

ここで問題となる項は、職業-数基底において対角にあるもの、すなわち、数演算子の積である ai†aia^\dagger_i a_i である。この定義に該当する項には、以下の3種類がある:

  • 定数エネルギーオフセット。これはゼロ数演算子の積であり、その時間発展はグローバルな位相のみに寄与する;
  • 個々の数演算子nin_i。これらは、時間発展により単一量子ビットの ZZ 回転に還元される;
  • ninjn_i n_j などの高階積。

これらはいずれも、それ自体では「占領数構成」間の住民の移動を引き起こすことはなく、すでに存在する構成の「フェーズ」にのみ作用する。 しかし、それらは決して無関係なものではない。これらの相対位相は、回路の後半にある励起項によって生じる干渉に影響を与えるため、それらを除去すると、実際に生成される挙動が変化し、サンプリング分布も変化する可能性がある。 これは、回路生成段階における意図的な近似であり、サンプリングを励起項に集中させるために行われるものであって、サンプリングされた分布をそのまま残す段階ではない。 上記の対称性グループ化とは異なり、このフィルタは、 qDRIFT の収束保証を損なうことなく、進化の対象となる演算子を変更します。 したがって、これらの回路はもはや完全ハミルトニアン下での進化を近似するものではなく、 qDRIFT の誤差上限は、元の演算子ではなく、フィルタリングされた演算子に適用されることになる。 ここではこれが許容されるのは、回路が構成案を提案するために用いられる単なるサンプリング手法に過ぎないためである。フィルタは回路の構築に使用されるハミルトニアンのみに適用されるのに対し、その後の古典的な対角化では対角項を含む完全なハミルトニアンが使用されるため、エネルギー推定そのものから項が失われることはない。 SQDの精度は、その古典的なステップに依存しており、このステップは、構成がどのように提案されたかに関わらず、サンプリングされた部分空間において変分的なままである。

この関 filter_diagonal_terms() 数は、演算子からそのような項をその場で削除します。 これは、それらの「正規順序構造」――すなわち、生成モードの多集合が消滅モードの多集合と一致すること――によってそれらを特定するため、すでに正規順序付けられている演算子に対してのみ有効である。 この仮定は実行時に検証されません。

# 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))

Output:

5060

ハミルトニアンの項をグループ分けしたので、回路のアンサンブルを生成するために、以下のパラメータを決定します:

  • 生成する回路の数: num_circuits
  • 励起群に基づく各回路の長さ: num_exc
  • 進化時間が異なる要因: times

フェルミオン回路の構築

それでは、各時間ステップごとにフェルミオン回路を作成していきます。 各回路は、先ほど定義した進化時間を用いた単一の進化ゲートで構成されます。 進化演算子はハミルトニアンである。 その後、これらの回路に対してトランスパイラー処理を行い、 qDRIFT 回路を生成します。

アンツァッツの準備

class InitializeModes を使用して、ハートリー・フォック状態を生成します。 窒素の場合、この処理は、まず最初の 量子 num_elec_a ビットに X ゲートを適用し、次に 量子 num_elec_b ビットに X ゲートを適用するという単純なもので、窒素の場合、これらはいずれも 7 個ずつです。 この状態は、窒素の7つの α\alpha 電子と7つの β\beta 電子を表しています。

# 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 トランスパイラ・パスが、この処理を代行してくれます。 これにより、量子ビット間の接続性が限られている場合でも、ハミルトニアンに長距離結合や二次以上の項が含まれている場合でも、ハードウェア上でより効率的に実行できる、より浅い回路を作成できるようになりました。 項のグループ化を行った後、重みに基づいて演算子をサンプリングします。 各演算子 hih_i について、重み WhiW_{h_i} は次のように定義される:

Whi=∣ci∣/λW_{h_i} = |c_i| / \lambda

フェルミオンおよびハードウェアネイティブの最適化

この関数は、を受け取り FermionicCircuit 、ハードウェア上で実行できるようトランスパイルできる最適化された最終回路を生成する MultiStagePassManager を返します generate_preset_jw_pass_manager() 。 そのデフォルトの最適化ステージを、私たちのパ QDriftTrotterization スを含む FermionicPassManager ものに置き換えます:

  • この QDriftTrotterization パスでは、内部で重み計算とサンプリングを行い、サンプリングに使用する回路を生成します
  • このパス RelabelModes は、フェルミオンモードの順列を変更して量子ビット間の接続性を最適化し、ゲートの深さを削減するために使用できる、もう1つの最適化パスです。詳細については、 APIリファレンスをご覧ください

残りの段階は自動的に実行 MultiStagePassManager され、フェルミオンから量子ビットへのマッピングをすべて処理します:

  • F2QLayout :プリセット・パス・マネージャーは、 nn のフェルミオン・ビットを nn の量子ビットに単純にマッピングするパスを適用します 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))

Output:

400

フェルミオンレベルの最適化が完了したので、シミュレータ上で実行するために回路をトランスパイルすることができます。

simulator = AerSimulator()
shots = 100

transpiled_circuits = transpile(sqdrift_circuits, simulator)

ステップ 3: Qiskit primitives を使用して実行する

回路が完成したので、 AerSimulator 上で Qiskit primitives を使用して実行することができます。 異なる回路からの集計結果をすべて合算します。 これらをブールベクトルに変換してから、最終的に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")

Output:

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")

Output:

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を実行する前に無効なビット文字列をフィルタリングするか、あるいは剪定を行わずに処理を進めるか、どちらかを選択できます。 ハードウェア実行においては、一般的に剪定を省略する方が望ましい。そうすることで、対称性が破れたショットを構成回復の対象として残すことができ、それらを有効な構成に修復して部分空間を拡大することができるためである。これに対し、剪定を行うと、そうしたショットは即座に破棄されてしまう。

窒素は α\alpha と β\beta の電子をそれぞれ7個しか持つことができないため、出力の前半と後半で 1s が7個より多い、あるいは少ないビット列はすべて除外することができます。 ビット文字列が有効かどうかを検証し、有効でない場合はそれらを破棄する関数を定義します。 誤ったビット列を除去した後、残りのビット列は対角化処理に送られます。 以下のフラグ PRUNE を使用して、2つの動作を切り替えてください。

剪定は、回路数、進化時間のセット、対角項フィルタリングと並んで、最終的な部分空間を形作るいくつかの選択肢のうちの1つにすぎないことを念頭に置いておいてください。 プリニングを施した実行と施していない実行を比較しても、他のすべての条件が固定されている場合にのみ意味があります。 C++の解説書では、この点についてさらに詳しく論じています。なぜなら、C++の解説書ではリカバリではなくポストセレクションを採用しており、またその他のパラメータにおいても異なるからです。

name = "assets/sqdrift/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")

Output:

Parsing assets/sqdrift/fcidump_files/N2_sto_3g
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

次のステップ

推奨事項

この作品に興味を持たれた方は、以下の資料もご参照ください:

このページは役に立ちましたか?
バグや誤字の報告、またはコンテンツの要求はGitHubで行ってください。