JuliaでOpenFermionを呼び出してVQE計算してみた

"Pythonのように書けてCのように動く"というのが売り文句に惹かれて、Juliaでいくつか簡単な量子計算を実装してみました。順次公開予定。まずはVQEです。
VQEの計算部分は[湊さんの実装[1]](https://blueqat.com/yuichiro_minato2/bc831bea-46dc-41b8-946f-9aa34dcd19c7)と大きく変わりませんが、今回はハミルトニアンを天下り的に用意するのではなく、電子積分の計算、第二量子化、Jordan-Wigner変換をしてqubitで扱える形に変換するところまでを[OpenFermion[2]](https://arxiv.org/abs/1710.07629)を用いて行いました。イケてるからJuliaを使っているというのに、結局Pythonのライブラリを読み込んで使っていることになりますが…。使えるものは使っていきましょう。
 対象は水素分子です。[Quantum Native Dojo[3]](https://dojo.qulacs.org/ja/latest/notebooks/6.2_qulacs_VQE.html)を参考にさせていただきました。

# PyCallでOpenFermionをインポート
JuliaでPythonのライブラリを使うために、PyCallを使います。`@pyimport`でopenfermionを読み込みます。ついでにクロネッカー積の演算子も定義します。

```julia
using PyCall, LinearAlgebra, Random, Optim, PyPlot
@pyimport openfermion
@pyimport openfermionpyscf
⊗(A,B) = kron(A,B)
```

# ハミルトニアンの計算&変換
[Quantum Native Dojo[3]](https://dojo.qulacs.org/ja/latest/notebooks/6.2_qulacs_VQE.html)の計算を使わせていただきました。

```julia
basis = "sto-3g"
multiplicity = 1
charge = 0
distance  = 0.9
geometry = [["H", [0,0,0]],["H", [0,0,distance]]]
description  = "H2"
molecule = openfermion.hamiltonians.MolecularData(geometry, basis, multiplicity, charge, description)
molecule = openfermionpyscf.run_pyscf(molecule,run_scf=1,run_fci=1)
N = molecule.n_qubits
n_electron = molecule.n_electrons
fermionic_hamiltonian = openfermion.transforms.get_fermion_operator(molecule.get_molecular_hamiltonian())
jw_hamiltonian_py = openfermion.transforms.jordan_wigner(fermionic_hamiltonian)
```

output:

```
PyObject (-0.25905412221337204+0j) [] +
(-0.04764292343981623+0j) [X0 X1 Y2 Y3] +
(0.04764292343981623+0j) [X0 Y1 Y2 X3] +
(0.04764292343981623+0j) [Y0 X1 X2 Y3] +
(-0.04764292343981623+0j) [Y0 Y1 X2 X3] +
(0.14907478844731503+0j) [Z0] +
(0.1611381637816487+0j) [Z0 Z1] +
(0.11162723403394147+0j) [Z0 Z2] +
(0.15927015747375767+0j) [Z0 Z3] +
(0.1490747884473151+0j) [Z1] +
(0.15927015747375767+0j) [Z1 Z2] +
(0.11162723403394147+0j) [Z1 Z3] +
(-0.16071249108067315+0j) [Z2] +
(0.167371259483041+0j) [Z2 Z3] +
(-0.16071249108067315+0j) [Z3]
```

Jordan-Wigner変換後のハミルトニアンが得られました。これで各qubitの|1>と|0>が各スピン軌道の占有・非占有に対応することになります。

# VQEで使う関数の定義
(1ビット)回転行列やCNOTのような量子ゲートと、ansatz生成、期待値計算、コスト関数を定義します。ansatz回路は以下のものを使います[(参考[4])](https://www.amazon.co.jp/量子コンピュータによる量子化学計算入門-KS化学専門書-杉﨑-研司/dp/4065218276)。

![use2550.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960698/8bbd1a1a-4700-ef5a-999d-01f3a16452a2.png)


```julia
#1量子ビット回転ゲート
function R_x(theta::Float64)
    return [cos(theta/2) -sin(theta/2)im; -sin(theta/2)im cos(theta/2)]
end
function R_z(theta::Float64)
    return [exp(-theta/2*im) 0; 0 exp(theta/2*im)]
end

#CNOT_i,j
function CNOT(control::Int64, target::Int64)
    CNOT_ide = 1
    CNOT_X = 1
    for i=1:N
        if i==control
            CNOT_ide = CNOT_ide ⊗ [1.0 0.0; 0.0 0.0]
            CNOT_X = CNOT_X ⊗ [0.0 0.0; 0.0 1.0]
        elseif i==target
            CNOT_ide = CNOT_ide ⊗ [1.0 0.0; 0.0 1.0]
            CNOT_X = CNOT_X ⊗ [0.0 1.0; 1.0 0.0]
        else
            CNOT_ide = CNOT_ide ⊗ [1.0 0.0; 0.0 1.0]
            CNOT_X = CNOT_X ⊗ [1.0 0.0; 0.0 1.0]
        end
    end
    return CNOT_ide + CNOT_X
end

#初期状態 |000…>を生成
function get_initial_state(nqubits::Int64)
    state = 1
    for i=1:nqubits
        state = state ⊗ [1.0, 0.0]
    end
    return state
end

#ansatz生成
function ansatz(theta_list::Array{Float64,3})
    state = get_initial_state(N)
    for i=1:size(theta_list,3), j=1:size(theta_list,2)
        gate = 1
        for k=1:size(theta_list,1)
            if j == 2
                gate = gate ⊗ R_x(theta_list[k,j,i])
            else
                gate = gate ⊗ R_z(theta_list[k,j,i])
            end
        end
        state = gate * state
        for j=1:N-1
            state = CNOT(j,j+1) * state
        end
    end
    return state
end

#期待値の計算
function get_expectation_value(state::Array{Complex{Float64},1}, observable::Array{Float64,2})
    return real(dot(state, observable*state))
end

#最適化計算用のコスト関数
function cost(theta_list::Array{Float64,3})
    return get_expectation_value(ansatz(theta_list), Hamiltonian)
end
```

# 計算実行
ハミルトニアンに使う$X_i, Y_i, Z_i\;(\mathrm{dim}(X_i=2^N$))ゲートの定義して、OpenFermionで得たハミルトニアンを取り出して行列形式への変換を行います。attributesについて詳しくは[OpenFemionのドキュメント[5]](https://openfermion.readthedocs.io/_/downloads/en/latest/pdf/)を確認してください(SymbolicOperatorクラス)。
最適化計算は[Optim.jl[6]](https://julianlsolvers.github.io/Optim.jl/stable/)を使います。　

```julia
layer = 3
itr = 3

#Pauliゲートを定義(X[i]=X_i)
X = [Matrix(1.0I,1,1) for i in 1:N]
Y = [Matrix{Complex}(1.0I,1,1) for i in 1:N]
Z = [Matrix(1.0I,1,1) for i in 1:N]
for i=1:N, j=1:N
    if i==j
        X[i] = X[i] ⊗ [0.0 1.0; 1.0 0.0]
        Y[i] = Y[i] ⊗ [0.0 -1.0im; 1.0im 0.0]
        Z[i] = Z[i] ⊗ [1.0 0.0; 0.0 -1.0]
    else
        X[i] = X[i] ⊗ Matrix(1.0I,2,2)
        Y[i] = Y[i] ⊗ Matrix(1.0I,2,2)
        Z[i] = Z[i] ⊗ Matrix(1.0I,2,2)
    end
end

Hamiltonian = zeros(2^N,2^N)

#OpenFermion形式のハミルトニアンを行列に変換
for (pauli,coefficient) in (jw_hamiltonian_py[:terms])
    if length(pauli)==0
        Hamiltonian += real(coefficient) * Matrix(1.0I,2^N,2^N)
        continue
    end
    pauli_gates = 1
    for i=1:length(pauli)
        if pauli[i][2] == "X"
            pauli_gates = pauli_gates * X[(pauli[i][1]+1)]
        elseif pauli[i][2] == "Y"
            pauli_gates = pauli_gates * Y[(pauli[i][1]+1)]
        elseif pauli[i][2] == "Z"
            pauli_gates = pauli_gates * Z[(pauli[i][1]+1)]
        else
            println("something went wrong…")
        end
    end
    pauli_gates = real(coefficient) * pauli_gates
    Hamiltonian += pauli_gates
end
Hamiltonian = real(Hamiltonian)

#θの初期値をランダムに決定し、最適化計算を実行
rng = MersenneTwister(4838)
theta_list = rand(rng, N,layer,itr)*2pi
res = optimize(cost, theta_list, iterations=10000, store_trace=true)
plot(Optim.f_trace(res))
plt.xlabel("Iteration")
plt.ylabel("Energy (Hartree)")
```

output:
![vqe_energy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960698/b29da35a-9c09-b9e0-042f-d6c11982b89d.png)

```julia
println(minimum(res))
```

output:

```julia
-1.1205602587271528
```

エネルギーが求められました。ハミルトニアンを直接対角化して、エネルギーが一致するか見てみましょう。juliaでは簡単にできますね。

```julia
eigvals(Hamiltonian)
```

output:

```julia
16-element Array{Float64,1}:
 -1.120560281299988
 -0.6828493923576944
 -0.6828493923576942
 -0.6828493923576939
 -0.5742460086733262
 -0.5742460086733259
 -0.5634367948093946
 -0.5634367948093945
 -0.30170600483916443
  0.032862358979865766
  0.03286235897986578
  0.0686039556493665
  0.0686039556493665
  0.1758813173718267
  0.5879746787999998
  0.634525489333432
```

VQEの結果とよく一致しています。


# 参考文献
[[1]Juliaでお手製VQEを実行](https://blueqat.com/yuichiro_minato2/bc831bea-46dc-41b8-946f-9aa34dcd19c7)
[[2]OpenFermion: The Electronic Structure Package for Quantum Computers, arXiv:1710.07629 [quant-ph]](https://arxiv.org/abs/1710.07629)
[[3]6-1. OpenFermionの使い方 (Quantum Native Dojo)](https://dojo.qulacs.org/ja/latest/notebooks/6.1_openfermion_basics.html)
[[4]量子コンピュータによる量子化学計算入門](https://www.amazon.co.jp/量子コンピュータによる量子化学計算入門-KS化学専門書-杉﨑-研司/dp/4065218276)
[[5] Optim.jl](https://julianlsolvers.github.io/Optim.jl/stable/)