Skip to main content
IBM Quantum Platform

化学ハミルトニアンのサンプル・ベースの量子対角化

使用時間の見積もり:Heron r2 プロセッサーで1分未満(注:これはあくまでも見積もりです。 ランタイムは異なるかもしれない)。


学習成果

このチュートリアルを終えた後、ユーザーは以下の点を理解できるようになります:

  • SQD Qiskit アドオンを使用して、量子処理ユニット(QPU)からサンプリングしたビット列を用いて、分子系の基底状態エネルギーを近似する方法。
  • ffsim を使用して、量子化学シミュレーション用の局所ユニタリークラスター・ジャストロウ(LUCJ)回路を構築する方法。

前提条件

このチュートリアルを進める前に、以下のトピックについて事前に確認しておくことをお勧めします:

  • 量子化学と第二量子化
  • サンプラープリミティブを使用した量子回路からのサンプリング

背景

このチュートリアルでは、 SQD Qiskit アドオンを使用してサンプルベース量子対角化(SQD)アルゴリズムを実装し、ノイズを含む量子サンプルを後処理して、平衡結合長における窒素分子 N2\text{N}_2 の基底状態を近似する方法を解説します。 このソフトウェアの詳細については、対応するドキュメントに記載されています。そこには、使い始めるための簡単な例も掲載されています。

このチュートリアルは、量子化学、特に分子の基底状態のエネルギーを求める方法に精通しているユーザーにおすすめです。 ワークフローの詳細な手順については、 「量子対角化アルゴリズム」コースを参照してください。

SQDは、量子計算と分散型古典計算を組み合わせて、量子系のハミルトニアンなどの量子演算子の固有値と固有ベクトルを求める手法である。 従来の分散計算は、量子プロセッサから得られたサンプルを処理し、それらが張る部分空間において対象のハミルトニアンを射影および対角化するために用いられる。 SQD ベースのワークフローには、以下の手順が含まれます:

  1. 回路アナザッツを選び、量子コンピュータ上で参照状態(この場合はハートリーフォック状態)に適用する。
  2. 出来上がった量子状態からビット列をサンプリングする。
  3. ビット列に対して自己一貫性のある構成復元手順を実行し、基底状態の近似を求める。

SQDは、対象となる固有状態がスパースである場合にうまく機能することが知られています。波動関数は基底状態のセット S={x}\mathcal{S} = \{|x\rangle \}、そのサイズは問題の大きさに対して指数関数的に増加することはありません。

量子化学

分子系のハミルトニアンは次のように書ける

H^=prσhpra^pσa^rσ+12prqsστhprqsa^pσa^qτa^sτa^rσ,\hat{H} = \sum_{ \substack{pr\\\sigma} } h_{pr} \, \hat{a}^\dagger_{p\sigma} \hat{a}_{r\sigma} + \frac12 \sum_{ \substack{prqs\\\sigma\tau} } h_{prqs} \, \hat{a}^\dagger_{p\sigma} \hat{a}^\dagger_{q\tau} \hat{a}_{s\tau} \hat{a}_{r\sigma},

ここで、 hprh_{pr}hprqsh_{prqs} は分子積分と呼ばれる複素数で、コンピュータ・プログラムを使って分子の仕様から計算できる。 このチュートリアルでは PySCF ソフトウェアパッケージを使用して積分を計算します。

分子ハミルトニアンの導出方法の詳細については、量子化学の教科書(例えば、SzaboとOstlund著『 Modern Quantum Chemistry 』)を参照されたい。 量子化学の問題がどのように量子コンピュータにマッピングされるかについての高レベルの説明は、Qiskit Global Summer School 2024の講義 Mapping Problems to Qubitsをご覧ください。

局所ユニタリークラスター・ジャストロー(LUCJ)近似

SQDでは、サンプルを抽出するための量子回路のアンザッツが必要です。 このチュートリアルでは、物理的な根拠とハードウェアへの適応性を兼ね備えていることから、 ローカルユニタリークラスター・ジャストロウ(LUCJ) アンザッツを用います。 ffsim を使用して、近似回路を構築します。

LUCJアプローチは、量子ビット間の接続性が制限されているQPUにも適応する。 スピン軌道は、SWAPゲートによるルーティングを必要としないような形で量子ビットにマッピングされる。 IBM® このハードウェアは、ヘビー・ヘックス格子型の量子ビットトポロジーを採用しており、その場合は、下図に示すような「ジグザグ」パターンを採用することができます。 このパターンでは、同じスピンを持つ軌道は線形トポロジーを持つ量子ビット(赤と青の円)にマッピングされ、異なるスピンを持つ軌道間の接続は空間上の4つおきの軌道に存在し、その接続は補助量子ビット(紫の円)によって実現されている。

