LoginSignup
6
4

More than 1 year has passed since last update.

量子情報理論の基本:表面符号による論理演算(Braiding)

Last updated at Posted at 2020-09-23

$$
\def\bra#1{\mathinner{\left\langle{#1}\right|}}
\def\ket#1{\mathinner{\left|{#1}\right\rangle}}
\def\braket#1#2{\mathinner{\left\langle{#1}\middle|#2\right\rangle}}
$$

はじめに

前回の記事でトーラスにマッピングされた格子上に配備された量子ビットを使って、性能の良い誤り訂正符号(表面符号)が構築できるということを見てきました。ところがこのようなトーラスを実際に物理的に作るのはとても難しいですし、表現できる論理量子ビット数はトーラスの穴の数に比例するということなので、この方向で大きな論理ビット数を持った量子コンピュータを作成するのはほぼ不可能と思われます。そこで、トーラスではない単なる平面にある種の欠陥を複数作り、それらを移動させて組み紐のように互いに巻き付け合う(Braidさせる)ことで論理演算を実行できる表面符号方式が提案されました。今回はそれを取り上げます。

この後の節で何を説明しようとしているか最初に言っておきます。まず、「理論説明」ではどんな欠陥を作れば論理ビットを表現できるか、どのように論理$X$演算子や論理$Z$演算子が定義できるか、さらに欠陥を移動(Braid)させることによって論理的な2量子ビット演算(論理CNOT演算)が実現できることを示します。「動作確認」では量子計算シミュレータqlazyを使って、平面上に作成した欠陥をBraidさせることで実際にCNOT演算が実行できることを確認します。

参考にさせていただいたのは、以下の文献です。

  1. 小柴、森前、藤井「観測に基づく量子計算」コロナ社(2017年)
  2. K.Fujii,"Quantum Computation with Topological Codes - from qubit to topological fault-tolerance",arXiv:1504.01444v1 [quant-ph] 7 Apr 2015

理論説明

面演算子と頂点演算子

下図に示すような格子を考えます。今回は前回のような周期的境界条件を想定しませんので単なる平面上に定義された格子だと思ってください。

fig1.png

この上に面演算子と頂点演算子を定義します。面演算子は各面を囲む4つの辺上の$Z$演算子のテンソル積になっており、同じ形で整然と並んでいます。が、頂点演算子は少々違います。平面の端にある各頂点は3つまたは2つの辺のみに接続するので、平面の端にある頂点演算子の$X$演算子の数は4つではありません。このように定義された面演算子と頂点演算子を生成元とするスタビライザー群を考えます。格子の数が縦横$M \times N$だとすると面演算子の数は$MN$で、頂点演算子の数は$(M+1)(N+1)=MN+M+N+1$なので、生成元の数は合わせて$2MN+M+N+1$です。が、すべて独立というわけではありません。上図を見ながら少し考えればわかると思いますが、すべての頂点演算子の積は$I$(恒等演算子)になります1。つまり、あるひとつの頂点演算子を任意に選ぶと、それは他のすべての頂点演算子の積で表せるということなので、独立な生成元の個数は$MN+M+N+1$から1つ減って$MN+M+N$となります。一方、量子ビット数は$(M+1)N+M(N+1)=2MN+M+N$個で、独立な生成元の個数と同じになります。したがって、このスタビライザー群によって量子状態はユニークに決まります。この状態のことをここでは「真空」と呼ぶことにします。この真空を出発点にして欠陥を生成したり移動させたりして量子演算が行えることを以下の節で順に説明していきます。

真空状態の作成

はじめに真空状態の作成方法です。格子状に並んだ量子ビット集団にどんな量子演算を行えば、このようなスタビライザー状態(真空状態)が作れるかを考えてみます。一般にスタビライザー群

S = <g_1, g_2, g_3, \cdots , g_n>  \tag{1}

で規定されるスタビライザー状態は、初期状態$\ket{00 \cdots 0}$のもとで$g_1,g_2, \cdots , g_n$を順に測定していけば得られます。以前の記事でも説明しましたが、簡単に復習してみます。状態$\ket{00 \cdots 0}$のもとで$g_1$を測定するというのは射影演算子$(I \pm g_1)/2$を状態に作用させることに等しいです。ここで符号$\pm$は測定値が$+1$だったか$-1$だったかに対応しています。測定後の状態は、

g_1 \frac{I \pm g_1}{2} \ket{00 \cdots 0} = \pm \frac{I \pm g_1}{2} \ket{00 \cdots 0}  \tag{2}

となることから、$g_1$の固有値$+1$か$-1$に対応した固有空間上の状態となります(射影されます)。次にこの測定後の状態に対して$g_2$を測定するとその測定値に応じて$g_2$の固有値$+1$か$-1$に対応した固有空間に射影されます。さらに$g_3,g_4, \cdots , g_n$という具合に測定をしていくと、最終的に$g_1,g_2, \cdots , g_n$の固有空間の積空間上の状態(同時固有状態)に射影されます。ただし、各々の$g_i$についてどっちの固有値に対応した固有空間に射影されるのかは完全に確率的です。以前の記事では$-1$の固有空間に射影されてしまったら固有値を反転させるような演算を追加実施することをやりました。そうすると、きれいに、式(1)のスタビライザー群に対応したスタビライザー状態が得られます。が、その手続きを端折ると、例えば、

S = <-g_1, g_2, -g_3, \cdots , -g_n>  \tag{3}

のようなスタビライザー群に対応したスタビライザー状態が得られます(1番目、3番目、n番目の生成元の測定値が-1だった場合こうなります)。一般には、

S = <\pm g_1, \pm g_2, \pm g_3, \cdots , \pm g_n>  \tag{4}

において各生成元の測定値に応じて$g_i$の符号が$+$または$-$になったスタビライザー群に対応したスタビライザー状態が得られます。誤り訂正を行う際には各生成元に対するシンドローム測定の基準をあらかじめ保持しておけば良いだけなので、これはこれで全然問題ないです。つまり、測定の結果のスタビライザー群が式(3)のように得られたのだとすると、1番目の生成元$g_1$を測定した結果$-1$が得られたらそれは正常値なのでエラーがなかったと判断できますし、逆に$+1$だった場合はエラーがあったと判断できます、という具合です。

