水素分子のための変分量子固有値ソルバー(VQE)#

このチュートリアルでは、水素分子(H₂)の基底状態エネルギーを求めるための変分量子固有値ソルバー(VQE)アルゴリズムの実装について解説します。分子ハミルトニアンの生成にはOpenFermion、量子回路の構築にはQamomile、量子シミュレーションにはQiskitなど、さまざまな量子コンピューティングフレームワークを使用します。

ワークフローは以下の通りです:

  1. 分子ハミルトニアンを量子ビット演算子へ変換

  2. パラメータ化された量子回路(アンザッツ)の作成

  3. VQEによる最適化の実装

  4. 原子間距離ごとのエネルギー地形の解析

量子コンピューティングを用いて量子化学の問題を解く方法を示し、特にH₂分子の最小エネルギー構造の探索に焦点を当てます。

# 必要なパッケージは以下のコマンドでインストールできます
# !pip install openfermion pyscf openfermionpyscf
import matplotlib.pyplot as plt
import numpy as np
import openfermion.chem as of_chem
import openfermion.transforms as of_trans
import openfermionpyscf as of_pyscf
import qiskit as qk
import qiskit.primitives as qk_pr
from qiskit_aer import AerSimulator
from qiskit_aer.primitives import EstimatorV2
from scipy.optimize import minimize

from qamomile.core.ansatz.efficient_su2 import create_efficient_su2_circuit
from qamomile.core.circuit.drawer import plot_quantum_circuit
import qamomile.core.operator as qm_o
import qamomile.qiskit as qm_qk

水素分子のハミルトニアンの作成#

basis = "sto-3g"
multiplicity = 1
charge = 0
distance = 0.977
geometry = [["H", [0, 0, 0]], ["H", [0, 0, distance]]]
description = "tmp"
molecule = of_chem.MolecularData(geometry, basis, multiplicity, charge, description)
molecule = of_pyscf.run_pyscf(molecule, run_scf=True, run_fci=True)
n_qubit = molecule.n_qubits
n_electron = molecule.n_electrons
fermionic_hamiltonian = of_trans.get_fermion_operator(molecule.get_molecular_hamiltonian())
jw_hamiltonian = of_trans.jordan_wigner(fermionic_hamiltonian)

Qamomile ハミルトニアンへの変換#

このセクションでは、OpenFermionのハミルトニアンをQamomileフォーマットに変換する。 まず、Jordan-Wigner変換を適用してフェルミ粒子演算子を量子ビット演算子へ変換し、その後、カスタム変換関数を用いてQamomileに適したハミルトニアン表現を作成する。

def operator_to_qamomile(operators: tuple[tuple[int, str], ...]) -> qm_o.Hamiltonian:
    pauli = {"X": qm_o.X, "Y": qm_o.Y, "Z": qm_o.Z}
    H = qm_o.Hamiltonian()
    H.constant = 1.0
    for ope in operators:
        H = H * pauli[ope[1]](ope[0])
    return H

def openfermion_to_qamomile(of_h) -> qm_o.Hamiltonian:
    H = qm_o.Hamiltonian()
    for k, v in of_h.terms.items():
        if len(k) == 0:
            H.constant += v
        else:
            H += operator_to_qamomile(k) * v
    return H

hamiltonian = openfermion_to_qamomile(jw_hamiltonian)

VQE アンザッツの作成#

このセクションでは、VQEアルゴリズムのための簡単なアンザッツを作成する。 アンザッツとは、試行波動関数を準備するパラメータ付き量子回路である。 ここでは、Qamomileフレームワークを用いてアンザッツ回路を構築する。

ansatz = create_efficient_su2_circuit(
    hamiltonian.num_qubits, rotation_blocks=["ry", "rz"],
    reps=4, entanglement="linear"
)
plot_quantum_circuit(ansatz)
../../_images/4c90b48c005371704a5f44e8e663c33d4103c840b0518d84a6aae148d6f16592.png

Qiskitを用いたVQEの実行#

このセクションでは、QamomileのハミルトニアンとアンザッツをQiskitフォーマットに変換した後、Qiskit Aerシミュレータを用いてVQEを実行する。もちろん、他の量子コンピューティングフレームワークを使用することも可能である。

qk_transpiler = qm_qk.QiskitTranspiler()
qk_ansatz = qk_transpiler.transpile_circuit(ansatz)
qk_hamiltonian = qk_transpiler.transpile_hamiltonian(hamiltonian)
# !pip install qiskit-aer
cost_history = []

aer_sim = AerSimulator()
qk_circuit_transpiled_ansatz = qk.transpile(qk_ansatz, aer_sim)
estimator = EstimatorV2()

def cost_estimator(param_values):
    job = estimator.run([(qk_ansatz, qk_hamiltonian, param_values)])
    result = job.result()[0]
    cost = result.data["evs"]
    return cost

def cost_callback(param_values):
    cost_history.append(cost_estimator(param_values))

initial_params = np.random.uniform(0, np.pi, len(qk_ansatz.parameters))

