化学ハミルトニアンのサンプル・ベースの量子対角化
使用時間の見積もり:Heron r2 プロセッサーで1分未満(注:これはあくまでも見積もりです。 ランタイムは異なるかもしれない)。
学習成果
- SQD Qiskit アドオンを使用して、量子処理ユニット(QPU)からサンプリングしたビット列を用いて、分子系の基底状態エネルギーを近似する方法。
- ffsim を使用して、量子化学シミュレーション用の局所ユニタリークラスター・ジャストロウ(LUCJ)回路を構築する方法。
前提条件
- 量子化学と第二量子化
- サンプラープリミティブを使用した量子回路からのサンプリング
背景
このチュートリアルでは、 SQD Qiskit アドオンを使用してサンプルベース量子対角化(SQD)アルゴリズムを実装し、ノイズを含む量子サンプルを後処理して、平衡結合長における窒素分子 の基底状態を近似する方法を解説します。 このソフトウェアの詳細については、対応するドキュメントに記載されています。そこには、使い始めるための簡単な例も掲載されています。
窒素化学は、肥料用のアンモニア生産を通じて、この例と持続可能性を結びつけています。 触媒が窒素をどのように活性化させるかを解明することは、エネルギー消費の少ない製造方法に関する研究の一環である。 孤立した窒素分子の基底状態を計算することは、そのより広範な課題に関連する電子構造計算手法を取り入れることにつながる。
このチュートリアルは、量子化学、特に分子の基底状態のエネルギーを求める方法に精通しているユーザーにおすすめです。 ワークフローの詳細な手順については、 「量子対角化アルゴリズム」コースを参照してください。
SQDは、量子計算と分散型古典計算を組み合わせて、量子系のハミルトニアンなどの量子演算子の固有値と固有ベクトルを求める手法である。 従来の分散計算は、量子プロセッサから得られたサンプルを処理し、それらが張る部分空間において対象のハミルトニアンを射影および対角化するために用いられる。 SQD ベースのワークフローには、以下の手順が含まれます:
- 回路アナザッツを選び、量子コンピュータ上で参照状態(この場合はハートリーフォック状態)に適用する。
- 出来上がった量子状態からビット列をサンプリングする。
- ビット列に対して自己一貫性のある構成復元手順を実行し、基底状態の近似を求める。
SQDは、対象となる固有状態がスパースである場合にうまく機能することが知られています。波動関数は基底状態のセット 、そのサイズは問題の大きさに対して指数関数的に増加することはありません。
量子化学
分子系のハミルトニアンは次のように書ける
ここで、 と は分子積分と呼ばれる複素数で、コンピュータ・プログラムを使って分子の仕様から計算できる。 このチュートリアルでは PySCF ソフトウェアパッケージを使用して積分を計算します。
分子ハミルトニアンの導出方法の詳細については、量子化学の教科書(例えば、SzaboとOstlund著『 Modern Quantum Chemistry 』)を参照されたい。 量子化学の問題がどのように量子コンピュータにマッピングされるかについての高レベルの説明は、Qiskit Global Summer School 2024の講義 Mapping Problems to Qubitsをご覧ください。
局所ユニタリークラスター・ジャストロー(LUCJ)近似
SQDでは、サンプルを抽出するための量子回路のアンザッツが必要です。 このチュートリアルでは、物理的な根拠とハードウェアへの適応性を兼ね備えていることから、 ローカルユニタリークラスター・ジャストロウ(LUCJ) アンザッツを用います。 ffsim を使用して、近似回路を構築します。
LUCJアプローチは、量子ビット間の接続性が制限されているQPUにも対応しています。 スピン軌道は、SWAPゲートによるルーティングを必要としないような形で量子ビットに割り当てられる。 IBM® ハードウェアのクビットトポロジーは、正方格子または重六角格子のいずれかである。 ヘビーヘックス格子については、以下に示すような「ジグザグ」パターンを採用することができます。 このパターンでは、同じスピンを持つ軌道が線状トポロジーを持つ量子ビット(赤と青の円)にマッピングされており、異なるスピンを持つ軌道間の接続は空間上の軌道ごとに4つおきに存在し、その接続は補助量子ビット(紫色の円)によって実現されている。
自己整合的な構成復旧
自己無撞着構成回復手順は、ノイズの多い量子サンプルから可能な限り多くの信号を抽出するように設計されている。 分子ハミルトニアンは粒子数とスピンZを保存するので、これらの対称性も保存する回路アサッツを選ぶことは理にかなっている。 ハートリーフォック状態に適用すると、結果として得られる状態は、ノイズのない設定において固定された粒子数とスピンZを持つ。 したがって、この状態からサンプリングされたビット列のスピン とスピン の半分は、ハートリーフォック状態と同じハミングウェイトを持つはずである。 現在の量子プロセッサーにはノイズが存在するため、測定されたビット列の中にはこの性質に反するものがある。 単純なポストセレクションでは、これらのビット列は破棄されるが、これは無駄である。 自己無撞着な回復手順は、後処理でその信号の一部を回復しようとする。 この手順は反復的であり、基底状態における各軌道の平均占有率の推定値を入力として必要とする。 この手順はループで実行され、各反復には以下のステップがある:
- 指定された対称性に違反する各ビット列について、ビット列を平均軌道占有率の現在の推定値に近づけるように設計された確率的手順でビットを反転させ、新しいビット列を得る。
- 対称性を満たす新旧のビット列をすべて集め、あらかじめ選んだ固定サイズの部分集合をサブサンプルする。
- ビット列の各サブセットについて、ハミルトニアンを対応する基底ベクトル(これらの基底ベクトルの説明については前節を参照)によってスパンされる部分空間に射影し、古典コンピュータ上で射影されたハミルトニアンの基底状態推定値を計算する。
- 平均軌道占有率の推定値を、最もエネルギーの低い基底状態の推定値で更新する。
SQDワークフロー図
SQDのワークフローは以下の図のようになっている:
要件
このチュートリアルを始める前に、以下のものがインストールされていることを確認してください:
- Qiskit SDK v1.0 またはそれ以降、 可視化サポート付き
- Qiskit Runtime v0.22 またはそれ以降 (
pip install qiskit-ibm-runtime) - SQD Qiskit アドオン v0.12 以降 (
pip install qiskit-addon-sqd) - ffsim v0.0.75 以降 (
pip install ffsim)
セットアップ
import math
import ffsim
import matplotlib.pyplot as plt
import numpy as np
import pyscf
import pyscf.cc
import pyscf.mcscf
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.primitives import StatevectorSampler
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler小規模シミュレータの例
このチュートリアルでは、平衡結合距離付近にある窒素分子の基底状態の近似解を求めます。 まず、実験をシミュレーションして正常に動作することを確認するため、小規模な STO-6G 基底セットを使用します。
ステップ1:古典的な入力を量子問題にマッピングする
まず、分子とその特性を特定します。
# Specify molecule properties
spin_sq = 0
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["N", (0, 0, -0.5)], ["N", (0, 0, 0.5)]],
basis="sto-6g",
symmetry="Dooh",
)
# Define active space
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)
# Compute exact energy using FCI
reference_energy = cas.run().e_tot
print(f"norb = {norb}")
print(f"nelec = {nelec}")Output:
converged SCF energy = -108.464957764796
CASCI E = -108.595987350986 E(CI) = -32.4115475088426 S^2 = 0.0000000
norb = 8
nelec = (5, 5)
LUCJアンサッツ回路を構成する前に、まず以下のコードセルでCCSD計算を行う。 この計算で得られた と の振幅は、ansatzのパラメータを初期化するのに使われる。
# Get CCSD t2 amplitudes for initializing the 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.t2Output:
E(CCSD) = -108.5933309085008 E_corr = -0.1283731437052352
ここで、 ffsim を使用してアンザッツ回路を作成します。 我々の分子は閉殻のハートリー・フォック状態を持つため、UCJアンザッツのスピン平衡型、すなわち UCJOpSpinBalanced を用いる。 の振幅の「圧縮」二重因数分解を有効にするため、method from_t_amplitudes メソッドに compressed を設定 optimize=True しました(詳細については、ffsimのドキュメントにある 「The local unitary cluster Jastrow (LUCJ) ansatz」 を参照してください)。
LUCJアンザッツはQPUの利用可能な接続性に適応するため、アンザッツを作成する前にQPUバックエンドを初期化する必要があります。 ここではまず、正方格子結合写像と、LUCJアンザッツが自然に分解されるゲートセットを備えた汎用的なバックエンドを作成します。 次に、を使用して、LUCJアンザッツを「ラダー」レイアウトに従って指定されたバックエンドへトランスパイルすることに特化したパスマネージャを作成 ffsim.qiskit.generate_lucj_pass_manager します(ffsimのドキュメントにある説明を参照してください)。 この関数は、スコアリングヒューリスティックを用いて、選択されたレイアウトに関連する誤差を最小限に抑えます。これは、バックエンドが実際のQPUである場合や、ノイズモデルを備えたシミュレータである場合に重要です。 この関数は、パスマネージャーを返すほか、ハードウェア上で実装可能なアルファ・ベータ結合ペアも返します。 すべてのペアを実装できない場合は、警告が出されます。
import warnings
from qiskit.transpiler import CouplingMap
warnings.formatwarning = lambda msg, *args, **kwargs: f"Warning: {msg}\n"
# Set ansatz properties
n_reps = 4
pairs_aa = [(p, p + 1) for p in range(norb - 1)]
# Let generate_lucj_pass_manager determine the alpha-beta interactions
pairs_ab = None
# Initialize backend
coupling_map = CouplingMap.from_grid(norb, 2)
backend = GenericBackendV2(
coupling_map.size(),
coupling_map=coupling_map,
basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
)
# Create pass manager
pass_manager, pairs_ab = ffsim.qiskit.generate_lucj_pass_manager(
backend=backend,
norb=norb,
connectivity="square",
interaction_pairs=(pairs_aa, pairs_ab),
optimization_level=3,
)
# Create the LUCJ ansatz operator
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
t2=t2,
t1=t1,
n_reps=n_reps,
interaction_pairs=(pairs_aa, pairs_ab),
# Setting optimize=True enables the "compressed" factorization
optimize=True,
)
# create an empty quantum circuit
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
# prepare Hartree-Fock state as the reference state and append it
# to the quantum circuit
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
# apply the UCJ operator to the reference state
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()ステップ2:量子ハードウェアでの実行に向けた最適化
次に、対象のハードウェアに合わせて回路を最適化します。 通常、このステップでは、ハードウェアバックエンドとそのバックエンド用のパスマネージャーを初期化します。 ただし、LUCJアプローチはハードウェアの接続性に適合しているため、これらの処理は前のステップですでに実施済みです。 あとは、回路上でパスマネージャーを実行し、QPU上で直接実行可能なISA回路にトランスパイルするだけです。
isa_circuit = pass_manager.run(circuit)
print(f"Gate counts: {isa_circuit.count_ops()}")Output:
Gate counts: OrderedDict({'xx_plus_yy': 254, 'cp': 88, 'p': 64, 'measure': 16, 'x': 10, 'barrier': 1})
ステップ3: Qiskit primitivesを使用して実行する
ハードウェア実行向けに回路を最適化した後、ターゲットハードウェア上で実行し、基底状態のエネルギー推定のためのサンプルを収集する準備が整いました。 回路は1つしかないため、 IBM Quantum の「Compute Service」 ジョブ実行モードを使用して、この回路を実行します。
rng = np.random.default_rng(12345)
sampler = StatevectorSampler(seed=rng)
job = sampler.run([isa_circuit], shots=100_000)primitive_result = job.result()
pub_result = primitive_result[0]ステップ4:後処理を行い、結果を希望の古典形式で返す
QPU出力の品質を判断する有用な指標は、返される有効な構成の数である。 有効な構成は正しい粒子数とスピンZを持つ。これはビット列の右半分がスピンアップ電子の数に等しいハミング重量を持ち、左半分がスピンダウン電子の数に等しいハミング重量を持つことを意味する。 以下のセルは、サンプリングされた構成のうち有効なものの割合を計算します。
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
)
bit_array = pub_result.data.meas
num_valid = sum(
is_valid_bitstring(b, norb, nelec) for b in bit_array.get_bitstrings()
)
valid_fraction = num_valid / bit_array.num_shots
print(f"Fraction of sampled configurations that are valid: {valid_fraction}")Output:
Fraction of sampled configurations that are valid: 1.0
ノイズのないシミュレータで回路をサンプリングしているため、すべてのビット列は有効です。 ノイズの多いQPU上で実行した場合、この比率は1未満になりますが、ビット列が均一にランダムにサンプリングされた場合に予想される比率よりは大きくなることを期待しています。その値は、次のセルで計算されます。
expected_fraction_random = (
math.comb(norb, n_alpha) * math.comb(norb, n_beta) / 2 ** (2 * norb)
)
print(
f"Expected fraction of valid configurations from uniformly random bitstrings: "
f"{expected_fraction_random}"
)Output:
Expected fraction of valid configurations from uniformly random bitstrings: 0.0478515625
ここで、 diagonalize_fermionic_hamiltonian 関数を用いてハミルトニアンの基底状態エネルギーを推定する。 この関数は、エネルギー推定値を改善するために、ノイズの多い量子サンプルを繰り返し精緻化する自己無撞着構成回復手順を実行する。 コールバック関数を渡すことで、後の分析のために中間結果を保存することができる。 diagonalize_fermionic_hamiltonian への引数の説明については、 APIドキュメントを参照のこと。
ここでは、基底状態における軌道占有の初期推定値としてハートリー・フォック配置を指定するために、引数 を用いて diagonalize_fermionic_hamiltonian``initial_occupancies を指定する。 この手法は、基底状態がハートリー・フォック配置に顕著な支持を持つシステムでは合理的であるが、他の状況では適切でない可能性がある。ただし、より高度な計算手法を用いれば、そうしたケースでもより良い初期推定値が得られるかもしれない。 Specifying initial_occupancies は、ノイズの多い QPU 上で大規模な回路をサンプリングする場合など、有効な構成がサンプリングされなかった場合でも構成復元を実行できるようにします。 この引数がなければ、有効な構成が提供されていない場合、構成の復元は失敗しエラーが発生します。
from functools import partial
from qiskit_addon_sqd.fermion import (
SCIResult,
diagonalize_fermionic_hamiltonian,
solve_sci_batch,
)
# SQD options
energy_tol = 1e-3
occupancies_tol = 1e-3
max_iterations = 5
# Eigenstate solver options
num_batches = 3
samples_per_batch = 1000
max_dim = 50
symmetrize_spin = True
carryover_threshold = 1e-4
max_cycle = 200
# Use the Hartree-Fock configuration as an initial guess for the orbital occupancies
initial_occupancies = (
np.array([1] * n_alpha + [0] * (norb - n_alpha)),
np.array([1] * n_beta + [0] * (norb - n_beta)),
)
# Pass options to the built-in eigensolver. If you just want to use the defaults,
# you can omit this step, in which case you would not specify the sci_solver argument
# in the call to diagonalize_fermionic_hamiltonian below.
sci_solver = partial(solve_sci_batch, spin_sq=0.0, max_cycle=max_cycle)
# List to capture intermediate results
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)}"
)
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,
max_dim=max_dim,
initial_occupancies=initial_occupancies,
carryover_threshold=carryover_threshold,
callback=callback,
seed=rng,
)
final_energy = result.energy + nuclear_repulsion_energy
energy_error = final_energy - reference_energy
print(f"Final energy: {final_energy}")
print(f"Final energy error: {energy_error}")Output:
Iteration 1
Subsample 0
Energy: -108.59589540253083
Subspace dimension: 2500
Subsample 1
Energy: -108.59589540253083
Subspace dimension: 2500
Subsample 2
Energy: -108.59589540253083
Subspace dimension: 2500
Iteration 2
Subsample 0
Energy: -108.59589540253083
Subspace dimension: 2500
Subsample 1
Energy: -108.59589540253083
Subspace dimension: 2500
Subsample 2
Energy: -108.59589540253083
Subspace dimension: 2500
Final energy: -108.59589540253083
Final energy error: 9.19484552071026e-05
結果を可視化する
最初のグラフは、このシミュレーションにおいて、最初の反復計算の後ですでに正確な解 1 mH に極めて近い値を得ていることを示しています(化学的精度は通常、 1 kcal/mol 程度と 1.6 mHみなされます)。ただし、これは小規模な系であり、サンプルにノイズが含まれていないため、構成の復元は必要ありません。 ノイズの多いQPU上で動作する大規模なシステムでは、構成の復元を複数回繰り返す必要が生じ、最終的な精度が低下する可能性があります。 一般的に、構成の復元反復回数を増やすか、バッチあたりのサンプル数を増やすことで、エネルギーを改善することができます。
2番目のプロットは、最終反復後の各空間軌道の平均占有率を示している。 スピン・アップ電子とスピン・ダウン電子の両方が、解の最初の5つの軌道を高い確率で占めていることがわかる。
# Data for energies plot
x1 = range(len(result_history))
min_e = [
min(result, key=lambda res: res.energy).energy + nuclear_repulsion_energy
for result in result_history
]
e_diff = [abs(e - reference_energy) for e in min_e]
# Chemical accuracy (+/- 1.6 milli-Hartree)
chem_accuracy = 0.0016
# Data for avg spatial orbital occupancy
y2 = np.sum(result.orbital_occupancies, axis=0)
x2 = range(len(y2))
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
# Plot energies
axs[0].plot(x1, e_diff, label="energy error", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-5, 1e-2)
axs[0].axhline(
y=chem_accuracy,
color="#BF5700",
linestyle="--",
label="chemical accuracy",
)
axs[0].set_title("Approximated Ground State Energy Error vs SQD Iterations")
axs[0].set_xlabel("Iteration Index", fontdict={"fontsize": 12})
axs[0].set_ylabel("Energy Error (Ha)", fontdict={"fontsize": 12})
axs[0].legend()
# Plot orbital occupancy
axs[1].bar(x2, y2, width=0.8)
axs[1].set_xticks(x2)
axs[1].set_xticklabels(x2)
axs[1].set_title("Avg Occupancy per Spatial Orbital")
axs[1].set_xlabel("Orbital Index", fontdict={"fontsize": 12})
axs[1].set_ylabel("Avg Occupancy", fontdict={"fontsize": 12})
plt.tight_layout()
plt.show()Output:
大規模なハードウェアの例
それでは、実際の量子ハードウェア上で、より大規模な例を実行してみましょう。 ここでは、 cc-pVDZ 基底関数セットを用いて、窒素分子のアクティブ空間を導出する。 また、以下の変更も行います:
- ゲートエラーによるノイズを低減するために
1、値をn_reps小さくする - バックエンドプロセッサのファミリーに基づいて、アンザッツの接続性を設定します(Heronの場合は「heavy-hex」、Nighthawkの場合は「square」)
- より大きな部分空間の次元を許容できるように
1000、をmax_dimに増やす。
手順 1~4
ここでは、これらすべての手順を統合して大規模な単一のワークフローとし、それを実際の量子ハードウェア上で実行します。
# ------------------------------ Step 1 ------------------------------
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["N", (0, 0, -0.5)], ["N", (0, 0, 0.5)]],
basis="cc-pvdz",
symmetry="Dooh",
)
# Define active space
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)
# Store reference energy from SCI calculation performed separately
reference_energy = -109.22802921665716
print(f"norb = {norb}")
print(f"nelec = {nelec}")
# Get CCSD t2 amplitudes for initializing the 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
# Set ansatz properties
n_reps = 1
pairs_aa = [(p, p + 1) for p in range(norb - 1)]
# Let generate_lucj_pass_manager determine the alpha-beta interactions
pairs_ab = None
# Initialize backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(f"Using backend {backend.name}")
# Create pass manager
connectivity_map = {"Heron": "heavy-hex", "Nighthawk": "square"}
pass_manager, pairs_ab = ffsim.qiskit.generate_lucj_pass_manager(
backend=backend,
norb=norb,
connectivity=connectivity_map[
backend.configuration().processor_type["family"]
],
interaction_pairs=(pairs_aa, pairs_ab),
optimization_level=3,
)
# Create the LUCJ ansatz operator
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
t2=t2,
t1=t1,
n_reps=n_reps,
interaction_pairs=(pairs_aa, pairs_ab),
# Setting optimize=True enables the "compressed" factorization
optimize=True,
)
# create an empty quantum circuit
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
# prepare Hartree-Fock state as the reference state and append it
# to the quantum circuit
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
# apply the UCJ operator to the reference state
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()
# ------------------------------ Step 2 ------------------------------
isa_circuit = pass_manager.run(circuit)
print(f"Gate counts: {isa_circuit.count_ops()}")
# ------------------------------ Step 3 ------------------------------
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_SQD"]
job = sampler.run([isa_circuit], shots=100_000)
primitive_result = job.result()
pub_result = primitive_result[0]
# ------------------------------ Step 4 ------------------------------
bit_array = pub_result.data.meas
num_valid = sum(
is_valid_bitstring(b, norb, nelec) for b in bit_array.get_bitstrings()
)
valid_fraction = num_valid / bit_array.num_shots
print(f"Fraction of sampled configurations that are valid: {valid_fraction}")
expected_fraction_random = (
math.comb(norb, n_alpha) * math.comb(norb, n_beta) / 2 ** (2 * norb)
)
print(
f"Expected fraction of valid configurations from uniformly random bitstrings: "
f"{expected_fraction_random}"
)
# SQD options
energy_tol = 1e-3
occupancies_tol = 1e-3
max_iterations = 5
# Eigenstate solver options
num_batches = 3
samples_per_batch = 1000
max_dim = 1000
symmetrize_spin = True
carryover_threshold = 1e-4
max_cycle = 200
# Use the Hartree-Fock configuration as an initial guess for the
# orbital occupancies
initial_occupancies = (
np.array([1] * n_alpha + [0] * (norb - n_alpha)),
np.array([1] * n_beta + [0] * (norb - n_beta)),
)
# Pass options to the built-in eigensolver. If you just want to use the defaults,
# you can omit this step, in which case you would not specify the
# sci_solver argument in the call to diagonalize_fermionic_hamiltonian below.
sci_solver = partial(solve_sci_batch, spin_sq=0.0, max_cycle=max_cycle)
# List to capture intermediate results
result_history = []
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,
max_dim=max_dim,
initial_occupancies=initial_occupancies,
carryover_threshold=carryover_threshold,
callback=callback,
seed=rng,
)
final_energy = result.energy + nuclear_repulsion_energy
energy_error = final_energy - reference_energy
print(f"Final energy: {final_energy}")
print(f"Final energy error: {energy_error}")
# Data for energies plot
x1 = range(len(result_history))
min_e = [
min(result, key=lambda res: res.energy).energy + nuclear_repulsion_energy
for result in result_history
]
e_diff = [abs(e - reference_energy) for e in min_e]
# Chemical accuracy (+/- 1.6 milli-Hartree)
chem_accuracy = 0.0016
# Data for avg spatial orbital occupancy
y2 = np.sum(result.orbital_occupancies, axis=0)
x2 = range(len(y2))
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
# Plot energies
axs[0].plot(x1, e_diff, label="energy error", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-3, 1e-1)
axs[0].axhline(
y=chem_accuracy,
color="#BF5700",
linestyle="--",
label="chemical accuracy",
)
axs[0].set_title("Approximated Ground State Energy Error vs SQD Iterations")
axs[0].set_xlabel("Iteration Index", fontdict={"fontsize": 12})
axs[0].set_ylabel("Energy Error (Ha)", fontdict={"fontsize": 12})
axs[0].legend()
# Plot orbital occupancy
axs[1].bar(x2, y2, width=0.8)
axs[1].set_xticks(x2)
axs[1].set_xticklabels(x2)
axs[1].set_title("Avg Occupancy per Spatial Orbital")
axs[1].set_xlabel("Orbital Index", fontdict={"fontsize": 12})
axs[1].set_ylabel("Avg Occupancy", fontdict={"fontsize": 12})
plt.tight_layout()
plt.show()Output:
converged SCF energy = -108.929838385609
norb = 26
nelec = (5, 5)
E(CCSD) = -109.2177884185544 E_corr = -0.2879500329450041
Using backend ibm_boston
Warning: Backend cannot accommodate pairs_ab=[(0, 0), (4, 4), (8, 8), (12, 12), (16, 16), (20, 20), (24, 24)].
Removing interaction (24, 24) from the end.
Warning: Backend cannot accommodate pairs_ab=[(0, 0), (4, 4), (8, 8), (12, 12), (16, 16), (20, 20)].
Removing interaction (20, 20) from the end.
Gate counts: OrderedDict({'sx': 7036, 'rz': 6965, 'cz': 1858, 'measure': 52, 'x': 50, 'barrier': 1})
Fraction of sampled configurations that are valid: 0.01677
Expected fraction of valid configurations from uniformly random bitstrings: 9.607888706852918e-07
Iteration 1
Subsample 0
Energy: -109.17922180754668
Subspace dimension: 1000000
Subsample 1
Energy: -109.17713541195594
Subspace dimension: 962361
Subsample 2
Energy: -109.17407767345108
Subspace dimension: 968256
Iteration 2
Subsample 0
Energy: -109.1873108379026
Subspace dimension: 1000000
Subsample 1
Energy: -109.1876970140745
Subspace dimension: 1000000
Subsample 2
Energy: -109.18255107152576
Subspace dimension: 1000000
Iteration 3
Subsample 0
Energy: -109.19052148187927
Subspace dimension: 1000000
Subsample 1
Energy: -109.19316375313544
Subspace dimension: 1000000
Subsample 2
Energy: -109.19087348337145
Subspace dimension: 1000000
Iteration 4
Subsample 0
Energy: -109.19374847699348
Subspace dimension: 1000000
Subsample 1
Energy: -109.19517039328333
Subspace dimension: 1000000
Subsample 2
Energy: -109.19422416032815
Subspace dimension: 1000000
Iteration 5
Subsample 0
Energy: -109.19580768221108
Subspace dimension: 1000000
Subsample 1
Energy: -109.19608553581007
Subspace dimension: 1000000
Subsample 2
Energy: -109.19548131833729
Subspace dimension: 1000000
Final energy: -109.19608553581007
Final energy error: 0.03194368084709254
次のステップ
この作品が興味深かったなら、以下の資料も気に入るかもしれません:
- フェルミオン格子モデルのサンプルベース・クリロフ量子対角化法 ― 変分アンサッツの代わりに時間発展回路を用いた関連チュートリアル
- DiceソルバーによるSQD化学ワークフローのスケールアップ ― 対角化に、より効率的なDiceソフトウェアを活用する方法を紹介するページ
- SQD アドオン API ドキュメント - 関
diagonalize_fermionic_hamiltonian数のリファレンス - 量子特化型スーパーコンピュータにおける厳密対角化のスケールを超えた化学計算 ― 本チュートリアルが基づく論文