実際にこのような測定を行って真空状態を作成する場合、格子の辺上に配備された量子ビット(データ量子ビット)に加えて、間接測定(シンドローム測定)するための補助量子ビットを追加配備するのが良いです。具体的にどうやるかというと簡単な話で、下図に示したように格子の各面の中央と各頂点に補助量子ビットを追加します。各面の中央に配備された補助量子ビットは面演算子を間接測定(シンドローム測定)するためのもので、各頂点に配備された補助量子ビットは頂点演算子を間接測定(シンドローム測定)するためのものです。

fig2.png

面演算子と頂点演算子を測定する前にまず、すべてのデータ量子ビットと補助量子ビットを$\ket{0}$に初期化しておきます。それから、各面に対応した面演算子を間接測定します。測定しようとしている面に対応した補助量子ビット番号を$f_0$、その面を囲む辺上にあるデータ量子ビット番号を$f_1,f_2,f_3,f_4$として、

f0 --H--*--*--*--*--H-- M
        |  |  |  |
f1 -----Z--|--|--|-----
f2 --------Z--|--|-----
f3 -----------Z--|-----
f4 --------------Z-----

のような量子回路で面演算子を測定します。これをすべての面で実行します。次に、各頂点に対応した頂点演算子を測定します。測定しようとしている頂点に対応した補助量子ビット番号を$v_0$、その頂点に接続している辺上にあるデータ量子ビット番号を$v_1,v_2,v_3,v_4$として、

v0 --H--*--*--*--*--H-- M
        |  |  |  |
v1 -----X--|--|--|-----
v2 --------X--|--|-----
v3 -----------X--|-----
v4 --------------X-----

のような量子回路で頂点演算子を測定します。これをすべての頂点について実行します。これで真空状態が作成されたことになります。

真空状態はひとつの状態です。符号空間ではありません。符号空間にするには少なくとも生成元をひとつ除去して$n-1$個の独立な生成元によるスタビライザー群を考える必要があります。前回は周期的境界条件=トーラスを考えることでその穴に巻き付くループ上に並んだ$X$あるいは$Z$演算子を論理演算子とすることができました。今回は平面上に人為的に欠陥(穴)を作ることで論理演算子を定義することにします。

欠陥対量子ビットの生成

まず「欠陥(defect)」を定義します。欠陥とは面演算子または頂点演算子がまったく定義されていない領域のことを言います。先程説明した真空状態は格子全体に面演算子と頂点演算子がびっしり敷き詰められているようなスタビライザー状態でした。この平面に面演算子(または頂点演算子)がない領域を作れたとするとそれが欠陥です。では、この欠陥をどうやって作れば良いでしょうか。実は真空状態を構成する平面上にあるどれかの量子ビットを測定することで簡単に作ることができます。

下図に示すように格子を構成する隣り合った2つの面に注目します。2つの面は共有する辺を1つ持っていますがその上にある量子ビットに対応した$X$演算子を測定することを考えます。つまり、図中の4番目の量子ビットを$X$基底で測定します(あるいは、4番目の量子ビットにアダマールゲートをかけてから$Z$基底で測定すると考えても同じことです)。

fig3.png

スタビライザー形式の言葉でこの測定を記述してみます2。測定したい演算子$X_4$と反可換な生成元は面演算子$Z_1 Z_3 Z_4 Z_6 \equiv A_1$と面演算子$Z_2 Z_4 Z_5 Z_7 \equiv A_2$の2つのみです。これ以外の面演算子とすべての頂点演算子とは可換です。この場合、反可換な1つの演算子を他の反可換な演算子にかけることで、スタビライザー群を不変に保ちながら、反可換な演算子を1つにすることができます。つまり、スタビライザー群

S = <A_1, A_2, \cdots>  \tag{5}

S = <A_1 A_2, A_2, \cdots>  \tag{6}

のようにして、反可換な生成元を$A_2$のみにすることができます。その上で測定対象である$X_4$を測定するのですが、測定値が$+1$だった場合$A_2$を$X_4$に置き換え、$-1$だった場合$A_2$を$-X_4$に置き換えたスタビライザー群

S = <A_1 A_2, \pm X_4, \cdots>  \tag{7}

に対応したスタビライザー状態が測定後の状態になります。もとの真空状態にあった面演算子$A_1$および$A_2$はなくなり、$A_1 A_2$および$\pm X_4$に置き換わったことがわかると思います。つまり、$X_4$の測定によって$A_1$および$A_2$に相当する部分に2つの欠陥が対生成されたと言えます。また、生成元の中に$\pm X_4$が含まれていることから、この状態は$X_4$の固有状態です。

測定によって欠陥が対生成されましたが式(7)はユニークな状態を表していますので、符号空間ではありません。例えば、$\pm X_4$を除いたスタビライザー群、

S^{\prime} = <A_1 A_2, \cdots>  \tag{8}

が1つの論理量子ビットを記述するための符号空間になります。

では、この符号空間上で論理$X$演算子はどのように定義できるでしょうか。改めて$X_4$を持ってきます。この$X_4$は$S^{\prime}$のすべての生成元と可換であり、かつ、$S^{\prime}$の要素ではないので、論理演算子の役割を果たします。そして、

<A_1 A_2, X_4, \cdots>  \tag{9}

は$X_4$の固有値$+1$に対応した状態で、

<A_1 A_2, -X_4, \cdots>  \tag{10}

は$X_4$の固有値$-1$に対応した状態なので、$X_4$を論理$X$演算子と定義すると、式(9)は$\ket{+^{L}}$、式(10)は$\ket{-^{L}}$を表すことになります(上付きの$L$は論理量子ビットを表すものとします。以下同様)。

一方、論理$Z$演算子は、論理$X$演算子$X_4$と反可換で$S^{\prime}$のすべての生成元と可換である演算子を選べば良いです。例えば、$A_1$はその条件に合致しますので、これを論理$Z$演算子と定義できます3。あるいは、$A_2$を論理$Z$演算子としても良いです。