# QAOA最適化を実行する
result = minimize(
    cost_estimator,
    initial_params,
    method="BFGS",
    options={"disp": True, "maxiter": 50, "gtol": 1e-6},
    callback=cost_callback
)
print(result)
         Current function value: -1.105868
         Iterations: 50
         Function evaluations: 2091
         Gradient evaluations: 51
  message: Maximum number of iterations has been exceeded.
  success: False
   status: 1
      fun: -1.1058675315265363
        x: [ 3.157e+00  2.124e+00 ...  1.591e+00  4.345e-01]
      nit: 50
      jac: [ 5.545e-05  1.662e-04 ...  5.196e-04 -2.392e-05]
 hess_inv: [[ 6.494e+00 -2.618e+00 ...  1.032e+01 -6.820e-01]
            [-2.618e+00  9.130e+00 ... -5.761e+00  3.409e+00]
            ...
            [ 1.032e+01 -5.761e+00 ...  2.391e+01 -1.763e+00]
            [-6.820e-01  3.409e+00 ... -1.763e+00  2.796e+00]]
     nfev: 2091
     njev: 51
/Users/yuyamashiro/Library/Caches/pypoetry/virtualenvs/qamomile-s0Pfpxir-py3.10/lib/python3.10/site-packages/scipy/optimize/_minimize.py:726: OptimizeWarning: Maximum number of iterations has been exceeded.
  res = _minimize_bfgs(fun, x0, args, jac, callback, **options)
plt.plot(cost_history)
plt.plot(
    range(len(cost_history)),
    [molecule.fci_energy] * len(cost_history),
    linestyle="dashed",
    color="black",
    label="Exact Solution",
)
plt.legend()
plt.show()
../../_images/1e3ad13830f656ee24d176671e6c303b3764d09c34626c6bc17675fa8cc6ebf1.png

原子同士の距離を変更する#

def hydrogen_molecule(bond_length):
    basis = "sto-3g"
    multiplicity = 1
    charge = 0
    geometry = [["H", [0, 0, 0]], ["H", [0, 0, bond_length]]]
    description = "tmp"
    molecule = of_chem.MolecularData(geometry, basis, multiplicity, charge, description)
    molecule = of_pyscf.run_pyscf(molecule, run_scf=True, run_fci=True)
    n_qubit = molecule.n_qubits
    n_electron = molecule.n_electrons
    fermionic_hamiltonian = of_trans.get_fermion_operator(
        molecule.get_molecular_hamiltonian()
    )
    jw_hamiltonian = of_trans.jordan_wigner(fermionic_hamiltonian)
    return openfermion_to_qamomile(jw_hamiltonian), molecule.fci_energy

bond_lengths = np.linspace(0.2, 1.5, 15)
energies = []
for bond_length in bond_lengths:
    hamiltonian, fci_energy = hydrogen_molecule(bond_length)
    
    ansatz = create_efficient_su2_circuit(
        hamiltonian.num_qubits, rotation_blocks=["ry", "rz"],
        reps=4, entanglement="linear"
    )

    qk_ansatz = qk_transpiler.transpile_circuit(ansatz)
    qk_hamiltonian = qk_transpiler.transpile_hamiltonian(hamiltonian)

    cost_history = []
    initial_params = np.random.uniform(0, np.pi, len(qk_ansatz.parameters))
    result = minimize(
        cost_estimator,
        initial_params,
        method="BFGS",
        options={"maxiter": 50, "gtol": 1e-6},
    )

    energies.append(result.fun)

    print("distance: ", bond_length, "energy: ", result.fun, "fci_energy: ", fci_energy)
distance:  0.2 energy:  0.15754119488608387 fci_energy:  0.15748213479836348
distance:  0.29285714285714287 energy:  -0.5679350331466697 fci_energy:  -0.5679447209710022
distance:  0.38571428571428573 energy:  -0.8833020521044717 fci_energy:  -0.8833596636183383
distance:  0.4785714285714286 energy:  -1.0335991644424345 fci_energy:  -1.0336011797110967
distance:  0.5714285714285714 energy:  -1.1035710721430323 fci_energy:  -1.1042094222435161
distance:  0.6642857142857144 energy:  -1.1322058758760072 fci_energy:  -1.132350882707551
distance:  0.7571428571428571 energy:  -1.136784862771528 fci_energy:  -1.1369026717971324
distance:  0.8500000000000001 energy:  -1.1281267827080066 fci_energy:  -1.1283618784581124
distance:  0.9428571428571428 energy:  -1.1125670649192354 fci_energy:  -1.1127252078468768
distance:  1.0357142857142858 energy:  -1.093269672823594 fci_energy:  -1.0934760882294043
distance:  1.1285714285714286 energy:  -1.0725622262836512 fci_energy:  -1.0727578805453502
distance:  1.2214285714285713 energy:  -1.0519588130652564 fci_energy:  -1.0520081621708446
distance:  1.3142857142857143 energy:  -1.0322273864176297 fci_energy:  -1.032240030624708
distance:  1.4071428571428573 energy:  -1.0137141437663768 fci_energy:  -1.014147058669549
distance:  1.5 energy:  -0.9978997023749084 fci_energy:  -0.9981493534714101
plt.plot(bond_lengths, energies, "-o")
plt.xlabel("Distance")
plt.ylabel("Energy")
plt.show()
../../_images/a1b5048a5b97f74da1d7b1145168d4b7180d112059626e0ee5fa651c8928ad97.png