重ヘックス格子上のLUCJアサッツのQubit写像図

自己整合的な構成復旧

自己無撞着構成回復手順は、ノイズの多い量子サンプルから可能な限り多くの信号を抽出するように設計されている。 分子ハミルトニアンは粒子数とスピンZを保存するので、これらの対称性も保存する回路アサッツを選ぶことは理にかなっている。 ハートリーフォック状態に適用すると、結果として得られる状態は、ノイズのない設定において固定された粒子数とスピンZを持つ。 したがって、この状態からサンプリングされたビット列のスピン α\alpha とスピン β\beta の半分は、ハートリーフォック状態と同じハミングウェイトを持つはずである。 現在の量子プロセッサーにはノイズが存在するため、測定されたビット列の中にはこの性質に反するものがある。 単純なポストセレクションでは、これらのビット列は破棄されるが、これは無駄である。 自己無撞着な回復手順は、後処理でその信号の一部を回復しようとする。 この手順は反復的であり、基底状態における各軌道の平均占有率の推定値を入力として必要とする。 この手順はループで実行され、各反復には以下のステップがある:

  1. 指定された対称性に違反する各ビット列について、ビット列を平均軌道占有率の現在の推定値に近づけるように設計された確率的手順でビットを反転させ、新しいビット列を得る。
  2. 対称性を満たす新旧のビット列をすべて集め、あらかじめ選んだ固定サイズの部分集合をサブサンプルする。
  3. ビット列の各サブセットについて、ハミルトニアンを対応する基底ベクトル(これらの基底ベクトルの説明については前節を参照)によってスパンされる部分空間に射影し、古典コンピュータ上で射影されたハミルトニアンの基底状態推定値を計算する。
  4. 平均軌道占有率の推定値を、最もエネルギーの低い基底状態の推定値で更新する。

SQDワークフロー図

SQDのワークフローは以下の図のようになっている:

SQDアルゴリズムのワークフロー図

要件

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

  • Qiskit SDK v1.0 またはそれ以降、 可視化サポート付き
  • Qiskit Runtime v0.22 またはそれ以降 (pip install qiskit-ibm-runtime)
  • SQD Qiskit アドオン v0.11 以降 (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)], ["N", (1.0, 0, 0)]],
    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計算を行う。 この計算で得られた t1t_1t2t_2 の振幅は、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.t2

Output:

E(CCSD) = -108.5933309085008  E_corr = -0.1283731437052354

ここで、 ffsim を使用してアンザッツ回路を作成します。 我々の分子は閉殻のハートリー・フォック状態を持つため、UCJアンザッツのスピン平衡型、すなわち UCJOpSpinBalanced を用いる。 t2t_2 の振幅の「圧縮」二重因数分解を有効にするため、method from_t_amplitudes メソッドに compressed を設定 optimize=True しました(詳細については、ffsimのドキュメントにある 「The local unitary cluster Jastrow (LUCJ) ansatz」 を参照してください)。

LUCJアンザッツはQPUの利用可能な接続性に適応するため、アンザッツを作成する前にQPUバックエンドを初期化する必要があります。 とりあえず、ヘビー・ヘックス結合マップと、LUCJアンザッツによって自然に分解されるゲートセットを持つ汎用的なバックエンドを作成します。 次に、 LUCJアプローチに関する背景セクションで説明した「ジグザグ」レイアウトに従って、LUCJアプローチを指定されたバックエンドへトランスパイルするための専用パスマネージャーを作成 ffsim.qiskit.generate_lucj_pass_manager します。 この関数は、選択されたレイアウトに関連する誤差を最小化するためにスコアリング・ヒューリスティックを使用します。これは、バックエンドが実際のQPUである場合や、ノイズモデルを備えたシミュレータである場合に重要です。 この関数は、パスマネージャーを返すだけでなく、ハードウェア上で実装可能なアルファ・ベータ結合ペアも返します。 すべてのペアを実装できない場合、警告が出力されます。

import warnings

from qiskit.transpiler import CouplingMap

warnings.formatwarning = lambda msg, *args, **kwargs: f"Warning: {msg}\n"

# 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
coupling_map = CouplingMap.from_heavy_hex(3)
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="heavy-hex",
    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,
    # Limit the number of optimization iterations to prevent the code cell
    # from running too long. Removing this line may improve results.
    options=dict(maxiter=1000),
)

# 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': 86, 'p': 16, 'measure': 16, 'cp': 15, 'x': 10, 'swap': 2, 'barrier': 1})

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

ハードウェア実行向けに回路を最適化した後、ターゲットハードウェア上で実行し、基底状態のエネルギー推定のためのサンプルを収集する準備が整いました。 回路は1つしかないため、 IBM Quantum の「Compute Service」 ジョブ実行モードを使用して、この回路を実行します。