以上がもっとも簡単な欠陥対の作成方法(=符号空間の作成方法)です。欠陥は通常の格子(primal lattice)上の面演算子の欠如なので、このタイプの欠陥をp型の欠陥と呼びます。もうひとつのタイプの欠陥もあります。それは双対格子上の面演算子(つまり、通常格子における頂点演算子)の欠如です。その作成方法を以下に説明します。

下図に示すような双対格子を構成する隣り合った2つの面に注目します(先程の量子ビット番号は忘れてください。改めて量子ビット番号を振り直しています)。元の通常格子では隣り合った十文字ということになります。この2つの十文字は共有する辺を1つ持っていますがその上にある量子ビットに対応した$Z$演算子を測定することを考えます。つまり、図中の4番目の量子ビットを$Z$基底で測定します。

fig4.png

スタビライザー形式の言葉でこの測定を記述してみます。測定したい演算子$Z_4$と反可換な生成元は頂点演算子$X_1 X_3 X_4 X_6 \equiv B_1$と頂点演算子$X_2 X_4 X_5 X_7 \equiv B_2$の2つのみです。これ以外の頂点演算子とすべての面演算子とは可換です。先程の議論と同様に測定後の状態は、

S = <B_1 B_2, \pm Z_4, \cdots>  \tag{11}

となり、これは$\pm Z_4$の固有状態です。ここで$\pm Z_4$を除いた

S^{\prime} = <B_1 B_2, \cdots>  \tag{12}

を考えると、これが符号空間を規定するスタビライザー群となります。そこで改めて$Z_4$を持ってくるとそれは$S^{\prime}$のすべての生成元と可換で、かつ、$S^{\prime}$の要素ではないので、論理演算子の役割を果たします。そして、

<B_1 B_2, Z_4, \cdots>  \tag{13}

は$Z_4$の固有値$+1$に対応した状態で、

<B_1 B_2, -Z_4, \cdots>  \tag{14}

は$Z_4$の固有値$-1$に対応した状態なので、$Z_4$を論理$Z$演算子と定義すると、式(13)は$\ket{0^{L}}$、式(14)は$\ket{1^{L}}$を表すことになります。

一方、論理$X$演算子は、$Z_4$と反可換で$S$のすべての生成元と可換である演算子を選べば良いです。例えば、$B_1$はその条件に合致しますので、これを論理$X$演算子と定義して良いです。あるいは、$B_2$を論理$X$演算子としても良いです。

以上がもっとも簡単な双対格子上での欠陥対の作成方法です。欠陥は双対格子(dual lattice)上の面演算子の欠如なので、このタイプの欠陥をd型の欠陥と呼びます。

欠陥対量子ビットの移動

測定を繰り返すことで欠陥の領域を拡大させることもできます。下図のようにp型欠陥の周囲にある量子ビットを$X$基底で測定することで、欠陥の領域は広がります(水色領域)。黒丸で示した量子ビットが測定済みの量子ビットです。

fig5.png

欠陥を消滅させることもできます。まず、下図左のようにp型欠陥を一旦横方向に拡大します。その上で3つ並んでいる欠陥のうち真ん中を消滅させてみます。その面に対応した面演算子を復活させることに等しいので、その面演算子を測定すれば良いです。実際には、面に対応した補助量子ビット(下図では省略していますが)を使って間接測定します。そうすると下図右のようになります。

fig6.png

「なります」と言われてもホント?という声が聞こえてきそうなので、先程のようにスタビライザー形式でこのプロセスを確認してみます。上図左の状態は、式(7)を得たときの議論を延長させれば、

S = <A_1 A_2 A_3, \pm X_5, \pm X_6, \cdots> \tag{15}

となることがわかると思います。ここで$\pm$の符号は$X_5$の測定結果と$X_6$の測定結果に応じて決まります。この状態のもとで$A_2$を測定します。$A_2$は$X_5$および$X_6$と反可換なので、まず、どちらかをどちらかにかけることで反可換なものを1つにします。例えば、

S = <A_1 A_2 A_3, \pm X_5 X_6, \pm X_6, \cdots> \tag{16}

のようにします。そうすると、$A_2$と反可換な生成元は$\pm X_6$のみとなります。この上で$A_2$を測定します。測定結果に応じて、

S = <A_1 A_2 A_3, \pm X_5 X_6, \pm A_2, \cdots> \tag{17}

のように状態は変化します。任意の生成元を別の生成元にかけてもスタビライザー状態は不変です。例えば、3番目の生成元を1番目の生成元にかけてみます。そうすると、

S = <\pm A_1 A_3, \pm X_5 X_6, \pm A_2, \cdots> \tag{18}

となります。これで上図右の状態ができたことになります。つまり、拡大と消滅を組み合わせることで欠陥を移動することができるということです。

ここで、$\pm X_5 X_6$を除いたスタビライザー群で定義された符号空間を考えると、先程の議論と同様に$X_5 X_6$のチェーンを論理$X$演算子とし、$A_1$という欠陥を囲むループを論理$Z$演算子として定義すれば良いことがわかります。

ちなみに、測定値に応じて符号が$+$になったり$-$になったりするのが気持ち悪い場合どうするか?少し補足しておきます。$X_6$を測定した結果、式(15)の$X_6$の符号が$-$になったとします。その場合、$X_6$と反可換で他のすべての生成元と可換な演算子である、論理$Z$演算子$A_3$(または$A_1 A_2$)を持ってきて全体に演算すれば良いです。一方、$A_2$を測定した結果、式(17)の$A_2$の符号が$-$になった場合は、移動する前の論理$X$演算子である$X_5$を全体に演算すれば良いです4。結果、

S = <A_1 A_3, X_5 X_6, A_2, \cdots> \tag{19}

のようなきれいな状態が得られます。これは$X_5 X_6$の固有値$+1$に対応した固有状態になっています。このプロセスはいくらでも連続実行できて、任意の場所に欠陥対を移動させることができます。が、どんな配置になったとしても、これで決まる状態は2つの欠陥をつなぐチェーン上の$X$演算子のテンソル積の固有状態になります5。そして、このチェーン上の$X$演算子のテンソル積は論理$X$演算子の役割を果たし、2つの欠陥のどちらかを囲むループ上の$Z$演算子は論理$Z$演算子の役割を果たします6

