はじめに
量子誤り訂正のシミュレーションをするのに有用なStimというPythonライブラリを使ってみる.
普通の量子回路シミュレータよりも動作が軽く,視覚化するためのライブラリが揃っている.QECを専門にやる人はこの辺を使うらしい.しかし,日本語でのチュートリアルに乏しいため公式のチュートリアルを訳しつつ色々と試してみることにする.
なお,筆者は初心者のため,間違い等があれば指摘していただければ幸いである.
導入
使用するライブラリ一覧(動作させたときのver.)
- numpy(1.26.4)
- scipy(1.14.0)
- stim(1.13)
まずはライブラリのインポート
import stim
早速,シンプルな回路を定義する.アダマールゲートとCNOTゲートをかけてBellペアを作るお馴染みの回路を作る.
circuit = stim.Circuit()
# 0番目の量子ビットにHをかけ,0から1にかけてCNOTをかけてBellペアを作る.
circuit.append("H", [0])
circuit.append("CNOT", [0, 1])
# そのあとにZ測定する.
circuit.append("M", [0, 1])
この回路を視覚化する.視覚化の方法は何個かある.
最初はデータが格納されているところを直接みる方法で表示してみる.
circuit
実行結果
stim.Circuit('''
H 0
CX 0 1
M 0 1
''')
次にdiagram()
を使って回路っぽく視覚化する.
circuit.diagram()
実行結果
q0: -H-@-M:rec[0]-
|
q1: ---X-M:rec[1]-
diagram()
の中に色々入れれば画像で出力したり3Dで出力したりすることもできる.
circuit.diagram('timeline-svg')
circuit.diagram('timeline-3d')
実行結果
3Dの方は実際に自分で実行してみるといいだろう.3Dモデルとして得られるので動かすと結構感動する.
サンプラーの定義と測定
測定のサンプラーは,.compile_sampler()
で定義できる.試しに前のBellペアの回路で10ショット分サンプリングしてみよう.
sampler = circuit.compile_sampler()
print(sampler.sample(shots=10))
実行結果
[[ True True]
[False False]
[ True True]
[False False]
[ True True]
[ True True]
[False False]
[ True True]
[False False]
[False False]]
$\ket0$のときはFalse,$\ket1$のときはTrueを返す.
shot数を10にしているため行数が10になっており,2-qubit系の測定のため列数が2になっていることがわかる.また,同じ列では真偽が一致していることがわかる.
これはBellペアでは出力状態が,
\ket\psi = \frac1{\sqrt2} \ket{00} + \frac1{\sqrt2}\ket{11}
となっていることによる.すなわち,0番目のqubitの測定結果と1番目のqubitの測定結果は必ず一致することがわかる.
誤り付きの計算とパリティチェック
量子誤り訂正計算では,一つの量子状態を複数の量子状態で表現することによりノイズへの耐性を得ている.ただし古典計算と違い,測定を行うと状態が変化してしまう.また,エラーの種類もたくさんあり,単純な多数決をとるだけでは実現できない.そのため適切にパリティチェックを行い,どのようなエラーが発生しているかを知り,逐次エラーを訂正していく必要がある.
簡単な例として,回路内に一定確率で発生するビット反転エラー(Xエラー)と測定結果のパリティチェックを追加する.
append
で記述することもできるが,今回は直接回路を記述する.
circuit = stim.Circuit("""
H 0
TICK
CX 0 1
X_ERROR(0.2) 0 1
TICK
M 0 1
DETECTOR rec[-1] rec[-2]
""")
TICKは時間を進ませる操作,X_ERROR(0.2) 0 1は0, 1番目のqubitに0.2の確率でXエラーを発生させる操作,DETECTORは,指定した二つの測定結果のパリティを検査する操作を表す.なお,rec[-1]は一個前の最新の測定結果,[-2]は二個前の測定結果を参照することを表す.
この記述を上の方の結果と比較してみるといいだろう.
それでは,何回かshotを打ってパリティを見てみよう.パリティ検査のサンプラーは.compile_detector_sampler
で定義できる.
sampler = circuit.compile_detector_sampler()
print(sampler.sample(shots=10))
実行結果
[[False]
[False]
[False]
[False]
[False]
[False]
[ True]
[ True]
[False]
[False]]
パリティが0だとFalse,パリティが1だとTrueを返す.
- どちらにもエラーが発生しない -> 0.8*0.8 = 0.64 (False)
- どちらかにエラーが発生する -> 20.20.8 = 0.32 (True)
- どちらにもエラーが発生する -> 0.2*0.2 = 0.04 (False)
1つあたりのエラー確率は0.2だから,上に示す確率でパリティ検査の結果が得られるはずである.たくさんshotを打って本当にそうなるのか確かめてみよう.
import numpy as np
print(np.sum(sampler.sample(shots=10**6)) / 10**6)
実行結果
0.31998
結果は実行するたびに変わるが,0.32に近い値となることが確かめられた.
なお,上の例では,Trueの場合は確実にエラーが発生したことがわかるが,Falseの場合は両方にエラーが発生したのか,両方ともエラーが発生していないのかの区別がつかない.Trueの場合も,どちらにエラーが発生したのかまではわからない.さらにいうと,実際の量子回路では測定自体にエラーが発生したり,XエラーだけでなくZエラーが発生したりもする.これをうまくやるのが表面符号だったりトポロジカル符号だったりする.次回は,公式チュートリアルにある複雑な回路を実装してスケーリングなどを見ていく.
おわりに
公式チュートリアルを踏まえてStimの簡単な動作確認を行った.
多少端折ったり説明を加えたりしたところもあるが,基本はチュートリアルに沿ったつもりである.
また,間違い等あれば指摘していただけると幸いである.