rng = np.random.default_rng()
sampler = StatevectorSampler(seed=rng)
job = sampler.run([isa_circuit], shots=100_000)

Output:

Warning: Trying to add QuantumRegister to a QuantumCircuit having a layout
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 = 300
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,
    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.59275573641656
		Subspace dimension: 900
	Subsample 1
		Energy: -108.59275573641656
		Subspace dimension: 900
	Subsample 2
		Energy: -108.59275573641656
		Subspace dimension: 900
Iteration 2
	Subsample 0
		Energy: -108.59275573641656
		Subspace dimension: 900
	Subsample 1
		Energy: -108.59275573641656
		Subspace dimension: 900
	Subsample 2
		Energy: -108.59275573641656
		Subspace dimension: 900
Final energy: -108.59275573641656
Final energy error: 0.0032316145694579745

結果を可視化する

最初のグラフは、このシミュレーションにおいて、最初の反復計算の後ですでに正確な解 1 mH に極めて近い値を得ていることを示しています(化学的精度は通常、 1 kcal/mol\approx 程度と 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]
yt1 = [1.0, 1e-1, 1e-2, 1e-3, 1e-4]

# Chemical accuracy (+/- 1 milli-Hartree)
chem_accuracy = 0.001

# 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_yticks(yt1)
axs[0].set_yticklabels(yt1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-4)
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:

Output of the previous code cell

大規模なハードウェアの例

それでは、実際の量子ハードウェアを使って、より大規模な例を実行してみましょう。 ここでは、 cc-pVDZ 基底関数セットを用いて、窒素分子のアクティブ空間を導出する。

手順 1~4

ここでは、これらすべての手順を統合して大規模な単一のワークフローとし、それを実際の量子ハードウェア上で実行します。

# ------------------------------ Step 1 ------------------------------
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
    atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]],
    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, min_num_qubits=133
)
print(f"Using backend {backend.name}")

# Create pass manager
pass_manager, pairs_ab = ffsim.qiskit.generate_lucj_pass_manager(
    backend=backend,
    norb=norb,
    connectivity="heavy-hex",
    interaction_pairs=(pairs_aa, pairs_ab),
    optimization_level=3,
)

# Create the 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,
    # Limit the number of optimization iterations to prevent the code cell
    # from running too long. Removing this line may improve results.
    options=dict(maxiter=1000),
)

# 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 = 300
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,
    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]
yt1 = [1.0, 1e-1, 1e-2, 1e-3, 1e-4]

# Chemical accuracy (+/- 1 milli-Hartree)
chem_accuracy = 0.001

# 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_yticks(yt1)
axs[0].set_yticklabels(yt1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-4)
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.2879500329450045
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': 7039, 'rz': 6990, 'cz': 1858, 'x': 61, 'measure': 52, 'barrier': 1})
Fraction of sampled configurations that are valid: 0.02124
Expected fraction of valid configurations from uniformly random bitstrings: 9.607888706852918e-07
Iteration 1
	Subsample 0
		Energy: -109.13889134249762
		Subspace dimension: 120409
	Subsample 1
		Energy: -109.11785470455858
		Subspace dimension: 110889
	Subsample 2
		Energy: -109.13234360554011
		Subspace dimension: 130321
Iteration 2
	Subsample 0
		Energy: -109.16392179579177
		Subspace dimension: 223729
	Subsample 1
		Energy: -109.16281938332986
		Subspace dimension: 223729
	Subsample 2
		Energy: -109.16955816711932
		Subspace dimension: 233289
Iteration 3
	Subsample 0
		Energy: -109.17905772999075
		Subspace dimension: 324900
	Subsample 1
		Energy: -109.17532445048462
		Subspace dimension: 357604
	Subsample 2
		Energy: -109.1733168689756
		Subspace dimension: 348100
Iteration 4
	Subsample 0
		Energy: -109.18437778820451
		Subspace dimension: 474721
	Subsample 1
		Energy: -109.18450164209159
		Subspace dimension: 476100
	Subsample 2
		Energy: -109.18493571190754
		Subspace dimension: 487204
Iteration 5
	Subsample 0
		Energy: -109.18616522497996
		Subspace dimension: 622521
	Subsample 1
		Energy: -109.18652868888333
		Subspace dimension: 644809
	Subsample 2
		Energy: -109.18753326484406
		Subspace dimension: 585225
Final energy: -109.18753326484406
Final energy error: 0.040495951813099396
Output of the previous code cell

次のステップ

推奨事項

この作品が興味深かったなら、以下の資料も気に入るかもしれません:

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