以上のことはd型欠陥についても同様で拡大、消滅を繰り返し欠陥を移動することができて、欠陥をつなぐチェーン上の$Z$演算子のテンソル積の固有状態になります。そしてそのチェーン上の$Z$演算子のテンソル積は論理$Z$演算子の役割を果たし、2つの欠陥のどちらかを囲む(双対格子における)ループ上の$X$演算子は論理$X$演算子の役割を果たします。

いままで簡単のため、主に1つの面演算子(または頂点演算子)の欠如による欠陥を考えてきましたが、先程説明したように任意に拡大できます。例えば、下図のような大きな欠陥対も測定を繰り返すだけで簡単に作ることができます。

fig7.png

このように大きな欠陥を作ると、その分論理演算子を構成するパウリ演算子の数が多くなります。つまり、符号の距離が大きくなります7。符号の距離が大きくなると訂正可能な量子ビットが多くなります。具体的には$2t+1$の距離をもつ符号によって$t$量子ビットの誤りまで訂正することができるようになります。ちなみに、上図の場合符号の距離は(数えてみればわかりますが)16なので、訂正可能な量子ビット数は7になります。

Braidingによる2量子ビット演算

前節までの内容をまとめます。真空状態から測定によってp型欠陥対を作成したとすると、その状態はこの欠陥をつなぐチェーン上のパウリ$X$演算子のテンソル積の固有状態になっています。チェーンを除いた生成元、つまり真空にできた2つの(p型)欠陥によって規定される符号空間を考えると、欠陥をつなぐチェーン上のパウリ$X$演算子のテンソル積というのは、この符号空間における論理$X$演算子と見なせます。また、そうだとすると、どちらかの欠陥を囲むループ上のパウリ$Z$演算子のテンソル積は、この符号空間における論理$Z$演算子となります。一方、d型欠陥対を作成したとすると、その状態はこの欠陥をつなぐチェーン上のパウリ$Z$演算子のテンソル積の固有状態になっています。チェーンを除いた生成元、つまり真空にできた2つの(d型)欠陥によって規定される符号空間を考えると、欠陥をつなぐチェーン上のパウリ$Z$演算子のテンソル積というのは、この符号空間における論理$Z$演算子と見なせます。また、そうだとすると、どちらかの欠陥を囲むループ上のパウリ$X$演算子のテンソル積は、この符号空間における論理$X$演算子となります。ということでした。

いま、平面上にp型欠陥対とd型欠陥対が下図のように離れて存在しているとします。p型欠陥対の方は2つの面演算子がなくなった代わりに、なくなった2つの面演算子の積が新たに生成元に加わった格好になっていますので、これで1つの論理量子ビットが表現できていることになります。また、d型欠陥対の方は2つの頂点演算子がなくなった代わりに、なくなった2つの頂点演算子の積が新たに生成元に加わった格好になっていますので、これで1つの論理量子ビットが表現できていることになります。したがって、下図のような欠陥によって2つの論理量子ビットが表現できるようになります。

fig8.png

ここで、p型欠陥対を0番目の論理量子ビット、d型欠陥対を1番目の論理量子ビットとします。0番目の論理量子ビットを論理$X$演算子の固有状態にしたいとします。つまり0番目の量子ビットを$\ket{+^{L}}$または$\ket{-^{L}}$に初期化したいとします。どうやれば良いでしょうか?スタビライザー形式の基本を思い出していただければすぐわかると思います。論理$X$演算子、すなわち欠陥をつなぐチェーン上に置かれた$X$演算子のテンソル積を生成元に加えれば良いです(下図)。これで状態$\ket{+^L}$ができあがります。前節までで説明したようにp型欠陥を対生成してどっちかの欠陥を移動させれば自然に状態$\ket{+^L}$または$\ket{-^L}$のどっちかが得られます。もし$\ket{-^L}$(あるいは$\ket{+^L}$)が得られたとしたら、論理$Z$演算子、すなわち、どちらかの欠陥を囲むループ上のパウリ$Z$演算子のテンソル積をこの状態に作用させることで$\ket{+^L}$(あるいは$\ket{-^L}$)に変えられます。

1番目の量子ビットを$\ket{0^L}$または$\ket{1^L}$に初期化したい場合は、論理$Z$演算子、すなわち欠陥をつなぐチェーン上に置かれた$Z$演算子のテンソル積を生成元に加えれば良いです(下図)。これで状態$\ket{0^L}$ができあがります。前節までで説明したようにd型欠陥を対生成してどっちかの欠陥を移動させれば自然にこの状態$\ket{0^L}$または$\ket{1^L}$のどっちかが得られます。もし$\ket{1^L}$(あるいは$\ket{0^L}$)が得られたとしたら、論理$X$演算子、すなわち、どちらかの欠陥を囲むループ上のパウリ$X$演算子のテンソル積をこの状態に作用させることで$\ket{0^L}$(あるいは$\ket{1^L}$)に変えられます。

fig9.png

では、このような2つの論理量子ビットを使って論理的なCNOT演算ができることを示します。何が実現できれば良いかをスタビライザー形式の言葉で言うと、

CNOT: <XI,IZ> \rightarrow <XX,ZZ>   \tag{20}

という変換です。2つの論理量子ビットに関する量子回路で表すなら、

X --*-- X    I --*-- Z
    |            |
I --X-- X    Z --X-- Z

が実現できれば良いです。

結論を先に言います8。p型欠陥対の一方をd型欠陥の一方の周囲を巻き付けるように一周させるように移動します。例えば、下図(1)のように0番目の論理量子ビットを論理$X$演算子の固有状態(固有値は1)にしておき、下図(2)のようにp型欠陥の一方(右の方)をd型欠陥の一方(右の方)の周りをぐるっと一周させる形に移動させます。そうすると、$X$演算子のチェーンはd型欠陥に巻き付きます。自明なループ上の$X$演算子を作用させても全体の状態は不変なので、下図(3)のような$X$演算子のループ(オレンジ色の線で示しました)を作用させてみると、結局、下図(4)のように、$X$演算子のチェーンは2つのパートに分かれます。一つはもともとあったp型欠陥対をつなぐチェーンで、もうひとつはd型欠陥に巻き付くループです。このループは連続的に収縮しても状態としては不変なので、下図(4)ではd型欠陥を囲む境界まで収縮させています。このループ上の$X$演算子は1番目の論理量子ビットの論理$X$演算子だったので、1番目の論理量子ビットは論理$X$演算子の固有状態(固有値は1)になっています。これで論理的な初期状態$XI$が論理的な終状態$XX$に変化したことになります。

