量子フーリエ変換を眺める
目的
Qiskit textbook の 量子フーリエ変換 を見ると、Bloch 球上のアニメーションが掲載されている。そう言えば以前に勉強した時は「ふ〜ん」で流した気がするので、少し真面目に見てみようという企画。
量子フーリエ変換とは?
量子コンピュータの色々な FTQC アルゴリズム、例えば量子位相推定や Shor のアルゴリズムで利用されるモジュールである。形式的には、古典的な離散フーリエ変換と対をなす形で定義される。
離散フーリエ変換
ここで、
これは積分の形で書くと以下のようになる:
離散フーリエ変換によって、
量子フーリエ変換
量子フーリエ変換は、この離散フーリエ変換を正規直交基底、とりわけ計算基底に拡張したものとして定義される。この構成は超函数 (distribution) のフーリエ変換の定義と同様である。
この構成法と同様にして、量子フーリエ変換は以下で定義される。なお、0101
を使って、
定義
ここで、
具体的な計算
この右辺を積分の順序交換を用いて計算すると、
となる。この事から量子フーリエ変換は
と書ける。この内容は 量子フーリエ変換 と符合している。
フーリエ基底での計算
量子フーリエ変換 の「2.1 フーリエ基底での計算」をもうちょっと具体的に計算して眺めたいというのがここからのテーマである。今回は量子回路を使わずに NumPy
の計算だけで済ませる。
準備
幾つか準備が必要である。今回複数量子ビットのケースを見たいが、量子フーリエ変換後は複合システムの状態ベクトルが現れる。一方、Qiskit textbook のアニメーションは個別の量子ビットに対する部分システムを描画している。このためには複合システムを密度行列で表した上で部分トレースをとって部分システムの状態を割り出す必要がある。
詳細は 量子コンピュータと量子通信I などに譲るとして、システム A とシステム B からなる複合システムの密度行列が
要するに、全体で見ると
以下、NumPy
で実装をするが、ストーリーは以下のようになっている:
- 計算基底に量子フーリエ変換を適用して状態ベクトルを得る。
- 状態ベクトルから密度行列を作る。
- 密度行列に部分トレースを適用して、部分システムとしての各量子ビットの状態を決定する。
NumPy 実装
密度行列を作るまでの関数らの定義を一気に掲載する:
import numpy as np
I = np.eye(2)
ZERO = np.array([1., 0.])
ONE = np.array([0., 1.])
# bin_num は |5> であれば '0101' を期待している。
def to_statevec(bin_num: str, n_qubits: int = 4) -> int | np.ndarray:
vec = 1
for c in bin_num:
v = ZERO if c == '0' else ONE
vec = np.kron(vec, v)
return vec
# 状態ベクトルから密度行列を計算する。
def density_matrix(sv: np.ndarray) -> np.ndarray:
return sv.reshape(-1, 1) @ sv.reshape(1, sv.shape[0]).conj()
# https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/quantum_info/states/densitymatrix.py
def dm2sv(dm: np.ndarray) -> np.ndarray:
evals, evecs = np.linalg.eig(dm)
psi = evecs[:, np.argmax(evals)]
if np.isclose(psi.real[0], 0).all():
if psi[0].imag >= 0:
psi *= -1.j
else:
psi *= 1.j
return psi
最後に密度行列から状態ベクトルをとりだす関数を定義している。一般にはこれは純粋状態に由来する密度行列でないと無理なのだが、今回扱う範囲ではそのようなケースのみなので、思い切り端折って実装している。この実装は Qisikit の https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/quantum_info/states/densitymatrix.py を拝借した。
また、量子フーリエ変換は定義をそのまま実装して以下のようになる:
def qft(x: int, n_qubits: int = 4):
N = 2**n_qubits
result = 0
for i in range(N):
bin_num = bin(i)[2:].zfill(n_qubits)
result += np.exp(2*np.pi*1.j*(i*x)/N)*to_statevec(bin_num, n_qubits)
return result / np.sqrt(N)
部分トレースをとる処理は汚くなったが以下のようにした:
def partial_trace(vec: np.ndarray, target_qubit: int, n_qubits: int = 4):
def partial_basis_generator(target_qubit: int, n_qubits: int = 4):
if n_qubits <= 1:
return
N = 2**(n_qubits-1)
for i in range(N):
bin_num = bin(i)[2:].zfill(n_qubits-1)
bin_num_former = bin_num[:n_qubits-target_qubit-1]
bin_num_latter = bin_num[n_qubits-target_qubit-1:]
sv1 = to_statevec(bin_num_former)
if isinstance(sv1, np.ndarray):
sv1 = sv1.reshape(-1, 1)
sv2 = to_statevec(bin_num_latter)
if isinstance(sv2, np.ndarray):
sv2 = sv2.reshape(-1, 1)
yield np.kron(np.kron(sv1, I), sv2)
dm = density_matrix(sv)
result = 0
for basis in partial_basis_generator(target_qubit, n_qubits):
result += basis.T.conj() @ dm @ basis
return result
target_qubit
番目の量子ビットに対応する部分システムの情報を取り出すものとして実装している。target_qubit
番目の量子ビットを除いた計算基底を順次返すジェネレータ partial_basis_generator
を用意して、(3) 式を参考に以下の数式を実行している。
ここで、target_qubit
番目の量子ビットに対応する部分システムを A、それ以外の量子ビットに対応する部分システムを B とし、target_qubit
番目の量子ビットを除いた計算基底全体をわたるとする[1]。
これで Qiskit textbook のアニメーションを検証する準備ができた。
検証
まずは 1 量子ビットの時から見よう。この時、量子フーリエ変換はただのアダマールゲート H であるので、
print(qft(0, 1))
print(qft(1, 1))
[0.70711+0.j 0.70711+0.j]
[ 0.70711+0.j -0.70711+0.j]
期待する結果、np.round(..., 5)
したものを掲載している。
次に 4 量子ビットのケースを検証しよう:
n_qubits = 4
for x in range(6+1):
print(f'{x=}')
sv = qft(x, n_qubits)
for i in range(n_qubits):
v = np.round(dm2sv(partial_trace(sv, i, 4)), 5)
print(i, v)
print()
x=0
0 [0.70711+0.j 0.70711+0.j]
1 [0.70711+0.j 0.70711+0.j]
2 [0.70711+0.j 0.70711+0.j]
3 [0.70711+0.j 0.70711+0.j]x=1
0 [0.70711+0.j 0.65328+0.2706j]
1 [0.5 -0.5j 0.70711+0.j ]
2 [0.70711+0.j 0. +0.70711j]
3 [ 0.70711+0.j -0.70711+0.j]x=2
0 [0.70711+0.j 0.5 +0.5j]
1 [0.70711+0.j 0. +0.70711j]
2 [ 0.70711+0.j -0.70711+0.j]
3 [0.70711+0.j 0.70711-0.j]x=3
0 [0.70711+0.j 0.2706 +0.65328j]
1 [ 0.70711+0.j -0.5 +0.5j]
2 [0.70711+0.j 0. -0.70711j]
3 [ 0.70711+0.j -0.70711+0.j]x=4
0 [0.70711+0.j 0. +0.70711j]
1 [ 0.70711+0.j -0.70711+0.j]
2 [0.70711+0.j 0.70711-0.j]
3 [0.70711+0.j 0.70711-0.j]x=5
0 [-0.2706 -0.65328j 0.70711+0.j ]
1 [ 0.70711+0.j -0.5 -0.5j]
2 [0.70711+0.j 0. +0.70711j]
3 [ 0.70711+0.j -0.70711+0.j]x=6
0 [ 0.70711+0.j -0.5 +0.5j]
1 [0.70711+0.j 0. -0.70711j]
2 [ 0.70711+0.j -0.70711-0.j]
3 [0.70711+0.j 0.70711-0.j]
数字なので分かりにくいが、
これで、0 番目の量子ビットは
まとめ
Qiskit textbook のアニメーションを NumPy
で実装しようとすると意外と面倒くさいことが分かったが、実装して確認すると符合する結果が得られた。
フーリエ何某の関連では “遠方は激しく振動する (ことで積分がキャンセルする; 離散的な標語としては級数が収束する)” といった感覚がある。今回はそう単純に当てはめられはしないのだが、何となく序数の大きい量子ビットでの状況は位相の変化が激しいという「これ系のコンテキストでよくある感覚のもの」が出てきているので馴染みやすい。
なお、こんな面倒くさいことをしなくても、Qiskit textbook を読み進めれば分かるように「4. 量子フーリエ変換」の数式を経て、具体的に純粋状態のテンソル積で書き下せることが分かる。
-
やや苦しい数式であり、厳密ではない。例えば 4 量子ビットのケースで、2 番目の量子ビットに対する状態を取り出したい場合にはこの数式では不完全である。
partial_basis_generator
では 0 番目と 1 番目の量子ビットの計算基底からなるベクトルと、3 番目の量子ビットの計算基底のベクトルの間に単位行列を挟む形でクロネッカー積をとっている。 ↩︎
Discussion