クリロフ量子対角化
クリロフ量子対角化(KQD)についてのこのレッスンでは、以下のことにお答えします:
- 一般的にクリロフ法とは何ですか?
- なぜクリロフ法が有効なのか、どのような条件下で有効なのか?
- 量子コンピューターはどのような役割を果たしているのか?
量子計算は文献 [1] に基づいている。
以下のビデオは、古典コンピューティングにおけるクリロフ法の概要、その使用動機、そして量子コンピューティングがそのワークストリームでどのような役割を果たせるかを説明している。 続くテキストでは、より詳細な説明があり、クリロフ法を古典的に、また量子コンピューターを用いて実装している。
1. クリロフ法の導入
クリロフ部分空間法は、 クリロフ部分空間と呼ばれるものを中心に構築されたいくつかの手法のいずれかを指す。 これらの完全なレビューはこのレッスンの範囲外であるが、参考文献 [2-4] はすべて、より実質的な背景を与えることができる。 ここでは、クリロフ部分空間とは何か、それが固有値問題を解く上でどのように、そしてなぜ有用なのか、そして最後に量子コンピューター上でどのように実装できるのかに焦点を当てる。
定義対称的な正の半定値 行列 が与えられたとき,次数 のクリロフ空間 は,行列 の高次のべき乗 までを参照ベクトル と掛け合わせることによって得られるベクトルによってスパンされる空間である.
上記のベクトルはクリロフ部分空間と呼ばれるものにまたがっているが、直交していると考える理由はない。 多くの場合、 グラム・シュミット直交化と同様の反復直交化処理を用いる。 ここでは、それぞれの新しいベクトルが生成されるときに他のベクトルに対して直交するように作られているため、プロセスが若干異なる。 この文脈では、これをアーノルディ反復と呼ぶ。 最初のベクトル から始めて、次のベクトル を生成し、 への投影を減算することによって、この2番目のベクトルが最初のベクトルと直交することを保証する。つまり
であることが、今や容易に理解できる。なぜなら、
次のベクトルも同様に、前の2つのベクトルと直交するようにする:
このプロセスをすべてのベクトル( )について繰り返すと、クリロフ空間の完全な正規直交基底ができる。 直交ベクトルは必然的に全空間にまたがるので、ここでの直交化処理は一度 、ゼロになることに注意。 また、 の固有ベクトルとなるベクトルがあれば、それ以降のベクトルはすべてそのベクトルの倍数となるため、この処理はゼロとなる。
1.1 簡単な例:手計算によるクリロフ
ここで、些細に小さな行列に対するクリロフ部分空間の生成の過程を、ステップを踏んで見てみよう。 の初期行列から始める:
この小さな例では、手作業でも簡単に固有ベクトルと固有値を求めることができる。 ここでは数値解を示す。
# One might use linalg.eigh here, but later matrices may not be Hermitian. So we use
# linalg.eig in this lesson.
import numpy as np
A = np.array([[4, -1, 0], [-1, 4, -1], [0, -1, 4]])
eigenvalues, eigenvectors = np.linalg.eig(A)
print("The eigenvalues are ", eigenvalues)
print("The eigenvectors are ", eigenvectors)Output:
The eigenvalues are [2.58578644 4. 5.41421356]
The eigenvectors are [[ 5.00000000e-01 -7.07106781e-01 5.00000000e-01]
[ 7.07106781e-01 1.37464400e-16 -7.07106781e-01]
[ 5.00000000e-01 7.07106781e-01 5.00000000e-01]]
後で比較するために、ここに記録しておく:
クリロフ部分空間 の次元を大きくするにつれて、このプロセスがどのように機能するか(あるいは機能しないか)を研究したい。そのために、このプロセスを適用する:
- ランダムに選ばれたベクトル (上記のようにすでに正規化されている場合は )と呼ぶ)から始まる完全なベクトル空間の部分空間を生成する。
- その部分空間に完全行列 を射影し,その射影行列 の固有値を求める.
- グラム・シュミット直交化に似たプロセスで、正規直交であることを保証しながら、より多くのベクトルを生成して部分空間のサイズを大きくする。
- をより大きな部分空間に投影し、その結果得られる行列の固有値 を求める。
- これを固有値が収束するまで(あるいはこのおもちゃのケースでは、元の行列 の全ベクトル空間にまたがるベクトルが生成されるまで)繰り返す。
クリロフ法の通常の実装では、クリロフ部分空間を構築するたびに、その部分空間に射影された行列の固有値問題を解く必要はない。 希望する次元の部分空間を構成し、その部分空間に行列を射影し、射影行列を対角化することができる。 各サブスペース次元での投影と対角化は、収束をチェックするためだけに行われる。
の寸法:
ランダムなベクトルを選ぶ
まだ正規化されていない場合は、正規化する。
ここで、行列 をこの1つのベクトルの部分空間に射影する:
これは、クリロフ部分空間への行列の射影であり、この部分空間にはベクトル が1つだけ含まれている。この行列の固有値はトリビュラルに4である。 これは、 の固有値(この場合は1つだけ)の0次推定値と考えることができる。稚拙な推定値ではあるが、桁としては正しい。
の寸法:
、前のベクトルに対して次のベクトルを生成する:
ここで、直交性を確保するために、このベクトルの前のベクトルへの射影を差し引く。
まだ正規化されていない場合は、正規化する。 この場合、ベクトルはすでに正規化されている
ここで、行列Aをこれら2つのベクトルの部分空間に射影する:
この行列の固有値を求める問題が残っている。 しかし、このマトリックスはフルマトリックスよりもわずかに小さい。 非常に大きな行列を含む問題では、この小さな部分空間で作業することが非常に有利になることがある。
これはまだ良い推定値とは言えないが、0次推定値よりは良い。 プロセスが明確であることを確認するため、もう1度、この反復を実施する。 なぜなら、次の反復で 3x3 行列を対角化することになり、時間も計算量も節約できないからである。
の寸法:
ここで、前のベクトルに対してAを操作して、部分空間の次のベクトルを生成する:
ここで、直交性を確保するために、このベクトルを前のベクトル両方に射影したものを差し引く。
まだ正規化されていない場合は、正規化する。 この場合、ベクトルはすでに正規化されている
ここで、行列 をこれらのベクトルの部分空間に射影する:
ここで固有値を決定する:
これらの固有値は、まさに元の行列 の固有値です。クリロフ部分空間は,元の行列 のベクトル空間全体にまたがるように拡張されているので,このようになるに違いない.
この例では、クリロフ法は直接対角化よりも特に簡単には見えないかもしれない。 実際、後の節で述べるように、クリロフ法が有利なのはある行列次元以上だけである。これは、非常に大きな行列の固有値/固有ベクトル問題を解くのに役立つことを目的としている。
これは "手で "作業した唯一の例であるが、以下のセクション2には計算例を示す。
用語の明確化
よくある誤解は、与えられた問題に対するクリロフ部分空間は1つしかないというものだ。 しかしもちろん、我々の行列を適用できる初期ベクトルは多数存在するので、可能なクリロフ部分空間も多数存在する。 クリロフ部分空間 」という表現は、特定の例についてすでに定義された特定のクリロフ部分空間を指す場合にのみ使用する。 一般的な問題解決アプローチについては、「a クリロフ部分空間」を参照する。 最後に明確にしておきたいのは、"クリロフ空間 "という言い方が有効だということだ。 初期空間から部分空間への行列の射影という文脈で使われるため、「クリロフ部分空間 」と呼ばれることも多い。 その文脈に沿って、ここではほとんど亜空間と呼ぶことにする。
理解度チェック
なぜ(a)クリロフ部分空間 の次元を、注目する行列の次元 以上に拡張することが有用でなく、(b)不可能なのかを説明せよ。
(a) ベクトルを生成する際に直交正規化を行っているため、 個のそのようなベクトルの集合は完全基底を成す。つまり、それらの線形結合を用いることで、その空間内の任意のベクトルを生成することができる。
(b) 直交化のプロセスは、新しいベクトルのすべての先行するベクトルへの射影を差し引くことから成る。 もし、それまでのすべてのベクトルがベクトル空間全体を張っているならば、部分空間全体への射影を引いても、常に零ベクトルが残ることになる。
ある研究者が、小さなおもちゃのような行列にクリロフ法を適用して実演しているとしよう。 行列 と初期ベクトル の選択に何か問題があるのでしょうか?
および
あなたの同僚は誤って初期ベクトルに固有ベクトルを選んでしまった。 初期ベクトルに対して行列を作用させると、同じベクトルを固有値でスケーリングして返すだけである。 これでは、次元が増加する部分空間は生成されない。 同僚に、異なる初期ベクトルを選ぶように助言し、それが固有ベクトルでないことを確認する。
与えられた行列にクリロフ法を適用し、適切な新しい初期ベクトルを選択する。 あなたのクリロフ部分空間の0次および1次における最小固有値の推定値を書き出してください。
初期ベクトルの選択によって、多くの答えが考えられる。 私たちはそれを選ぶ:
を得るには、 に を一度適用し、次に を に対して直交させる。
0次では、クリロフ部分空間への射影は次のようになる
1次では、このクリロフ部分空間への射影は次のようになる
これは手作業でもできるが、numpyを使うのが最も簡単である:
import numpy as np vstar = np.array([[1/np.sqrt(3),1/np.sqrt(3),1/np.sqrt(3)],[-1/np.sqrt(6),np.sqrt(2/3),-1/np.sqrt(6)]] ) A = np.array([[1, 1, 0], [1, 1, 1], [0, 1, 1]]) v = np.array([[1/np.sqrt(3),-1/np.sqrt(6)],[1/np.sqrt(3),np.sqrt(2/3)],[1/np.sqrt(3),-1/np.sqrt(6)]]) proj = vstar@A@v print(proj) eigenvalues, eigenvectors = np.linalg.eig(proj) print("The eigenvalues are ", eigenvalues) print("The eigenvectors are ", eigenvectors)outputs:
[[ 2.33333333 0.47140452] [ 0.47140452 -0.33333333]] The eigenvalues are [ 2.41421356 -0.41421356] The eigenvectors are [[ 0.98559856 -0.16910198] [ 0.16910198 0.98559856]]最小固有値の推定値は -0.414 である。
1.2 クリロフ法の種類
「クリロフ部分空間法」は、大規模な線形システムや固有値問題を解くために使われる、いくつかの反復技法のいずれかを指す。 これらに共通するのは、クリロフ部分空間から近似解を構成することである
ここで、 は初期推測値である(文献 [5] を参照)。 収束率、メモリ使用量、全体的な計算コストなどの要素をバランスさせながら、この部分空間から最適な近似を選択する方法が異なる。 このレッスンの焦点は、クリロフ部分空間法の文脈で量子コンピューティングを活用することである。 以下の簡単な定義は、あくまでも文脈のためのものであり、これらの方法をさらに調べるための参考文献も含まれている。
共役勾配(CG)法 :この方法は、対称な正定値線形システムを解くために使用される [6]。 これは、各反復における誤差のAノルムを最小化し、離散化楕円型PDEから生じる系に特に効果的です [7]。 次のセクションでは、このアプローチを使って、なぜクリロフ部分空間が線形システムの解の改善を探るのに効果的な部分空間なのかを説明する。
一般化最小残差(GMRES)法 :これは、一般的な非対称線形システムを解くために設計されています。 これは、各反復においてクリロフ空間上の残差ノルムを最小化するもので、ロバストであるが、大規模なシステムではメモリを消費する可能性がある [7]。
最小残差(MINRES)法 :この方法は、対称不定一次方程式を解くのに用いられる。 GMRESと似ていますが、行列の対称性を利用して計算コストを削減します [8]。
その他の注目すべきアプローチには、固有値問題に対するArnoldiの手法と密接な関係にある完全直交化法(FOM)、 両共役勾配法( BiCG )、 誘導次元削減法(IDR)などがある。
1.3 クリロフ部分空間法が機能する理由
ここでは、クリロフ部分空間法が、最急降下法というレンズを通して、固有ベクトル近似の反復精密化を通して行列固有値を近似する効率的な方法であることを動機づける。 基底状態の初期推測が与えられたとき、その初期推測に対する連続的な補正のうち、最速の収束をもたらす空間はクリロフ部分空間であることを論じる。 収束動作の厳密な証明には至っていない。
注目の行列 が対称かつ正定値であると仮定する。 このため、我々の議論は上記のCG方式に最も適している。 また、 がエルミートでなければならないと主張しているわけでもない(ハミルトニアンであればそうである必要がある)。
私たちは通常、次のような形式の問題を解きたいと考える
ここで、 は固有値問題のようにある定数である。 しかし、今のところ、我々の問題提起はより一般的なものにとどまっている。
まず、近似解であるベクトル から始めます。 この推測 と、セクション 1.1 にある との間には類似点がありますが、ここではこれらを活用していません。 にはエラーがあると思われます。このエラーを と呼んでいます:
また、残差 を次のように定義する:
ここでは、クリロフ部分空間 の次元と残差を区別するために、大文字の 。
ここで、次のような補正を加えたい
これは私たちの近似値を向上させるものである。 ここで 、まだ決定していないベクトルがある。 修正後の誤差を とする。 結果
真
私たちは、私たちの誤差が私たちの行列によって変換されたときにどのように振る舞うかに興味がある。 そこで、誤差の -normを計算してみよう。 それは
ここでは、 という対称性、および という関係式を利用している。ここで、 は とは無関係な定数である。セクション 1.2 で述べたように、誤差の ノルムは、最小化の対象として選択できる唯一の量ではないが、適切な量の一つである。 この量が、補正ベクトル の選択によってどのように変化するかを確認したい。そこで、次のように設定して、関数 を定義する
は、 -normで測定された補正 の関数としての誤差 。 したがって、 ができるだけ小さくなるように、 を選びたい。 この目的のために、 の勾配を計算する。 の対称性を用いると、次のようになる
勾配は上昇が最も急な方向を指しており、つまりその反対方向が、関数が最も減少する方向、すなわち下降が最も急な方向となります。 初期の推測として、 (ここで、 )とすると、 が成り立つ。 したがって、関数 は、残差 の方向で最も減少する。したがって、あるスカラー に対して、ベクトル を加えることで、この初期の選択が最も恩恵を受けることになる。
次のステップでは、やはりベクトル 、その値を現在の近似値に加える。 先ほどと同じ論法で、あるスカラー に対して を選ぶ。このように続けると、 の反復ベクトルは次のようになる
同様に、 、 、...と順番に追加していくことで、改善された推定値を選択する空間を構築したい。 推定されたベクトルは次のようになる
さて、次の関係を使う
つまり
つまり、正しい解 を最も効率的に近似する空間は、行列 を に対して順次作用させることによって構築される空間とまったく同じである。クリロフ部分空間とは*、* 最急降下法の各段階の方向ベクトルによって張られる空間のことである。
最後に、我々はこのアプローチのスケーリングについて数値的な主張をしていないこと、また疎行列に対する比較的な利点について議論していないことを再確認する。 これは、クリロフ部分空間法を使う動機付けと、直感的な感覚を加えることだけを意図している。 ここで、これらの方法の数値的な挙動を探ってみることにする。
理解度チェック
上記のワークフローでは、誤差の -normを最小化することを提案した。 基底状態とその固有値を求める際に、他にどのような量を最小化することが考えられるか?
誤差の -normの代わりに残差ベクトルを使うことも考えられる。 誤差ベクトルそのものを考慮することが有用な場合もあるだろう。
2. クラシカル計算におけるクリロフ法
このセクションでは、固有値問題を解く際にクリロフ部分空間を活用できるように、アーノルディ反復を計算機的に実装する。 まず小規模な例でこれを適用し、次に対象となる行列のサイズが大きくなるにつれて、計算時間がどのようにスケーリングするかを検証する。 ここでの重要な考え方は、クリロフ空間にまたがるベクトルの生成が、必要な総計算時間に大きく寄与するということである。 必要なメモリは、特定のクリロフ法によって異なる。 しかし、メモリの制約により、従来のクリロフ法の使用が制限されることがある。
2.1 単純な小規模な例
クリロフ部分空間を作る過程で、部分空間内のベクトルを正規直交化する必要がある。 私たちの部分空間 vknown (正規化されているとは仮定していない)から確立されたベクトルと、私たちの部分空間 vnext に追加する候補ベクトルを取り、 vnext を vknown に直交させ、正規化する関数を定義しよう。 さらに、クリロフ部分空間内のすべての確立されたベクトルに対して、このプロセスを段階的に実行する関数を定義して、完全に正規直交する集合を保証しよう。
# vknown is some established vector in our subspace. vnext is one we wish to add,
# which must be orthogonal to vknown.
def orthog_pair(vknown, vnext):
vknown = vknown / np.sqrt(vknown.T @ vknown)
diffvec = vknown.T @ vnext * vknown
vnext = vnext - diffvec
return vnext
# v is the candidate vector to be added to our subspace. s is the existing subspace.
def orthoset(v, s):
v = v / np.sqrt(v.T @ v)
temp = v
for i in range(len(s)):
temp = orthog_pair(s[i], temp)
v = temp / np.sqrt(temp.T @ temp)
return vここで、クリロフベクトルの空間が元の行列の全空間に及ぶまで、繰り返しより大きなクリロフ部分空間を構築する関数を定義しよう。 これにより、クリロフ部分空間の次元の関数として、我々のクリロフ部分空間法を用いて得られた固有値が、どの程度正確な値と一致するかを見ることができる。 重要なのは、我々の関数 krylov_full_build 、クリロフ・ベクトル、投影ハミルトニアン、固有値、所要時間を返すことである。
# Necessary imports and definitions to track time in microseconds
import time
def time_mus():
return int(time.time() * 1000000)
# This function constructs a Krylov subspace that spans the whole space of the original matrix.
# Input:
# v0 : initial vector
# matrix : original matrix to be diagonalized
# Output:
# ks : Krylov vectors
# Hs : projected Hamiltonians
# eigs : eigenvalues
# k_tot_times : time required for the operation
def krylov_full_build(v0, matrix):
t0 = time_mus()
b = v0 / np.sqrt(v0 @ v0.T)
A = matrix
ks = []
ks.append(b)
Hs = []
eigs = []
Hs.append(b.T @ A @ b)
eigs.append(np.array([b.T @ A @ b]))
k_tot_times = []
for j in range(len(A) - 1):
vec = A @ ks[j].T
ortho = orthoset(vec, ks)
ks.append(ortho)
ksarray = np.array(ks)
Hs.append(ksarray @ A @ ksarray.T)
eigs.append(np.linalg.eig(Hs[j + 1]).eigenvalues)
k_tot_times.append(time_mus() - t0)
# Return the Krylov vectors, the projected Hamiltonians, the eigenvalues,
# and the total time required.
return (ks, Hs, eigs, k_tot_times)まだかなり小さいが、手作業でやるよりは大きい行列でテストしてみよう。
# Define our small test matrix
test_matrix = np.array(
[
[4, -1, 0, 1, 0],
[-1, 4, -1, 2, 1],
[0, -1, 4, 3, 3],
[1, 2, 3, 4, 0],
[0, 1, 3, 0, 4],
]
)
# Give the test matrix and an initial guess as arguments in the function defined above.
# Calculate outputs.
test_ks, test_Hs, test_eigs, text_k_tot_times = krylov_full_build(
np.array([0.5, 0.5, 0, 0.5, 0.5]), test_matrix
)最後のステップ(クリロフ空間が元の行列の完全なベクトル空間である場合)で、クリロフ法による固有値が厳密な数値対角化の固有値と正確に一致することを確認することで、我々の関数をチェックすることができる:
print(np.linalg.eig(test_matrix).eigenvalues)
print(test_eigs[len(test_matrix) - 1])Output:
[-1.36956923 8.43756009 2.9040308 5.34436028 4.68361806]
[-1.36956923 8.43756009 2.9040308 4.68361806 5.34436028]
それは成功した。 もちろん、本当に重要なのは、クリロフ部分空間の次元の関数として、我々の近似がどれだけ優れているかということだ。 我々は基底状態やその他の最小固有値を見つけることにしばしば関心を持つので(そして以下に説明する代数的な理由もある)、クリロフ部分空間の次元の関数として最小固有値の推定値を見てみよう。 それは
def errors(matrix, krylov_eigs):
targ_min = min(np.linalg.eig(matrix).eigenvalues)
err = []
for i in range(len(matrix)):
err.append(min(krylov_eigs[i]) - targ_min)
return errimport matplotlib.pyplot as plt
krylov_error = errors(test_matrix, test_eigs)
plt.plot(krylov_error)
plt.axhline(y=0, color="red", linestyle="--") # Add dashed red line at y=0
plt.xlabel("Order of Krylov subspace") # Add x-axis label
plt.ylabel("Error in minimum eigenvalue") # Add y-axis label
plt.show()Output:
クリロフ部分空間が まで拡大すると、最小固有値がかなり正確に得られることがわかります。また、 の時点で完全に一致することがわかります。
2.2 行列の次元による時間スケール
クリロフ法が厳密な数値固有値解法よりも有利であることを、以下の方法で納得させよう:
- ランダム行列を構成する(スパースではないので、KQDの理想的なアプリケーションではない)
- NumPy を直接使う方法とクリロフ部分空間を使う方法の2つの方法で固有値を決定する。
- 我々は、クリロフ推定を受け入れる前に、固有値がどの程度正確でなければならないかのカットオフ値を選択する。
- この2つの方法で解くのに必要な壁時間を比較する。
注意点以下で詳しく説明するように、クリロフ量子対角化は、行列表現が疎であるか、あるいは少数のパウリ作用素群を使って書くことができる作用素に適用するのが最適である。 ここで使っているランダム行列は、そのような説明には当てはまらない。 これらは、古典的なクリロフ法が役に立つかもしれないスケールを探るのに役立つに過ぎない。 第二に、クリロフ法を使う場合、多くの異なるサイズのクリロフ部分空間を使って固有値を計算する。 基底状態固有値の要求精度を達成する最小次元クリロフ部分空間の所要時間を報告する。 繰り返しになるが、これは厳密固有値ソルバーで実行不可能な問題を解くのとは少し違う。なぜなら、必要な次元を評価するために厳密解を使っているからだ。
まず、ランダムな行列のセットを生成する。
import numpy as np
# Set the random seed
np.random.seed(42)
# how many random matrices will we make
num_matrix = 200
matrices = []
for m in range(1, num_matrix):
matrices.append(np.random.rand(m, m))ここで、numpyを使って各行列を直接対角化する。 後で比較するために、対角化に要する時間を計算する。
matrix_numpy_times = []
matrix_numpy_eigs = []
for mm in range(num_matrix - 1):
t0 = time_mus()
matrix_numpy_eigs.append(min(np.linalg.eig(matrices[mm]).eigenvalues))
matrix_numpy_times.append(time_mus() - t0)
plt.plot(matrix_numpy_times)
plt.xlabel("Dimension of matrix") # Add x-axis label
plt.ylabel("Time to diagonalize (microsec)") # Add y-axis label
plt.show()Output:
上の画像で、125次元付近で時間が異常に長くなっているのは、行列のランダムな性質によるものか、使用された古典的プロセッサの実装によるものかもしれないが、再現性はない。 コードを再実行すると、異なる異常ピークを持つ異なるプロファイルが得られる。
ここで各行列についてクリロフ部分空間を構築し、段階的に固有値を計算する。 各ステップで、指定した絶対誤差の範囲内で最小固有値が得られたかどうかをチェックする。 指定した誤差内で最初に固有値を与える部分空間が、計算時間を記録する部分空間である。 このセルを実行するには、プロセッサーの速度にもよるが、数分かかるかもしれない。 評価を省略したり、対角化する行列の最大次元を小さくしたりすることは自由である。 事前に計算された結果を見るだけで十分だ。
# Choose the absolute error you can tolerate, and make a list for tracking the Krylov subspace size
# at which that error is achieved.
abserr = 0.05
accept_subspace_size = []
# Lists to store total time spent on the Krylov method, and the subset of that time spent on
# diagonalizing the projected matrix.
matrix_krylov_tot_times = []
matrix_krylov_dim = []
# Step through all our random matrices
for mm in range(0, num_matrix - 1):
test_ks, test_Hs, test_eigs, test_k_tot_times = krylov_full_build(
np.ones(len(matrices[mm])), matrices[mm]
)
# We have not yet found a Krylov subspace that produces our minimum eigenvalue to
# within the required error.
found = 0
for j in range(0, len(matrices[mm]) - 1):
# If we still haven't found the desired subspace...
if found == 0:
# ...but if this one satisfies the requirement, then record everything
if (
abs((min(test_eigs[j]) - matrix_numpy_eigs[mm]) / matrix_numpy_eigs[mm])
< abserr
):
accept_subspace_size.append(j)
matrix_krylov_tot_times.append(test_k_tot_times[j])
matrix_krylov_dim.append(mm)
found = 1比較のために、この2つの方法で得られたタイムをプロットしてみよう:
plt.plot(matrix_numpy_times, color="blue")
plt.plot(matrix_krylov_dim, matrix_krylov_tot_times, color="green")
plt.xlabel("Dimension of matrix") # Add x-axis label
plt.ylabel("Time to diagonalize (microsec)") # Add y-axis label
plt.show()Output:
これらは実際に必要な時間であるが、議論のために、隣接するいくつかの点/マトリックス次元の平均によってこれらの曲線を平滑化してみよう。 これは以下のように行われる:
smooth_numpy_times = []
smooth_krylov_times = []
# Choose the number of adjacent points over which to average forward;
# the same will be used backward.
smooth_steps = 10
# We will do this smoothing for all points/matrix dimensions
for i in range(len(matrix_krylov_tot_times)):
# Ensure we don't exceed the boundaries of our lists
start = max(0, i - smooth_steps)
end = min(len(matrix_krylov_tot_times) - 1, i + smooth_steps)
# Dummy variables for accumulating an average over adjacent points. This is done for both Krylov
# and the NumPy calculations.
smooth_count = 0
smooth_numpy_sum = 0
smooth_krylov_sum = 0
for j in range(start, end):
smooth_numpy_sum = smooth_numpy_sum + matrix_numpy_times[j]
smooth_krylov_sum = smooth_krylov_sum + matrix_krylov_tot_times[j]
smooth_count = smooth_count + 1
# Appending the averaged adjacent values to our new smooth lists
smooth_numpy_times.append(smooth_numpy_sum / smooth_count)
smooth_krylov_times.append(smooth_krylov_sum / smooth_count)plt.plot(smooth_numpy_times, color="blue")
plt.plot(smooth_krylov_times, color="green")
plt.xlabel("Dimension of matrix") # Add x-axis label
plt.ylabel("Time to diagonalize (smoothed, microsec)") # Add y-axis label
plt.show()Output:
クリロフ部分空間の構築に必要な時間は、当初はnumpyの完全対角化に必要な時間を上回ることに注意してください。 しかし、行列のサイズが大きくなると、クリロフ法が有利になる。 これは許容誤差を小さくしても同じで、マトリックスサイズが大きくなるほど有利になる。 これはバラバラにする価値がある。
数値対角化の時間複雑度は (アルゴリズムによって多少の違いはある)。 ベクトルの正規直交基底を生成する時間の複雑さも である。つまり、クリロフ法の利点は、 の正規直交基底の使用とは関係なく、関心のある固有値を効果的に選び出す特定の正規直交基底の使用にある。 このことは、このレッスンの最初のセクションの証明のスケッチからすでに見たとおりであり、これはクリロフ法の収束保証にとって重要である。
これまでの歩みを振り返ってみよう:
- 非常に大きな行列の場合、クリロフ部分空間法は、従来の対角化アルゴリズムよりも速く、必要な許容範囲内で近似固有値を得ることができる。
- このような非常に大きな行列では、クリロフ部分空間の生成がクリロフ部分空間法の最も時間のかかる部分である。
- したがって、クリロフ部分空間を効率的に生成する方法は非常に価値がある。 ここでようやく量子コンピューターが登場する。
理解度チェック
上記の、対角化時間と行列の次元との関係を示す平滑化されたグラフを参照してください。
(a) このグラフによると、クリロフ法はおよそどの行列次元から計算速度が向上したか
(b) クリロフ法がより高速になる閾値は、計算のどのような側面によって変化しうるか
(a) 計算を再実行すると結果が異なる場合がありますが、クリロフ法は次元がおよそ80~85になると処理速度が向上します。
(b) 答えはいくつもあります。 重要な要素としては、求められる精度や、対角化される行列の疎さが挙げられます。
3. 時間発展によるクリロフ
これまで説明してきたことはすべて、古典的に行うことができる。 では、量子コンピューターをいつ、どのように使うのか? 非常に大きな行列の場合、クリロフ法は長い計算時間と大量のメモリを必要とする。 、 の行列演算に要する時間は、最悪の場合 のような規模になる。 ベクトル上の疎行列の乗算(古典的なクリロフ型ソルバーの典型的なケース)でさえ、 のような時間複雑度のスケーリングがある。これは、部分空間内のすべてのベクトルに対して行われます。 通常、部分空間の次元 は の何分の一にもならず、 のようにスケールすることが多い。したがって、すべてのベクトルを生成することは、最悪の場合、 のようにスケールする。 直交化のような他のステップもあるが、このスケーリングは覚えておくべき主要なスケーリングである。
量子コンピューティングは、必要な時間とリソースのスケーリングを決定する問題の属性を変えることを可能にする。 行列の大きさ( )に全面的に依存するのではなく、ショット数やハミルトニアンを構成する非共約パウリ項の数といったものを見ることになる。 この仕組みを探ってみよう。
3.1 時間発展
量子状態を時間発展させる作用素は (特に量子コンピューティングでは、表記から を削除することが非常に一般的)であることを思い出してほしい。 このような演算子の指数関数を理解し、実現する一つの方法は、そのテイラー級数展開を見ることである。 ある初期ベクトル に作用するこの操作は、初期状態に適用される のべき乗が増加する項の和をもたらすことに注意。 初期推測の状態を時間発展させることで、クリロフ部分空間を作ればよさそうだ!
注意点は、実際の量子コンピューターで時間発展を実現することである。 ハミルトニアンの項の多くは互いに交わらない。 したがって、 のような単純な指数演算子は単純な回路に対応するが、一般的なハミルトニアンはそうではない。 また、これらの項には非整合の項が含まれているため、指数では数のように単純な項の積に分解することはできない。
つまり、これは些細なことではないが、量子コンピューティングではよく研究されているプロセスなのだ。 私たちは量子コンピュータ上でトロッター化と呼ばれるプロセスを用いて時間進化を行うが、それ自体が豊かなテーマである [10]。 しかし、非常に高いレベルでは、時間発展を非常に小さなステップ、例えばサイズ の ステップに分割することによって、項の非可換性の影響を制限することができる。
ここで 。
Hのべき乗を直接用いて古典的な文脈で生成した、次数rのクリロフ部分空間を「べき乗クリロフ部分空間」と呼ぼう。
ここで、ユニタリー時間発展演算子 を用いて同様の空間を生成する。これを「ユニタリー・クリロフ空間」 と呼ぶことにする。私たちが古典的に用いているパワークリロフ部分空間 は、 がユニタリー演算子ではないため、量子コンピュータ上で直接生成することはできません。 ユニタリー・クリロフ部分空間を用いることで、べき乗クリロフ部分空間と同様の収束保証が得られることが示される。すなわち、初期状態 、指数関数的に消失しない真の基底状態との重なりがあり、固有値間に十分なギャップがある限り、基底状態の誤差は効率的に収束する。 収束についてのより正確な議論は文献 [1] を参照。
ここで、 の累乗は異なる時間ステップになる( の累乗 は時間 だけ進む)。 総時間 に対して時間発展する部分空間の要素を、 とラベル付けすることができる。
ハミルトニアンHをユニタリー・クリロフ部分空間 に射影することができる。言い換えれば、 の各行列要素を の基底で計算します。 この投影行列を と呼ぶことにする。
3.2 量子コンピュータでの実装方法
の行列要素は期待値 で与えられ、これは量子コンピュータを使って推定できる。 、異なる量子ビット上のパウリ演算子の和として書くことができ、すべてのパウリ演算子を同時に測定できるわけではないことに留意してほしい。 パウリ項を通約項のグループに分類し、それらを一度に測定することができる。 しかし、すべての条件をカバーするためには、このようなグループがたくさん必要かもしれない。 そこで、項を分割できる別個の通学グループの数、 が重要になる。
ここで、 は、 の形をとるパウリ文字列、あるいは互いに可換なそのようなパウリ文字列の集合である。 をこのような可測演算子の和として表すことができることを踏まえると、 の行列要素に関する以下の式は、 IBM Quantum のプリミティブ推定器を用いて実現することができる。
ここで、 はユニタリ・クリロフ空間のベクトルであり、 は、選択された時間ステップ の倍数である。 量子コンピュータ上では、各行列要素の計算は、量子状態間の重なりを得ることができる任意のアルゴリズムを用いて行うことができます。 このレッスンでは、アダマール検定について重点的に取り上げます。 の次元が であることを踏まえると、この部分空間に射影されたハミルトニアンの次元は となる。 が十分に小さい場合(一般に、固有値の推定値が収束するためには で十分である)、古典的には、射影されたハミルトニアン を容易に対角化することができる。 しかし、クリロフ空間のベクトルが直交していないため、 を直接対角化することはできない。 それらの重なりを測定し、行列を作成する必要があります。
これにより、非直交空間における固有値問題(一般化固有値問題とも呼ばれる)を解くことができる
そして、この一般化された固有値問題の解を見ることによって、 の固有値と固有状態の推定値を得ることができる。 例えば、基底状態のエネルギーの推定値は、最小の固有値 と、対応する固有ベクトル から基底状態を取ることによって得られる。 の係数は、 にまたがるさまざまなベクトルの寄与を決定します。
一般化された固有値問題
なぜ単純に を対角化できないのでしょうか? 、クリロフ基底(非常に特殊な場合を除き、すべて非直交である)の幾何学的形状に関する情報が含まれているので、 、それだけでは完全なハミルトニアンの射影を記述しないので、その固有値は完全なハミルトニアンの固有値とは特に関係がありません。 一般化固有値問題を解くことは、完全なハミルトニアンのクリロフ空間への射影に対応する近似固有値と固有ベクトルを得るために必要である。
図に示すのは、異なる量子状態間の重なりを計算するために用いられる修正ハダマールテストの回路図である。 各行列要素 、状態 、 の間のハダマルド検定が行われる。 図では、行列要素と対応する 、 の操作の配色によって、このことが強調されている。 したがって、投影ハミルトニアン のすべての行列要素を計算するために、クリロフ空間ベクトルのすべての可能な組み合わせに対するハダマルド検定のセットが必要となる。ハダマルド検定回路の一番上のワイヤーはアンシラ量子ビットで、XまたはY基底で測定され、その期待値は状態間の重なりの値を決定します。 一番下のワイヤーは、システム・ハミルトニアンのすべての量子ビットを表している。 ( )、 ( )はシステム・ハミルトニアンのパウリ分解を表す。量子コンピュータでの実装を以下に詳しく示します。
4. 量子コンピュータ上でのクリロフ量子対角化
次に、クリロフ量子対角化を実際の量子コンピュータで実装する。 まずは便利なパッケージをいくつかインポートしてみよう。
import numpy as np
import scipy as sp
import matplotlib.pylab as plt
from typing import Union, List
import warnings
from qiskit.quantum_info import SparsePauliOp, Pauli
from qiskit.circuit import Parameter
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.synthesis import LieTrotter
# from qiskit.providers.fake_provider import Fake20QV1
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator, Batch
import itertools as it
warnings.filterwarnings("ignore")先ほど説明した一般化固有値問題を解く関数を以下に定義する。
def solve_regularized_gen_eig(
h: np.ndarray,
s: np.ndarray,
threshold: float,
k: int = 1,
return_dimn: bool = False,
) -> Union[float, List[float]]:
"""
Method for solving the generalized eigenvalue problem with regularization
Args:
h (numpy.ndarray):
The effective representation of the matrix in our Krylov subspace
s (numpy.ndarray):
The matrix of overlaps between vectors of our Krylov subspace
threshold (float):
Cut-off value for the eigenvalue of s
k (int):
Number of eigenvalues to return
return_dimn (bool):
Whether to return the size of the regularized subspace
Returns:
lowest k-eigenvalue(s) that are the solution of the regularized generalized eigenvalue problem
"""
s_vals, s_vecs = sp.linalg.eigh(s)
s_vecs = s_vecs.T
good_vecs = np.array([vec for val, vec in zip(s_vals, s_vecs) if val > threshold])
h_reg = good_vecs.conj() @ h @ good_vecs.T
s_reg = good_vecs.conj() @ s @ good_vecs.T
if k == 1:
if return_dimn:
return sp.linalg.eigh(h_reg, s_reg)[0][0], len(good_vecs)
else:
return sp.linalg.eigh(h_reg, s_reg)[0][0]
else:
if return_dimn:
return sp.linalg.eigh(h_reg, s_reg)[0][:k], len(good_vecs)
else:
return sp.linalg.eigh(h_reg, s_reg)[0][:k]少なくとも最初のベンチマークでは、収束挙動をチェックするために、厳密な古典解を知っておくことは有用である。 以下の関数は、ハミルトニアンと量子ビット数を引数として、ハミルトニアンの基底状態エネルギーを計算します。
def single_particle_gs(H_op, n_qubits):
"""
Find the ground state of the single particle(excitation) sector
"""
H_x = []
for p, coeff in H_op.to_list():
H_x.append(set([i for i, v in enumerate(Pauli(p).x) if v]))
H_z = []
for p, coeff in H_op.to_list():
H_z.append(set([i for i, v in enumerate(Pauli(p).z) if v]))
H_c = H_op.coeffs
print("n_sys_qubits", n_qubits)
n_exc = 1
sub_dimn = int(sp.special.comb(n_qubits + 1, n_exc))
print("n_exc", n_exc, ", subspace dimension", sub_dimn)
few_particle_H = np.zeros((sub_dimn, sub_dimn), dtype=complex)
sparse_vecs = [
set(vec) for vec in it.combinations(range(n_qubits + 1), r=n_exc)
] # list all of the possible sets of n_exc indices of 1s in n_exc-particle states
m = 0
for i, i_set in enumerate(sparse_vecs):
for j, j_set in enumerate(sparse_vecs):
m += 1
if len(i_set.symmetric_difference(j_set)) <= 2:
for p_x, p_z, coeff in zip(H_x, H_z, H_c):
if i_set.symmetric_difference(j_set) == p_x:
sgn = ((-1j) ** len(p_x.intersection(p_z))) * (
(-1) ** len(i_set.intersection(p_z))
)
else:
sgn = 0
few_particle_H[i, j] += sgn * coeff
gs_en = min(np.linalg.eigvalsh(few_particle_H))
print("single particle ground state energy: ", gs_en)
return gs_en4.1 ステップ1: 問題を量子回路と演算子にマッピングする
ここでハミルトニアンを定義する。 これは上の関数とは異なり、上の関数はハミルトニアンを引数にとり、基底状態のみを返す。 ここで定義するハミルトニアンは、すべてのエネルギー固有状態のエネルギー準位を決定するものであり、このハミルトニアンはパウリ作用素を用いて構成することができ、量子コンピューター上で実装することができる。
我々は、「ハイゼンベルグ鎖」と呼ばれる、空間上でどのような向きにもなりうるスピンの鎖に対応するハミルトニアンを選ぶ。 我々は、 のスピンは、最も近い隣人( と のスピン)の影響を受けるが、より遠い隣人の影響を受けることはないと仮定している。 また、スピンが異なる軸を向いている場合、スピン間の相互作用が異なる可能性も考慮する。 この非対称性は、例えばスピンが埋め込まれた結晶格子の構造によって生じることがある。
# Define problem Hamiltonian.
n_qubits = 10
# coupling strength for XX, YY, and ZZ interactions
JX = 1
JY = 3
JZ = 2
# Define the Hamiltonian:
H_int = [["I"] * n_qubits for _ in range(3 * (n_qubits - 1))]
for i in range(n_qubits - 1):
H_int[i][i] = "Z"
H_int[i][i + 1] = "Z"
for i in range(n_qubits - 1):
H_int[n_qubits - 1 + i][i] = "X"
H_int[n_qubits - 1 + i][i + 1] = "X"
for i in range(n_qubits - 1):
H_int[2 * (n_qubits - 1) + i][i] = "Y"
H_int[2 * (n_qubits - 1) + i][i + 1] = "Y"
H_int = ["".join(term) for term in H_int]
H_tot = [
(term, JZ)
if term.count("Z") == 2
else (term, JY)
if term.count("Y") == 2
else (term, JX)
for term in H_int
]
# Get operator
H_op = SparsePauliOp.from_list(H_tot)
print(H_tot)Output:
[('ZZIIIIIIII', 2), ('IZZIIIIIII', 2), ('IIZZIIIIII', 2), ('IIIZZIIIII', 2), ('IIIIZZIIII', 2), ('IIIIIZZIII', 2), ('IIIIIIZZII', 2), ('IIIIIIIZZI', 2), ('IIIIIIIIZZ', 2), ('XXIIIIIIII', 1), ('IXXIIIIIII', 1), ('IIXXIIIIII', 1), ('IIIXXIIIII', 1), ('IIIIXXIIII', 1), ('IIIIIXXIII', 1), ('IIIIIIXXII', 1), ('IIIIIIIXXI', 1), ('IIIIIIIIXX', 1), ('YYIIIIIIII', 3), ('IYYIIIIIII', 3), ('IIYYIIIIII', 3), ('IIIYYIIIII', 3), ('IIIIYYIIII', 3), ('IIIIIYYIII', 3), ('IIIIIIYYII', 3), ('IIIIIIIYYI', 3), ('IIIIIIIIYY', 3)]
以下のコードでは、ハミルトニアンを単一粒子状態に制限し、スペクトルノルムを用いて時間ステップ の適切なサイズを設定しています。ハミルトニアンノルムの上限に基づいて)時間ステップ dt の値を発見的に選択します。 文献 [9] は、十分に小さいタイムス テップは 、ある点までは、この値を過大評価するよりも過小評価する方が望ましいことを示した。過大評価すると、高エネルギー状態からの寄与がクリロフ空間の最適状態さえも破壊してしまうからである。 一方、 を小さくしすぎると、クリロフ基底ベクトルのタイムステップごとの違いが小さくなるため、クリロフ部分空間のコンディショニングが悪くなる。
# Get Hamiltonian restricted to single-particle states
single_particle_H = np.zeros((n_qubits, n_qubits))
for i in range(n_qubits):
for j in range(i + 1):
for p, coeff in H_op.to_list():
p_x = Pauli(p).x
p_z = Pauli(p).z
if all(p_x[k] == ((i == k) + (j == k)) % 2 for k in range(n_qubits)):
sgn = ((-1j) ** sum(p_z[k] and p_x[k] for k in range(n_qubits))) * (
(-1) ** p_z[i]
)
else:
sgn = 0
single_particle_H[i, j] += sgn * coeff
for i in range(n_qubits):
for j in range(i + 1, n_qubits):
single_particle_H[i, j] = np.conj(single_particle_H[j, i])
# Set dt according to spectral norm
dt = np.pi / np.linalg.norm(single_particle_H, ord=2)
dtOutput:
np.float64(0.17453292519943295)
時間発展に使用するトロッターステップの数を指定します。 また、最大クリロフ次元を4と指定します。 このクリロフ次元は、現実的な応用には十分ではない。 しかし、この例にはこれで十分である。 さらに、より小さな次元での収束性を検証する。 後のレッスンでは、ハミルトニアンをより大きな部分空間にスケーリングおよび射影する方法を探求します。
# Set parameters for quantum Krylov algorithm
krylov_dim = 4 # size of krylov subspace
num_trotter_steps = 4
dt_circ = dt / num_trotter_steps状態準備
基底状態と重なる参照状態( )を選ぶ。 このハミルトニアンでは、中央の量子ビット( )が励起された状態を参照状態として用いる。
qc_state_prep = QuantumCircuit(n_qubits)
qc_state_prep.x(int(n_qubits / 2) + 1)
qc_state_prep.draw("mpl", scale=0.5)Output:
時間発展
我々は、与えられたハミルトニアンによって生成される時間発展演算子を実現することができる。 、 リー・トロッター近似を経由する。 簡単のため、時間発展回路に内蔵されている PauliEvolutionGate 。 一般的な構文はこうだ。
t = Parameter("t")
## Create the time-evo op circuit
evol_gate = PauliEvolutionGate(
H_op, time=t, synthesis=LieTrotter(reps=num_trotter_steps)
)
qr = QuantumRegister(n_qubits)
qc_evol = QuantumCircuit(qr)
qc_evol.append(evol_gate, qargs=qr)Output:
<qiskit.circuit.instructionset.InstructionSet at 0x7ccaa4664250>
以下、ハダマードテストでこのバージョンを使用するが、時間 に対して前進する。
ハダマール検定
ここで、ハダマール検定を用いて、 およびグラム行列 の両方の行列要素を計算したいことを思い出してください。 この文脈において、これがどのように機能するのかを振り返ってみましょう。まずは、 の構成に焦点を当てます。全体的なプロセスを以下の図に示します。 色分けされた状態準備ブロックの層( )は、この処理が、我々の部分空間における および のすべての組み合わせに対して行われることを示唆している。
各ステップにおけるシステムの状態は以下の通りである:
ここで、 はハミルトニアンの分解におけるパウリ項である(ユニタリーでなくなるため、複数のパウリ項の線形結合にはなりえないことに注意--後で示す別の構成を使ってグループ化は可能)。 、 は、ユニタリー・クリロフ空間のベクトル 、 を準備する制御された演算であり、 。この回路に と の測定を適用すると、必要な行列要素の実部と虚部がそれぞれ計算される。
上記のステップ4から始めて、0番目の量子ビットにハダマードゲート 。
次に、 または を測定する。
恒等式から 。同様に、 を測定すると、次のようになる
先に設定した時間進化にこれらのステップを加えると、次のようになる。
## Create the time-evo op circuit
evol_gate = PauliEvolutionGate(
H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)
)
## Create the time-evo op dagger circuit
evol_gate_d = PauliEvolutionGate(
H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)
)
evol_gate_d = evol_gate_d.inverse()
# Put pieces together
qc_reg = QuantumRegister(n_qubits)
qc_temp = QuantumCircuit(qc_reg)
qc_temp.compose(qc_state_prep, inplace=True)
for _ in range(num_trotter_steps):
qc_temp.append(evol_gate, qargs=qc_reg)
for _ in range(num_trotter_steps):
qc_temp.append(evol_gate_d, qargs=qc_reg)
qc_temp.compose(qc_state_prep.inverse(), inplace=True)
# Create controlled version of the circuit
controlled_U = qc_temp.to_gate().control(1)
# Create hadamard test circuit for real part
qr = QuantumRegister(n_qubits + 1)
qc_real = QuantumCircuit(qr)
qc_real.h(0)
qc_real.append(controlled_U, list(range(n_qubits + 1)))
qc_real.h(0)
print("Circuit for calculating the real part of the overlap in S via Hadamard test")
qc_real.draw("mpl", fold=-1, scale=0.5)Output:
Circuit for calculating the real part of the overlap in S via Hadamard test
トロッター・サーキットの奥深さについてはすでに警告した。 このような条件でハダマルドテストを実行すると、特にネイティブゲートに分解すると、さらに深い回路が得られる。 もしデバイスのトポロジーを考慮すれば、これはさらに増えるだろう。 従って、量子コンピュータを使う前に、回路の2量子ビットの深さをチェックするのは良い考えだ。
print(
"Number of layers of 2Q operations",
qc_real.decompose(reps=2).depth(lambda x: x[0].num_qubits == 2),
)Output:
Number of layers of 2Q operations 14401
これほど複雑な回路では、現代の量子コンピュータでは実用的な結果を得ることができません。 と を構築するには、もっと良い方法が必要です。 これが、後述する効率的なアダマール検定が導入された理由である。
4. 2 ステップ2. 対象ハードウェア向けに回路と演算子を最適化する
効率的なハダマール検定
いくつかの近似を導入し、モデルのハミルトニアンに関するいくつかの仮定に依存することで、得られたハダマルド・テストの深層回路を最適化することができる。 例えば、次のようなハダマード・テストの回路を考えてみよう:
ハミルトニアン の下での の固有値 を古典的に計算できると仮定する。 これはハミルトニアンがU(1)対称性を保存しているときに満たされる。 これは強い仮定のように思えるかもしれないが、ハミルトニアンの作用に影響されない真空状態(この場合、 状態に対応する)が存在すると仮定しても安全な場合がたくさんある。 これは例えば、安定な分子(電子の数が保存されている)を記述する化学のハミルトニアンに当てはまります。 ゲート 、所望の参照状態 を準備することを考えると、例えば、化学のためのHF状態 を準備することは、単一量子ビットNOTの積になるので、制御された- は単なるCNOTの積である。 すると、上記の回路は、測定前の以下の状態を実現する:
ここでは、ステップ2からステップ3まで、古典的なシミュラブル位相シフト 。 したがって期待値は
これらの仮定を用いることで、より少ない制御演算で、関心のある演算子の期待値を書くことができた。 実際には、制御された状態準備( )だけを実装すればよく、制御された時間発展は必要ない。 上記のように計算を組み直すことで、結果として得られる回路の深さを大幅に減らすことができる。
ボーナスとして、パウリ演算子は回路の途中で制御されたゲートとしてではなく、回路の最後で測定されるようになったので、上で示した分解 のように、他の通学パウリ演算子と一緒に測定することができる。
トロッター分解を用いて時間発展演算子を分解する
時間発展演算子を厳密に実装する代わりに、トロッター分解を用いてその近似を実装することができます。 ある順序のトロッター分解を数回繰り返すことで、近似によって生じる誤差をさらに低減することができます。 以下では、検討しているハミルトニアンの相互作用グラフ(最近接相互作用のみ)に対して、最も効率的な方法でトロッター法の実装を直接構築する。 実際には、結合強度 、 、 およびパラメータ化された角度 を持つパウリ回転 、 、 を挿入します。これらは、 の近似的な実装に対応しています。パウリ回転の定義と、実装しようとしている時間発展との違いを考慮すると、 の時間発展を実現するには、パラメータ を使用する必要があります。 さらに、トロッターステップの繰り返し回数が奇数の場合、演算の順序を逆転させます。これは機能的には同等ですが、隣接する演算を単一の ユニタリー演算で合成できるようになります。 これにより、汎用機能 PauliEvolutionGate() を使用した場合よりもはるかに浅い回路が得られます。
t = Parameter("t")
# Create instruction for rotation about XX+YY-ZZ:
Rxyz_circ = QuantumCircuit(2)
Rxyz_circ.rxx(2 * JX * t, 0, 1)
Rxyz_circ.ryy(2 * JY * t, 0, 1)
Rxyz_circ.rzz(2 * JZ * t, 0, 1)
Rxyz_instr = Rxyz_circ.to_instruction(label="R J_x XX + J_y YY + J_z ZZ")
interaction_list = [
[[i, i + 1] for i in range(0, n_qubits - 1, 2)],
[[i, i + 1] for i in range(1, n_qubits - 1, 2)],
] # linear chain
qr = QuantumRegister(n_qubits)
trotter_step_circ = QuantumCircuit(qr)
for i, color in enumerate(interaction_list):
for interaction in color:
trotter_step_circ.append(Rxyz_instr, interaction)
if i < len(interaction_list) - 1:
trotter_step_circ.barrier()
reverse_trotter_step_circ = trotter_step_circ.reverse_ops()
qc_evol = QuantumCircuit(qr)
for step in range(num_trotter_steps):
if step % 2 == 0:
qc_evol = qc_evol.compose(trotter_step_circ)
else:
qc_evol = qc_evol.compose(reverse_trotter_step_circ)
qc_evol.decompose().draw("mpl", fold=-1, scale=0.5)Output:
この効率的なハダマルド・テストのために、再び初期状態を準備する。
control = 0
excitation = int(n_qubits / 2) + 1
controlled_state_prep = QuantumCircuit(n_qubits + 1)
controlled_state_prep.cx(control, excitation)
controlled_state_prep.draw("mpl", fold=-1, scale=0.5)Output:
および の行列要素をHadamardテストを用いて計算するためのテンプレート回路
ハダマード・テストで使用される回路の唯一の違いは、時間発展演算子の位相と測定される観測値である。 したがって、時間発展演算子に依存するゲートのプレースホルダーを持つ、ハダマード・テストの一般的な回路を表すテンプレート回路を用意することができる。
# Parameters for the template circuits
parameters = []
for idx in range(1, krylov_dim):
parameters.append(dt_circ * (idx))# Create modified hadamard test circuit
qr = QuantumRegister(n_qubits + 1)
qc = QuantumCircuit(qr)
qc.h(0)
qc.compose(controlled_state_prep, list(range(n_qubits + 1)), inplace=True)
qc.barrier()
qc.compose(qc_evol, list(range(1, n_qubits + 1)), inplace=True)
qc.barrier()
qc.x(0)
qc.compose(controlled_state_prep.inverse(), list(range(n_qubits + 1)), inplace=True)
qc.x(0)
qc.decompose().draw("mpl", fold=-1)Output:
print(
"The optimized circuit has 2Q gates depth: ",
qc.decompose().decompose().depth(lambda x: x[0].num_qubits == 2),
)Output:
The optimized circuit has 2Q gates depth: 50
この深さは、オリジナルのハダマード・テストに比べて大幅に減少している。 この深さは、最新の量子コンピューターでも管理可能なものだが、それでもかなり高い。 有用な結果を得るためには、最先端のエラー緩和を用いる必要がある。
量子クリロフ計算を実行するバックエンドを選択し、その量子コンピューター上で実行できるように回路をトランスパイルする。
# Use the least-busy backend or specify a quantum computer using the syntax commented out below.
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
# Or you may choose a specify backend and channel if necessary for your workflow.
# service = QiskitRuntimeService(channel="ibm_quantum_platform")
# backend = service.backend("ibm_fez")ここで回路と演算子をトランスパイルする。
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
target = backend.target
basis_gates = list(target.operation_names)
pm = generate_preset_pass_manager(
optimization_level=3, backend=backend, basis_gates=basis_gates
)
qc_trans = pm.run(qc)print(qc_trans.depth(lambda x: x[0].num_qubits == 2))
print(qc_trans.count_ops())
qc_trans.draw("mpl", fold=-1, idle_wires=False, scale=0.5)Output:
36
OrderedDict([('rz', 410), ('sx', 361), ('cz', 156), ('x', 18), ('barrier', 6)])
最適化の後、我々のトランスパイルされた2量子ビットの深さはさらに減少した。
4.3 ステップ3。 IBM Quantum プリミティブを使用して実行する
次に、Estimatorで実行するためのPUBを作成する。
# Define observables to measure for S
observable_S_real = "I" * (n_qubits) + "X"
observable_S_imag = "I" * (n_qubits) + "Y"
observable_op_real = SparsePauliOp(
observable_S_real
) # define a sparse pauli operator for the observable
observable_op_imag = SparsePauliOp(observable_S_imag)
layout = qc_trans.layout # get layout of transpiled circuit
observable_op_real = observable_op_real.apply_layout(
layout
) # apply physical layout to the observable
observable_op_imag = observable_op_imag.apply_layout(layout)
observable_S_real = (
observable_op_real.paulis.to_labels()
) # get the label of the physical observable
observable_S_imag = observable_op_imag.paulis.to_labels()
observables_S = [[observable_S_real], [observable_S_imag]]
# Define observables to measure for H
# Hamiltonian terms to measure
observable_list = []
for pauli, coeff in zip(H_op.paulis, H_op.coeffs):
# print(pauli)
observable_H_real = pauli[::-1].to_label() + "X"
observable_H_imag = pauli[::-1].to_label() + "Y"
observable_list.append([observable_H_real])
observable_list.append([observable_H_imag])
layout = qc_trans.layout
observable_trans_list = []
for observable in observable_list:
observable_op = SparsePauliOp(observable)
observable_op = observable_op.apply_layout(layout)
observable_trans_list.append([observable_op.paulis.to_labels()])
observables_H = observable_trans_list
# Define a sweep over parameter values
params = np.vstack(parameters).T
# Estimate the expectation value for all combinations of
# observables and parameter values, where the pub result will have
# shape (# observables, # parameter values).
pub = (qc_trans, observables_S + observables_H, params)の回路は古典的に計算可能である。 量子コンピューターを使った 。
from qiskit.quantum_info import StabilizerState, Pauli
qc_cliff = qc.assign_parameters({t: 0})
# Get expectation values from experiment
S_expval_real = StabilizerState(qc_cliff).expectation_value(
Pauli("I" * (n_qubits) + "X")
)
S_expval_imag = StabilizerState(qc_cliff).expectation_value(
Pauli("I" * (n_qubits) + "Y")
)
# Get expectation values
S_expval = S_expval_real + 1j * S_expval_imag
H_expval = 0
for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):
# Get expectation values from experiment
expval_real = StabilizerState(qc_cliff).expectation_value(
Pauli(pauli[::-1].to_label() + "X")
)
expval_imag = StabilizerState(qc_cliff).expectation_value(
Pauli(pauli[::-1].to_label() + "Y")
)
expval = expval_real + 1j * expval_imag
# Fill-in matrix elements
H_expval += coeff * expval
print(H_expval)Output:
(10+0j)
効率的なハダマード・テストを用いることで、ゲートの深さを1桁減らすことができたが、それでも最先端のエラー緩和を必要とするには十分な深さである。 以下では、使用されるミティゲーションの属性を指定する。 使用される手法はどれも重要だが、特に確率的エラー増幅(PEA )について言及する価値がある。 この強力なテクニックには、大量の量子オーバーヘッドが伴う。 ここで行われた計算は、実際の量子コンピューターで実行すると20分以上かかる。 精度とオーバーヘッドを増減させるために、以下のパラメータを調整するとよいだろう。 以下のデフォルト設定では、忠実度の高い結果が得られる。
# Experiment options
num_randomizations = 300
num_randomizations_learning = 20
max_batch_circuits = 20
shots_per_randomization = 100
learning_pair_depths = [0, 4, 24]
noise_factors = [1, 1.3, 1.6]
# Base option formatting
options = {
# Builtin resilience settings for ZNE
"resilience": {
"measure_mitigation": True,
"zne_mitigation": True,
"zne": {"noise_factors": noise_factors},
# TREX noise learning configuration
"measure_noise_learning": {
"num_randomizations": num_randomizations_learning,
"shots_per_randomization": shots_per_randomization,
},
# PEA noise model configuration
"layer_noise_learning": {
"max_layers_to_learn": 10,
"layer_pair_depths": learning_pair_depths,
"shots_per_randomization": shots_per_randomization,
"num_randomizations": num_randomizations_learning,
},
},
# Randomization configuration
"twirling": {
"num_randomizations": num_randomizations,
"shots_per_randomization": shots_per_randomization,
"strategy": "all",
},
# Experimental settings for PEA method
"experimental": {
# # Just in case, disable any further qiskit transpilation not related to twirling / DD
# "skip_transpilation": True,
# Execution configuration
"execution": {
"max_pubs_per_batch_job": max_batch_circuits,
"fast_parametric_update": True,
},
# Error Mitigation configuration
"resilience": {
# ZNE Configuration
"zne": {
"amplifier": "pea",
"return_all_extrapolated": True,
"return_unextrapolated": True,
"extrapolated_noise_factors": [0] + noise_factors,
}
},
},
}最後に、 と の回路をEstimatorで実行する。
# This job required 17 minutes of QPU time to run on a Heron r2 processor. This is only an estimate.
# Your execution time may vary.
with Batch(backend=backend) as batch:
# Estimator
estimator = Estimator(mode=batch, options=options)
job = estimator.run([pub], precision=1)4.4 ステップ4. 結果の後処理と分析
私たちが量子コンピュータから得たものは、 の個々の行列要素と、 の行列要素を構成する通約パウリ群である。これらの項を組み合わせて行列を復元し、一般化固有値問題を解くことができるようにしなければならない。
# Store the outputs as 'results'.
results = job.result()[0]有効ハミルトニアンとオーバーラップ行列を計算する
まず、制御されていない時間発展中に 状態によって蓄積された位相を計算する
prefactors = [
np.exp(-1j * sum([c for p, c in H_op.to_list() if "Z" in p]) * i * dt)
for i in range(1, krylov_dim)
]回路の実行結果が得られたら、データを後処理して、以下の行列要素を計算することができる。
# Assemble S, the overlap matrix of dimension D:
S_first_row = np.zeros(krylov_dim, dtype=complex)
S_first_row[0] = 1 + 0j
# Add in ancilla-only measurements:
for i in range(krylov_dim - 1):
# Get expectation values from experiment
expval_real = results.data.evs[0][0][i] # automatic extrapolated evs if ZNE is used
expval_imag = results.data.evs[1][0][i] # automatic extrapolated evs if ZNE is used
# Get expectation values
expval = expval_real + 1j * expval_imag
S_first_row[i + 1] += prefactors[i] * expval
S_first_row_list = S_first_row.tolist() # for saving purposes
S_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)
# Distribute entries from first row across matrix:
for i, j in it.product(range(krylov_dim), repeat=2):
if i >= j:
S_circ[j, i] = S_first_row[i - j]
else:
S_circ[j, i] = np.conj(S_first_row[j - i])from sympy import Matrix
Matrix(S_circ)Output:
そして
import itertools
# Assemble S, the overlap matrix of dimension D:
H_first_row = np.zeros(krylov_dim, dtype=complex)
H_first_row[0] = H_expval
for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):
# Add in ancilla-only measurements:
for i in range(krylov_dim - 1):
# Get expectation values from experiment
expval_real = results.data.evs[2 + 2 * obs_idx][0][
i
] # automatic extrapolated evs if ZNE is used
expval_imag = results.data.evs[2 + 2 * obs_idx + 1][0][
i
] # automatic extrapolated evs if ZNE is used
# Get expectation values
expval = expval_real + 1j * expval_imag
H_first_row[i + 1] += prefactors[i] * coeff * expval
H_first_row_list = H_first_row.tolist()
H_eff_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)
# Distribute entries from first row across matrix:
for i, j in itertools.product(range(krylov_dim), repeat=2):
if i >= j:
H_eff_circ[j, i] = H_first_row[i - j]
else:
H_eff_circ[j, i] = np.conj(H_first_row[j - i])from sympy import Matrix
Matrix(H_eff_circ)Output:
最後に、 の一般化固有値問題を解くことができる:
を計算し、基底状態のエネルギーの推定値を得る。
gnd_en_circ_est_list = []
for d in range(1, krylov_dim + 1):
# Solve generalized eigenvalue problem
gnd_en_circ_est = solve_regularized_gen_eig(
H_eff_circ[:d, :d], S_circ[:d, :d], threshold=1e-1
)
gnd_en_circ_est_list.append(gnd_en_circ_est)
print("The estimated ground state energy is: ", gnd_en_circ_est)Output:
The estimated ground state energy is: 10.0
The estimated ground state energy is: 5.933953916292923
The estimated ground state energy is: 4.4101773995740645
The estimated ground state energy is: 3.921288588521255
一粒子セクターの場合、ハミルトニアンのこのセクターの基底状態を古典的に効率よく計算することができる
gs_en = single_particle_gs(H_op, n_qubits)Output:
n_sys_qubits 10
n_exc 1 , subspace dimension 11
single particle ground state energy: 2.391547869638771
len(H_op)Output:
27
plt.plot(
range(1, krylov_dim + 1),
gnd_en_circ_est_list,
color="blue",
linestyle="-.",
label="KQD estimate",
)
plt.plot(
range(1, krylov_dim + 1),
[gs_en] * krylov_dim,
color="red",
linestyle="-",
label="exact",
)
plt.xticks(range(1, krylov_dim + 1), range(1, krylov_dim + 1))
plt.legend()
plt.xlabel("Krylov space dimension")
plt.ylabel("Energy")
plt.title("Estimating Ground state energy with Krylov Quantum Diagonalization")
plt.show()Output:
5. 考察と発展
要約すると、まず参照状態から始め、それをさまざまな期間進化させてユニタリー・クリロフ部分空間を生成する。 ハミルトニアンをその部分空間に投影する。 また、部分空間ベクトルの重なりも推定する。 最後に、低次元の一般化固有値問題を古典的に解く。
古典的なクリロフ技術と量子力学的なクリロフ技術の計算コストを決定するものを比較してみよう。 すべてのステップにおいて、古典的アプローチと量子的アプローチの間に完全な類似性があるわけではない。 この表は、さまざまなステップのスケーリングを集めたものである。
ハミルトニアンには、一般的に(互いに可換でないため)同時に測定できない項が含まれていることを思い出してください。 ハミルトニアンの項を、すべて同時に測定可能な、互いに可換なパウリ演算子のグループに分類します。また、互いに可換でないすべての項を説明するためには、このようなグループを多数必要とする場合があります。 量子コンピュータ上で を構築するには、ハミルトニアン内の可換なパウリ文字列の各グループごとに個別の測定を行う必要があり、そのそれぞれに多数のショットが必要となる。 個の異なる行列要素について、これを行う必要があり、これらは 通りの異なる時間発展因子の組み合わせに対応する。 これを削減する方法がいくつかある場合もありますが、この大まかな処理では、これに必要な時間は のオーダーで増加します。 の各要素は推定する必要がありますが、その計算量は のオーダーで増加します。最後に、投影空間における一般化固有値問題を解くには、従来の手法では の計算量が必要です。
量子クリロフ対角化は、ハミルトニアン中のパウリ群の数が比較的少ない場合に有効であることがわかる。 これらのスケーリング依存性は、クリロフ法が有用なアプリケーションとそうでないアプリケーションを示唆している。 ハミルトニアンの中には、量子ビットに写像したときに複雑さを増すものがあり、多数の非共通パウリ文字列を含み、少数の共通群に簡単に分割できないものがある。 例えば、量子化学の問題ではよくあることだ。 この複雑さは、近い将来の量子コンピューターに2つの主要な課題をもたらす:
- の各要素の推定は、項数が多いため計算コストが高くなる。
- 必要なトロッター回路は法外に深くなる。
上記の2点は、量子コンピューターがフォールトトレランス(耐障害性)に到達すれば問題は少なくなるが、当面は考慮しなければならない。 量子化学の系よりも "単純な "写像を持つ系であっても、ハミルトニアンに非共約項が多すぎる場合には、同じ障害が発生する可能性がある。 クリロフ法は、ハミルトニアンが比較的少数の通約パウリ群に分割でき、 、トロッター回路の実装が容易な場合に最も有用である。 例えば、物理学で注目されている多くの格子モデルでは、これらの条件が両方とも満たされている。 KQDは、基底状態についてほとんど知られていない場合に特に有効である。 これは、その固有の収束保証と、地上状態に関する知識が不十分であるために他の方法が使えないようなシナリオで適用できることに起因する。
KQDは強力なツールであるが、プロトコルの時間のかかる側面、特に投影ハミルトニアンの各要素の推定とクリロフ状態のオーバーラップには改善の余地がある。 別のアプローチとしては、クリロフ法をサンプリングベースの手法と組み合わせて活用する方法がある。
6. 付録
付録I:実時間発展からのクリロフ部分空間
ユニタリー・クリロフ空間は次のように定義される
後で決めるタイムステップ 。 仮に が偶数であると仮定する。 を定義する。ハミルトニアンを上記のクリロフ空間に射影すると,クリロフ空間と区別がつかないことに注意してください.
つまり、すべての時間進化が タイムステップだけ後ろにシフトしている。 見分けがつかない理由は、行列の要素にある
は、進化時間の全体的なシフトに対して不変であり、時間進化はハミルトニアンと通約するからである。 奇数 については、 の分析を使うことができる。
このクリロフ空間のどこかに、低エネルギー状態が必ず存在することを示したい。 これは [3] の定理 3.1 :
請求項1: ハミルトニアンのスペクトル範囲(つまり、基底状態エネルギーと最大エネルギーの間)のエネルギー に対して、次のような関数 が存在する。
- から 離れたところにある のすべての値に対して、つまり指数関数的に抑制される。
- に対する の線形結合である。
以下に証明を示すが、完全で厳密な議論を理解したい人以外は読み飛ばしても構わない。 今のところは、上記の主張の意味に焦点を当てる。 上記の性質3により、上記のシフト・クリロフ空間は状態 を含むことがわかる。これが我々の低エネルギー状態である。 その理由を知るために、 をエネルギー固有ベーシスで書いてみよう:
ここで はk番目のエネルギー固有状態であり、 は初期状態 におけるその振幅である。これによって表現すると、 は次式で与えられる
が固有状態 に作用するとき、 を で置き換えることができるという事実を利用している。したがって、この状態のエネルギー誤差は
これを理解しやすい上界にするために、まず分子の和を の項と の項に分ける:
第一項を で上界することができる、
ここで、最初のステップは、和のすべての について 、2番目のステップは、分子の和が分母の和の部分集合であるため、続く。 第2項については、まず分母を 。 :すべてを足し合わせると、次のようになる
残されたものを単純化するために、これらのすべての 、 の定義によって、 。さらに、 を上界し、 を上界すると、次のようになる
これは、どのような についても成り立つので、 を目標誤差に等しく設定すれば、上の誤差境界は、クリロフ次元 に従って指数関数的に収束する。また、 の場合、 の項は、実際には、上記の境界から完全になくなることに注意してください。
議論を完結させるために、上記はクリロフ空間における最低エネルギー状態のエネルギー誤差ではなく、特定の状態 のエネルギー誤差に過ぎないことにまず注意する。 しかし、(Rayleigh-Ritz) 変分原理により、クリロフ空間内の最低エネルギー状態のエネルギー誤差は、クリロフ空間内の任意の状態のエネルギー誤差によって上界されるため、上記は最低エネルギー状態のエネルギー誤差、つまりクリロフ量子対角化アルゴリズムの出力に対する上界でもある。
上記と同様の分析が、ノイズとノートで説明した閾値処理手順を追加して実施できる。 この分析については [2] と [4] を参照のこと。
付録II:請求項1の証明
以下はほとんど [3] の定理 3.1: とし、 を最大次数 の残差多項式(0での値が1である多項式)の空間とする。に対する解は
次と同一である
となり、対応する最小値は
我々はこれを複素指数で自然に表現できる関数に変換したい。なぜなら、それが量子クリロフ空間を生成する実時間進化だからである。 そのためには、ハミルトニアンのスペクトル範囲内のエネルギーを、 :defineの範囲内の数に変換する以下の変換を導入するのが便利である
ここで は のようなタイムス テップである。 が から遠ざかるにつれて、 と が成長することに注意。
ここで、パラメータa, b, dを , , d = int( r/2 ) に設定した多項式 を使い、関数を定義する:
ここで、 は基底状態のエネルギーである。 が次数 の三角多項式、つまり に対する の線形結合であることは、 を挿入することでわかる。さらに、上記の の定義から、 と、 のようなスペクトル範囲内の任意の に対して、次のようになる
参照資料:
[1 ]https://arxiv.org/abs/2407.14431
[2 ]https://arxiv.org/abs/1811.09025
[3 ]https://people.math.ethz.ch/~mhg/pub/biksm.pdf
[4 ]https://academic.oup.com/book/36426
[5] https://en.wikipedia.org/wiki/Krylov
[6] クリロフ部分空間法:原理と分析, イェルク・リーゼン, ズデネク・ストラコス https://academic.oup.com/book/36426
[7] スパース線形システムの反復法」 ユセフ・サード著
[8] "MINRES-QLP: A Krylov Subspace Method for Indefinite or Singular Symmetric Systems" by Sou-Cheng Choi, Christopher Paige, and Michael Saunders. マイケル・サンダース( https://epubs.siam.org/doi/10.1137/100787921 )
[9] イーサン・エッパリー、リン・リン、中務裕司。 「量子部分空間対角化の理論」。 SIAM Journal on Matrix Analysis and Applications 43, 1263-1290 (2022).