fig10.png
fig11.png

次に、同様な移動によって、論理的な初期状態$IZ$が論理的な終状態$ZZ$に変化することを示します。下図(1)のように1番目の論理量子ビットを論理$Z$演算子の固有状態(固有値は1)にしておき、先程のようにp型欠陥をd型欠陥の周りをぐるっと一周させるように移動させます。p型欠陥がd型欠陥をつなぐ$Z$演算子のチェーンのところにまで到達してさらに移動すると、下図(2)に示したように$Z$演算子のチェーンを押して変形させながら移動する形になります9。自明なループ上の$Z$演算子を作用させても状態は不変なので、下図(3)のような$Z$演算子のループ(オレンジ色の線で示しました)を作用させてみると、結局、下図(4)のように、$Z$演算子のチェーンは2つのパートに分かれます。一つはもともとあったd型欠陥対をつなぐチェーンで、もう一つはp型欠陥に巻き付くループです。このループ上の$Z$演算子は0番目の論理量子ビットの論理$Z$演算子だったので、0番目の論理量子ビットは論理$Z$演算子の固有状態(固有値は1)になっています。これで論理的な初期状態$IZ$が論理的な終状態$ZZ$に変化したことがわかりました。

fig12.png
fig13.png

このようにBraidingによって論理的な2量子ビットに対するCNOT演算ができました。初期状態は$\ket{+^{L} 0^{L}}$でそれにCNOT演算を実施したので、終状態は論理的なBell状態$(\ket{0^{L} 0^{L}} + \ket{1^{L} 1^{L}})/\sqrt{2}$になっています。ということですね。

動作確認

それでは、量子計算シミュレータqlazyを使って、1枚の格子平面に欠陥を生成させることで表面符号を構成し、BraidingによるCNOT演算が正しく実行できることを確認してみます。上で示した例をそのまま実行してみたいと思います。初期状態は$\ket{+^{L} 0^{L}}$でそれにCNOT演算を実施するので、終状態は論理的なBell状態$(\ket{0^{L} 0^{L}} + \ket{1^{L} 1^{L}})/\sqrt{2}$になっているはずです。論理的な$Z$基底で測定すると状態$\ket{0^{L} 0^{L}}$か状態$\ket{1^{L} 1^{L}}$が半々の確率で観測されるはずなので、それを確認します。

実装

全体のPythonコードを示します。

【2021.9.5追記】qlazy最新版でのソースコードはここに置いてあります。

from collections import Counter
from qlazypy import Stabilizer

def get_common_qid(obj_A, obj_B):

    return list(set(obj_A['dat']) & set(obj_B['dat']))

def create_lattice(row, col):

    face = [[None]*col for _ in range(row)]
    vertex = [[None]*(col+1) for _ in range(row+1)]

    q_row = 2 * row + 1
    q_col = 2 * col + 1
    q_id = 0
    for i in range(q_row):
        for j in range(q_col):
            if i%2 == 1 and j%2 == 1: # face
                dat = []
                dat.append((i - 1) * q_col + j)  # up
                dat.append((i + 1) * q_col + j)  # down
                dat.append(i * q_col + (j - 1))  # left
                dat.append(i * q_col + (j + 1))  # right
                face[i//2][j//2] = {'anc': q_id, 'dat': dat}
            elif i%2 == 0 and j%2 == 0: # vertex
                dat = []
                if i > 0: dat.append((i - 1) * q_col + j)          # up
                if i < q_row - 1: dat.append((i + 1) * q_col + j)  # down
                if j > 0: dat.append(i * q_col + (j - 1))          # left
                if j < q_col - 1: dat.append(i * q_col + (j + 1))  # right
                vertex[i//2][j//2] = {'anc': q_id, 'dat': dat}
            q_id += 1

    return {'face': face, 'vertex': vertex}

def initialize(sb, lattice):

    sb.set_all('Z')
    for face_list in lattice['face']:
        for face in face_list:
            sb.h(face['anc'])
            [sb.cz(face['anc'], target) for target in face['dat']]
            sb.h(face['anc'])
            sb.m(qid=[face['anc']])

    for vertex_list in lattice['vertex']:
        for vertex in vertex_list:
            sb.h(vertex['anc'])
            [sb.cx(vertex['anc'], target) for target in vertex['dat']]
            sb.h(vertex['anc'])
            sb.m(qid=[vertex['anc']])

def create_move_defect_p(sb, pos_A, pos_B, path, lattice):

    # create defect pair
    face_A = lattice['face'][pos_A[0]][pos_A[1]]
    face_B = lattice['face'][pos_B[0]][pos_B[1]]
    q = get_common_qid(face_A, face_B)[0]
    md = sb.h(q).m(qid=[q])
    sb.h(q)
    if md.last == '1': [sb.z(i) for i in face_B['dat']]

    # move defect
    chain = [q]
    for i in range(1,len(path)):
        # extend defect
        face_A = lattice['face'][path[i-1][0]][path[i-1][1]]
        face_B = lattice['face'][path[i][0]][path[i][1]]
        q = get_common_qid(face_A, face_B)[0]
        md = sb.h(q).m(qid=[q])
        sb.h(q)
        if md.last == '1': [sb.z(i) for i in face_B['dat']]

        # remove defect
        sb.h(face_A['anc'])
        [sb.cz(face_A['anc'], target) for target in face_A['dat']]
        sb.h(face_A['anc'])
        md = sb.m(qid=[face_A['anc']])
        if md.last == '1': [sb.x(i) for i in chain]

        chain.append(q)

def create_move_defect_d(sb, pos_A, pos_B, path, lattice):

    # create defect pair
    vertex_A = lattice['vertex'][pos_A[0]][pos_A[1]]
    vertex_B = lattice['vertex'][pos_B[0]][pos_B[1]]
    q = get_common_qid(vertex_A, vertex_B)[0]
    md = sb.m(qid=[q])
    if md.last == '1': [sb.x(i) for i in vertex_B['dat']]

    # move defect
    chain = [q]
    for i in range(1,len(path)):
        # extend defect
        vertex_A = lattice['vertex'][path[i-1][0]][path[i-1][1]]
        vertex_B = lattice['vertex'][path[i][0]][path[i][1]]
        q = get_common_qid(vertex_A, vertex_B)[0]
        md = sb.m(qid=[q])
        if md.last == '1': [sb.x(i) for i in vertex_B['dat']]

        # remove defect
        sb.h(vertex_A['anc'])
        [sb.cx(vertex_A['anc'], target) for target in vertex_A['dat']]
        sb.h(vertex_A['anc'])
        md = sb.m(qid=[vertex_A['anc']])
        if md.last == '1': [sb.z(i) for i in chain]

        chain.append(q)

def get_chain(pos_list, lattice):

    chain = []
    for i in range(1,len(pos_list)):
        pos_A = pos_list[i-1]
        pos_B = pos_list[i]
        chain.append(get_common_qid(lattice['vertex'][pos_A[0]][pos_A[1]],
                                    lattice['vertex'][pos_B[0]][pos_B[1]])[0])
    return chain

def measure_logical_Z(sb, face, chain, shots=10):

    mval_list = []
    for _ in range(shots):
        sb_tmp = sb.clone()
        mval_0 = sb_tmp.m(qid=face['dat']).last
        mval_1 = sb_tmp.m(qid=chain).last
        mval = (str(sum([int(s) for s in list(mval_0)])%2)
                + str(sum([int(s) for s in list(mval_1)])%2))
        mval_list.append(mval)
        sb_tmp.free()
    return Counter(mval_list)

if __name__ == '__main__':

    lattice_row = 4
    lattice_col = 6
    lattice = create_lattice(lattice_row, lattice_col)

    # make vacuum state
    qubit_num = (2*lattice_row + 1) * (2*lattice_col + 1)
    sb = Stabilizer(qubit_num=qubit_num)
    initialize(sb, lattice)

    # logical qubit #1
    d_pos_A = [2,1]
    d_pos_B = [2,2]
    d_path = [[2,2],[2,3],[2,4]]
    create_move_defect_d(sb, d_pos_A, d_pos_B, d_path, lattice)

    # logical qubit #0
    p_pos_A = [0,0]
    p_pos_B = [0,1]
    # p_path = [[0,1],[0,2]]
    p_path = [[0,1],[0,2],[1,2],[2,2],[3,2],[3,3],[3,4],[3,5],
              [2,5],[1,5],[0,5],[0,4],[0,3],[0,2]]
    create_move_defect_p(sb, p_pos_A, p_pos_B, p_path, lattice)

    # measure logical qubits: #0 and #1
    face = lattice['face'][p_pos_A[0]][p_pos_A[1]]
    chain = get_chain([[2,1],[2,2],[2,3],[2,4]], lattice)
    freq = measure_logical_Z(sb, face, chain, shots=100)
    print(freq)

    sb.free()

何をやっているか説明します。main処理部を見てください。

lattice_row = 4
lattice_col = 6
lattice = create_lattice(lattice_row, lattice_col)

で、量子ビットを配置する格子情報を作ります。引数として与えるlattice_rowとlattice_colは各々縦方向の格子(面)の数と横方向の格子(面)の数です。関数の中身を見ていただければわかると思いますが、出力するデータは{'face': face, 'vertex': vertex}という辞書データです。ここで、faceは面の位置に応じた配置で並んでいる2次元配列(リストのリスト)で、その要素は{'anc': q0, 'dat': [q1,q2,q3,q4]}という辞書データになっています。ancは対応した面に対する補助量子ビットの番号でdatはその面の境界に位置するデータ量子ビットの番号です。また、vertexは頂点の位置に応じた配置で並んでいる2次元配列(リストのリスト)で、その要素はfaceと同様{'anc': q0, 'dat': [q1,q2,q3,q4]}という辞書データです。ancは対応した頂点に対する補助量子ビットの番号でdatはその頂点に接続する辺に位置するデータ量子ビットの番号です。これで漏れなく考えている格子情報が定義できたことになります10。詳細は関数定義を見てください。

次に、

# make vacuum state
qubit_num = (2*lattice_row + 1) * (2*lattice_col + 1)
sb = Stabilizer(qubit_num=qubit_num)
initialize(sb, lattice)

で、真空状態をつくります。まず量子ビット数を計算してqubit_numに格納します。今の想定だと117量子ビットになります。こんな規模の量子状態をシミュレータで計算することは到底不可能です。が、ご安心ください。このぐるっと回るBraidinngをやりたいがために、qlazyにスタビライザーの計算を実行するクラスStabilizerを追加しました(v.0.1.1)。基本、Clifford演算しかできないのですが量子ビット数の制限なく(メモリが許す限りですが)計算が実行できます。今回これを使います。Stabilizerコンストラクタの引数に量子ビット数を与えて、インスタンスを生成して変数sbとします。初期状態はすべての生成元が恒等演算子になっているものなので、適当に初期化して、真空状態をつくります。関数initializeでそれを実行しています。関数initializeの最初の行で、

sb.set_all('Z')

としていますが、これはすべての量子ビットに$Z$演算子をセットすることを表しています。つまり、すべての量子ビットを$\ket{0}$に初期化することを表しています。これを出発点にして生成元を順に間接測定していくことで真空状態を作ります。具体的には、以下のようにします。

for face_list in lattice['face']:
    for face in face_list:
        sb.h(face['anc'])
        [sb.cz(face['anc'], target) for target in face['dat']]
        sb.h(face['anc'])
        sb.m(qid=[face['anc']])

面演算子を測定する部分だけを抜き出しましたが、頂点演算子を測定する部分も同様です11

真空状態ができたらば、次に、欠陥の対生成と移動によって論理量子ビットを作ります。実装の都合により0番目でなく1番目の論理量子ビットからやります。

# logical qubit #1
d_pos_A = [2,1]
d_pos_B = [2,2]
d_path = [[2,2],[2,3],[2,4]]
create_move_defect_d(sb, d_pos_A, d_pos_B, d_path, lattice)

ここで、d_pos_Aとd_pos_Bは各々最初に対生成させるd型欠陥の頂点座標を表しています。先程説明に使った図を合わせて見ていただけるとわかりやすいかと思います。対生成した右の方の欠陥を移動させるための移動経路情報がd_pathです。頂点座標[2,2]->[2,3]->[2,4]の順で欠陥が移動することを表します。sb, d_pos_A, d_pos_B, d_pathを引数にして、実際に状態を変化させる関数がcreate_move_defect_dです。では、関数の中身を見てみましょう。

# create defect pair
vertex_A = lattice['vertex'][pos_A[0]][pos_A[1]]
vertex_B = lattice['vertex'][pos_B[0]][pos_B[1]]
q = get_common_qid(vertex_A, vertex_B)[0]
md = sb.m(qid=[q])
if md.last == '1': [sb.x(i) for i in vertex_B['dat']]

で、d型欠陥を対生成します。$Z$基底で測定する量子ビットを特定するため対生成する頂点情報をlatticeから得てvertex_Aとvertex_Bに格納します。vertex_Aとvertex_Bに共通して含まれる量子ビット番号を得るために関数get_common_qidを呼び出します(詳細は関数定義を見てください)。変数qがその番号になるので、これを$Z$基底で測定します。測定結果は変数mdに格納されてlastプロパティがその測定値になります。測定結果が$\ket{0}$の場合何もせず、$\ket{1}$だった場合固有値を反転させるため論理$X$演算子を作用させます。これで欠陥が対生成されたことになりますが、隣接した状態になっているため、一方(右の方)を移動させます。

# move defect
chain = [q]
for i in range(1,len(path)):
    # extend defect
    vertex_A = lattice['vertex'][path[i-1][0]][path[i-1][1]]
    vertex_B = lattice['vertex'][path[i][0]][path[i][1]]
    q = get_common_qid(vertex_A, vertex_B)[0]
    md = sb.m(qid=[q])
    if md.last == '1': [sb.x(i) for i in vertex_B['dat']]

   # remove defect
   sb.h(vertex_A['anc'])
   [sb.cx(vertex_A['anc'], target) for target in vertex_A['dat']]
   sb.h(vertex_A['anc'])
   md = sb.m(qid=[vertex_A['anc']])
   if md.last == '1': [sb.z(i) for i in chain]

   chain.append(q)

上記forループの中身は2つのパートに分かれています。前半は欠陥を移動経路情報に従って1マス分拡大させる部分で、後半はもとの欠陥を消滅させる部分です。前半の拡大させる部分ではもとの欠陥に相当する頂点をvertex_A、拡張する欠陥に相当する頂点をvertex_Bとして、両者に共通している量子ビットを$Z$測定します。先程と同様測定結果が$\ket{0}$の場合は何もせず、$\ket{1}$の場合は固有値を反転させるため論理$X$演算子を作用させます。後半の消滅させる部分ではもとの欠陥の位置にあった頂点演算子を復活させるため、その頂点演算子を測定します。この場合補助量子ビットを使った間接測定をしないといけないので、頂点演算子を構成する4つのデータ量子ビットを標的とするCNOT演算を、補助量子ビットに対するアダマールではさみます。そして補助量子ビットを$Z$基底で測定するのですが、測定結果が$\ket{0}$だった場合は何もせず、$\ket{1}$だった場合は固有値を反転させるため論理$Z$演算子を作用させます。ここでchainというのは移動する欠陥が引き連れているチェーンに含まれている量子ビット番号リストです。この量子ビット上に配備された$Z$演算子のテンソル積が論理$Z$演算子になるので移動のたびに量子ビット番号をappendして、その時点での論理$Z$演算が正しく実行できるようにしておきます。固有値を反転する必要が発生した場合、このchainに基づき論理$Z$演算子を作用させます。これで1番目の論理量子ビットが状態$\ket{0^{L}}$になりました。

main処理部に戻って、次に0番目の論理量子ビットを作ります。つまり、p型欠陥を対生成させて移動させます。

# logical qubit #0
p_pos_A = [0,0]
p_pos_B = [0,1]
# p_path = [[0,1],[0,2]]
p_path = [[0,1],[0,2],[1,2],[2,2],[3,2],[3,3],[3,4],[3,5],
          [2,5],[1,5],[0,5],[0,4],[0,3],[0,2]]
create_move_defect_p(sb, p_pos_A, p_pos_B, p_path, lattice)

ここで、p_pos_Aとp_pos_Bはp型欠陥を最初に対生成する面の座標を表しています。p_pathは対生成された欠陥の一方(右の方)を移動するときの移動経路情報を表しています。コメントアウトされているp_pathは短い移動ですので、これでできあがる論理状態は論理$X$演算子の固有状態$\ket{+^{L}}$です。一方、コメントアウトされていないp_pathは短い移動に加えて、さらに(先程の図と見比べてみればわかるように)d型欠陥の周りをぐるっと一周するような移動になっています。なので長い移動をさせてみると論理状態に対するCNOT演算が実現できているはずです。これらを引数にしてcreate_move_defect_p関数を実行して状態を変化させます。関数の中身はd型の場合と同じような動作になりますので、説明は省略します。

最後に2つの論理量子ビットを論理$Z$基底で測定して、結果を表示します。

# measure logical qubits: #0 and #1
face = lattice['face'][p_pos_A[0]][p_pos_A[1]]
chain = get_chain([[2,1],[2,2],[2,3],[2,4]], lattice)
freq = measure_logical_Z(sb, face, chain, shots=100)
print(freq)

0番目の論理量子ビットはp型欠陥なので論理$Z$演算子はどちらか一方の欠陥を囲む$Z$演算子です。変数faceはその$Z$演算子の場所を特定するためのものです。また1番目の論理量子ビットはd型欠陥なので論理$Z$演算子は2つの欠陥をつなぐチェーン上の$Z$演算子です。変数chainはその$Z$演算子の場所を格納しておくためのものです。関数get_chainでそれを取得します。関数measure_logical_Zに引数としてスタビライザーsbとfaceとchainと測定回数を表すshots=100を与えて頻度データを取得します。関数の中ではfaceとchainに基づき愚直に測定を繰り返し、結果をカウントアップして頻度データ(Python標準のCounterクラスのインスタンス)を作りリターンしています。以上です。

結果

それでは、実行結果を示します。まず、0番目の論理量子ビットが巻き付かない場合について見てみます。上のコードでコメントアウトしていた短い距離を移動する経路データのコメントを外し、長い距離を移動する経路データをコメントアウトます。つまり、

p_path = [[0,1],[0,2]]
# p_path = [[0,1],[0,2],[1,2],[2,2],[3,2],[3,3],[3,4],[3,5],
#          [2,5],[1,5],[0,5],[0,4],[0,3],[0,2]]

として実行してみます。すると、

Counter({'00': 53, '10': 47})

となりました。0番目の論理量子ビットは$\ket{+^{L}}$で1番目の論理量子ビットは$\ket{0^{L}}$です。つまり、

\ket{+^{L} 0^{L}} = \frac{1}{\sqrt{2}} (\ket{0^{L} 0^{L}} + \ket{1^{L} 0^{L}})   \tag{21}

なので、確かに$\ket{0^{L} 0^{L}}$と$\ket{1^{L} 0^{L}}$が半々の確率で観測されるというのは正しい結果です。

これを初期状態だと見なして、次にp型欠陥がd型欠陥の周りを回って、初期状態の場所に戻ってくる場合について見てみます。先程のコメントを逆にして実行してみます。つまり、

# p_path = [[0,1],[0,2]]
p_path = [[0,1],[0,2],[1,2],[2,2],[3,2],[3,3],[3,4],[3,5],
         [2,5],[1,5],[0,5],[0,4],[0,3],[0,2]]

として実行すると、

Counter({'11': 53, '00': 47})

という結果になります。これは、ちょうど、

\frac{1}{\sqrt{2}} (\ket{0^{L} 0^{L}} + \ket{1^{L} 1^{L}})   \tag{22}

というBell状態を測定した結果に等しいです。というわけで、CNOT演算が確かに実現されていることが確認できました。めでたし、めでたし。

おわりに

平面上に欠陥を作ってぐるっと別の欠陥に巻きつけるとCNOT演算になるという面白い現象を自分の手で何とか再現したいがために、今回、スタビライザー形式の計算を実装するところから始めました。やってみると、思っていたほど簡単ではなくて(単に自分の知識不足のせいですが)、例えば、スタビライザー群の独立な生成元の個数をどうやって計算するかとか、ある演算子がスタビライザー群の要素になっているかどうかの判定をどうするかとか、ちょっと悩んでしまいました(結局「掃き出し法」を使って検査行列のランクを計算する方法でやりましたが)。それから、表面符号の欠陥の移動に関して教科書には「測定すればよろし」と書いてあるのですが、固有値が-1になった場合どうするかというところまで書いてなかったりするので(つまり「行間を読め」ということなのですが)、プログラムを書きながら試行錯誤してしまいました(結局本文記載の通りやれば良いことがわかりました)。ということで、今回の記事を完成させるまでに思いのほか時間がかかってしまいました。が、その分勉強になったので良しとします。

さて、次回の予定です。まだ決めてないですが、今回、表面符号に関するClifford演算が実現できたので、次はnon-Cliffordな論理演算をどうやって実現すれば良いのか、というあたりを考えています。

以上


  1. 頂点演算子をすべて並べてみると同じ量子ビットに対応した$X$演算子は必ず2回登場することからすべての積は恒等演算子になることがわかります。ちなみに、すべての面演算子の積は平面を囲むループに並んだ$Z$演算子のテンソル積になります。 

  2. スタビライザー形式における測定はこの後の説明で何度も出てきます。以前の記事「量子情報理論の基本:量子誤り訂正(スタビライザー符号:2)」に詳細説明がありますので、適宜ご参照ください。 

  3. $A_1$または$A_2$はもはやこのスタビライザー群の生成元ではないことにご注意ください。 

  4. 後で動作確認する際、この方法で欠陥対の移動シミュレーションを実行します。 

  5. 欠陥をつなぐどんなチェーンを考えても同じことです。前回の記事で連続変形できるループ上の演算子の積は同じ効果を表すと言っていたのと類似したお話です。 

  6. 式(19)から$X_5 X_6$を除いたスタビライザー群で決まる符号空間を考えると、論理$X$および論理$Z$演算子は本文記載の通りとなります。式(19)は論理$X$の固有値$+1$に対する固有状態なので、$\ket{+^{L}}$状態を表します。この状態に論理$Z$を演算すると、論理的な位相が反転して$\ket{-^{L}}$状態に変化します。 

  7. 量子誤り訂正符号における符号の距離は、論理演算子を構成するパウリ演算子の個数の最小値と定義されています。意味合いとしては、任意の符号から何らかの論理演算を行って別の符号に移すときに変化した量子ビット数の最小値です。変化する量子ビット数は論理演算子に含まれるパウリ演算子の数に等しいので符号の距離はこのように定義されます。ちなみに、古典線形符号における符号の距離というのは論理的に異なる符号同士のハミング距離の最小値です。つまり、量子の場合も古典の場合も符号の距離は、ある符号から別の符号に変化させるために最低何ビット変化させれば良いかを表す指標と考えておけば良いと思います。 

  8. なぜこれでCNOTが実現できると考えられたのか、その発想の大元が実はよくわかっていないので、汗。 

  9. なぜこうなるか詳細説明は省きます。スタビライザー形式での測定をひとつひとつ手計算しながら確認してみるとわかると思います。 

  10. 格子情報をどのようにデータとして定義するかは、いろんなやり方があるのだとは思いますが、後の処理のしやすさを考えて自分なりに工夫してみた結果、こんな風にちょっと複雑な形になってしまいました。 

  11. 先程定義した格子データの工夫がここで生きています。格子データの構造はちょっとわかりにくかったかもしれませんが、それを使った実装は割とわかりやすくシンプルにできていると思うのですが、いかがでしょうか? 

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4