量子情報理論の基本：表面符号によるユニバーサル量子計算（１）〜Braidingで論理CNOT演算〜

$$
\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}}
$$

## はじめに

[前回の記事](https://qiita.com/SamN/items/d3600efa7d953916298f)では、平面格子上に敷き詰められた面演算子と頂点演算子に欠陥（演算子がない領域）をつくることで論理量子ビットと論理$X$および論理$Z$演算子を定義できて、さらに欠陥を互いに巻きつけるように移動させることでCNOT演算が実現できるということを見てきました。ところが、このCNOT演算はユニバーサル量子計算の要素とするには、実は不完全なものです。理由はこのあと説明します。今回の記事では、じゃあ完全なCNOTはどうやったら実現できるのか、そのあたりを勉強してみます。ユニバーサル量子計算を実現するには、さらに1量子ビットの任意のユニタリ演算が用意できれば良いのですが、それは次回にします。一通り理解できたところで、量子計算シミュレータ[qlazy](https://github.com/samn33/qlazy)を使って、その動作を確認します。

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

1. [小柴、森前、藤井「観測に基づく量子計算」コロナ社（2017年）](https://www.coronasha.co.jp/np/isbn/9784339028706/)
2. [K.Fujii,"Quantum Computation with Topological Codes - from qubit to topological fault-tolerance",arXiv:1504.01444v1 [quant-ph] 7 Apr 2015](https://arxiv.org/abs/1504.01444)
3. [藤井「量子計算超入門」（2012年）](http://quantphys.org/keisukefujii/tokyotopological.pdf)
4. [ニールセン・チャン「量子コンピュータと量子通信Ⅱ」（2005年）](https://www.ohmsha.co.jp/book/9784274200083/)

## 理論説明

### Braiding操作のトポロジカルな表現

本題に入る前にひとつ押さえておきたい話題があります。[前回の記事](https://qiita.com/SamN/items/d3600efa7d953916298f)でBraidingによって2量子ビットの論理演算を表現できることがわかりましたが、平面上の欠陥をぐるっと移動させる説明図では欠陥をどのように時間変化させるとどんなトポロジー的な巻きつけが実現されて、それがどんな論理演算に対応しているのか極めてわかりにくいです。そこで3次元時空間上でのトポロジー的な特徴をわかりやすくするための模式図を導入します。

まず、p型欠陥の移動について考えます。下図左に示すようなp型欠陥対があったとします。この欠陥をつなぐ$X$演算子のチェーンを生成元に追加することで論理$X$の固有状態である論理$\ket{+}$を表現することができました（下図中上）。p型欠陥で作った論理$\ket{+}$という意味で$\ket{+}^{p}$という記号でこの状態を表すことにします（図に示したように下付きの添字Lをケットの右に書きたいのですが、、なぜかうまくレンダリングしてくれません、すみません、Lがあるものと思ってください、以下同様）。これを模式的に表したのが下図右上です。ここで縦方向が空間位置を表しており（2次元平面を1次元にマッピングしていると思ってください）、横方向が時間軸を表しています。こうすると$X$チェーンの時間変化はこの時空間における曲面によって表されることになります。欠陥をつなぐ紐が時間軸に沿って移動しながら時空間中をリボンのような軌跡を描いていくというようにイメージしていただければ良いです。一方、欠陥対のどちらかの欠陥を囲む$Z$演算子のループを生成元に追加することで$\ket{0}^{p}$を表現することができます（下図中下）。これを模式的に表したのが下図右下です。この$Z$演算子のループの時間変化もこの時空間上の曲面によって表されます。が、先程と違って欠陥を囲むループ（輪ゴムのようなもの）が時間軸に沿って移動していくのでその軌跡はチューブのような曲面になります。

![fig01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/857844e8-3f69-4153-1dfa-6aed3c64e069.png)


この模式図で欠陥対の生成と測定も表現することができます。p型欠陥対は真空状態のどこか1点（量子ビット）を$X$測定することによって生成させることができました。これは何もない真空からある時どこかで測定が行われ欠陥対が対生成されるような形で模式的に表せます。このように生成された状態は2つの欠陥をつなぐ$X$チェーンの固有状態になっています。そして、この$X$チェーンを測定することで論理ビットとしての値を確定することができますが、その操作を欠陥が対消滅するような図で表現することにします[^1]（下図）。

[^1]: ここで欠陥対が消滅してしまう印象をもってしまうかもしれませんが、論理量子ビットの固有値が+1か-1に確定するだけで、真空状態に戻るのではないと思います（多分）。真空状態に戻すためには、2つの欠陥を隣接させておいてどちらかの面演算子（$Z$演算子のループ）を測定しないといけないと思います（間違っていたらご指摘ください）。p型欠陥対の片方の周囲に置かれた$Z$演算子で論理$Z$演算子の固有状態を表すことができますが、このときもこれを測定しても欠陥が消滅するわけではありません（と思います）。

![fig02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/dc298d4f-6028-2ea4-c567-512e3932923e.png)

次に、d型欠陥について考えます。$X$演算子と$Z$演算子の立場が逆になるだけでp型とまったく同じイメージなのでサラッといきます。下図左に示すようなd型欠陥対があったとします。この欠陥をつなぐ$Z$演算子のチェーンを生成元に追加することで$\ket{0}^{d}$を表すことができ（下図中上）、この時間変化は模式的に下図右上のような曲面（リボン）で表されます。また、この欠陥対のどちらか一方を囲む$X$演算子のループで$\ket{+}^{d}$を表すことができ（下図中下）、この時間変化は模式的に下図右下のような曲面（チューブ）で表されます。

![fig03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/0867b05f-72dc-9bd6-cbb7-dbfa545f9432.png)


d型欠陥対の生成は、$Z$測定によって真空から対生成されて論理$Z$の固有状態$\ket{0}^{d}$となることで表現できます。最後に$Z$チェーンを測定することで論理ビットとしての値を得ることができますが、この操作を欠陥対が消滅するような図で表現することにします（下図）。

![fig04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/793bed72-ddb3-940a-1f2f-76f7bc17f112.png)


### 前回のCNOT

それでは、[前回の記事](https://qiita.com/SamN/items/d3600efa7d953916298f)で実現したCNOT演算を、前節で導入した模式図を使って表してみます。下図は、p型欠陥対で論理$X$の固有状態$\ket{+}^{p}$を作成して、その欠陥の片方をd型欠陥に巻きつけた様子を表しています[^2]。p型欠陥をつなぐチェーンがd型欠陥に巻き付いてd型欠陥に新たにチューブ（論理$X$演算子）が現れ、元のp型のリボンはそのままの形を保っています。これは、

[^2]: [参考文献3](http://quantphys.org/keisukefujii/tokyotopological.pdf)にあった図の引用です。本記事の他の箇所でもいくつか引用させていただきました。藤井先生のこのスライドはトポロジカルな図がたくさん掲載されていてわかりやすいです。おすすめです！

```math
X_1 \otimes I_2 \rightarrow X_1 \otimes X_2  \tag{1}
```

という論理演算に対応しています。どうでしょう、わかりますでしょうか。下のd型欠陥を2本の棒だと思ってください。その上にp型欠陥を表すチェーンがあって、それが左から右に移動しながらチェーンの下端が下の棒にぐるっと巻きつくのです。そうするとひとつのチェーンから一つの輪っかが新たに生まれることがイメージできると思います[^3]。

[^3]: この様子を動画で説明できればよりわかりやすいと思うのですが、すみません、当方動画制作の素養がありません。頑張って頭の中で動画作成してみてください。

![fig05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/1c27d0a1-3cc3-9145-29f3-0dc56656b101.png)


同じ操作を初期状態を変えてやってみたものが下図です。ここではd型欠陥対で論理$Z$演算子の固有状態$\ket{0}^{d}$を作成して、p型欠陥の片方をd型欠陥に巻きつけた様子を表しています。d型欠陥をつなぐチェーンがp型欠陥に巻き取られてp型欠陥に新たにチューブ（論理$Z$演算子）が現れることがわかると思います。元のd型の面はそのままの形を保っています。これは、

```math
I_1 \otimes Z_2 \rightarrow Z_1 \otimes Z_2  \tag{2}
```

という論理演算に対応しています。先程と同じ操作ですが、今度はd型欠陥を表す2本棒の間に薄い膜があって、上のp型欠陥の片方がぐるっとd型の棒に巻き付いてp型欠陥の片方の周りに新たな輪っかが生まれるイメージです。

![fig06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/934a6193-4db4-6281-3abc-0b5d4ebde61b.png)


[前回の記事](https://qiita.com/SamN/items/d3600efa7d953916298f)では、これでCNOTが実現できた、と言っていました。ところが、これ、よく見ると制御側がp型欠陥で、標的側がd型欠陥になっています。この組み合わせでないとCNOTができません。それで何がマズイの？と言われそうですが、とてもよろしくないです。例えば、

![fig07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/2e84a110-3f7a-79c2-8ea9-9b6b89ec827f.png)


のようなCNOTの組み合わせしかできません。SWAPゲートは代表的な2量子に対するユニタリ演算ですが、

![fig08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/8b746673-e074-cd86-a643-e8822521a2a4.png)


のように書いてみればわかるように、p型欠陥がCNOT演算の制御側にしか使えないのだとすると実現不可能です。一般に任意のN量子ビット状態に対するユニタリ演算は1量子ビットのユニタリ演算と2量子ビットのCNOT演算の積に分解することができます（[参考文献4](https://www.ohmsha.co.jp/book/9784274200083/)参照）が、特定の量子ビットしか制御ビットにできない（あるいは特定の量子ビットしか標的ビットにできない）という制約があるのだとすると、そのような分解は不可能になります。つまり、欠陥対による表面符号を用いてユニバーサル量子計算を実現するためには、少なくとも制御ビットも標的ビットもp型欠陥であるようなCNOT演算が実現できないといけないということです。

#### 今回のCNOT

それではそのような制御側も標的側もp型欠陥であるようなCNOT演算はどうやれば実現できるのでしょうか？ともったいぶっても、どうやって導き出したのか正直よくわからないので、参考文献に書いてあった答えをそのまま言います。答えは、以下のようなCNOT演算の等価回路を考えればよろし、です[^4]。

[^4]: これ、確かにCNOTになっているのですが、大丈夫でしょうか。スタビライザー形式で入出力を考えてみればわかりやすいと思います。つまり、入力が$X \otimes I$のときに出力が$X \otimes X$になっているとか、入力が$I \otimes Z$のときに出力が$Z \otimes Z$になっている等々を確認すれば確かにCNOTだね、ということがわかります。ただし、注意すべき点が一つあります。4番目のp型欠陥を消滅させたときの測定値が-1だった場合、1番目と3番目の論理量子ビットを反転（$\ket{+}$を$\ket{-}$に$\ket{-}$を$\ket{+}$に反転）させる必要があります。スタビライザー形式の計算を注意深くやるとわかります。

![fig09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/76fa7c5d-7b2b-c651-d787-61a45e72073e.png)


これ自体は表面符号ではない普通の量子回路ですが、上から1番目と3番目と4番目の量子ビットが制御ビットとして使われていて、2番目の量子ビットが標的ビットとして使われているという形になっています。ということなので、これをそのまま表面符号に焼き直して、以下のように1番目と3番目と4番目をp型欠陥として、2番目をd型欠陥とすれば、p型欠陥だけを使ったCNOTが実現できます。

![fig10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/7dd4c3ce-490c-831b-2889-35bec03abf16.png)


先程説明した模式図で書くと下図左のような形になります。巻きつき方の構造を不変に保ちながらトポロジー的に同値になるように簡単に表現し直すと下図右のようになります。

![fig11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/9132c6c0-99db-f49d-0e0a-b83506d32074.png)


さらに空間次元は本来2次元だったので、正確に3次元時空間上でトポロジーを表現すると、

![fig12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/c93978c6-3564-7190-7b43-54a07e22911b.png)


となります。どうでしょう。何やら不思議な現代アートのオブジェ風の物体が現れましたが、これが欠陥を用いた表面符号におけるCNOT演算を表しています。


## 動作確認

それでは量子計算シミュレータ[qlazy](https://github.com/samn33/qlazy)を使って、これで本当にCNOT演算が実現できるのかを確認してみます。前回以上に複雑なBraidingが必要になるのでその分用意すべき平面格子のサイズが大きくなるのですが、クリフォード演算と測定だけで済むのでスタビライザー形式を使えば問題ないです。

具体的にどういうBraidingを実行するか最初に設計しておきます。欠陥とその動き方が上で説明したトポロジーになっていればどう決めても良いはずなので、とりあえず下図に示すような動き方にしてみました。

![fig13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/7750721d-e69f-d567-ff01-9f924c21566f.png)

![fig14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391058/c24e10c7-d60e-a83d-2f7d-b60c5c7f5da1.png)


まず(1)で真空状態を作成します（面演算子と頂点演算子を格子全体に敷き詰めます）。

(2)で欠陥対を4つ生成します（適当な場所を測定して欠陥を移動させます）。先程示したCNOTの回路図の一番上から量子ビット番号を0番目、1番目、2番目、3番目と名付けて[^5]、それを(2)のように配置します。0番目、2番目、3番目はp型欠陥対で論理$X$の固有状態、1番目はd型欠陥対で論理$Z$の固有状態となります。測定値に応じて論理$Z$または論理$X$演算子を適用することで$+1$の固有状態を用意することができます。

[^5]: プログラム実装の都合により量子ビット番号は0番目からはじめることにします。理論説明の際には1番目から始めた方が説明しやすいので本ブログでもほぼそうしていますが、大抵のプログラミング言語では配列のインデックスは0番目から始まります（FORTRANは確か配列のインデックスは1番目からだったと思いますが）。ときどき混乱しますが慣れるしかないですね。

(3)で欠陥をBraidさせます。0番目の欠陥対の片方を1番目の欠陥対の片方に巻きつけて、2番目の欠陥対と3番目の欠陥対の片方を1番目の欠陥対のもう片方に巻きつけます。

(4)で1番目の欠陥対を$Z$測定し、3番目の欠陥対を$X$測定します。

これでCNOTが実現されるはずです。$X$基底で入出力関係を表すなら、

| 入力側の制御と標的 | 出力側の制御と標的 |
|----------|---------|
| $\ket{++}$ | $\ket{++}$ 
| $\ket{+-}$ | $\ket{-+}$ |
| $\ket{+-}$ | $\ket{--}$ |
| $\ket{--}$ | $\ket{+-}$ |

となります[^6]。上の(2)で入力側の制御ビット（0番目の量子ビット）として$\ket{+}$、入力側の標的ビット（3番目の量子ビット）として$\ket{+}$を用意したとすると、この表の1行目から、出力側の制御ビット（0番目の量子ビット）は$\ket{+}$、出力側の標的ビット（2番目の量子ビット）は$\ket{+}$になります。つまり、0番目と2番目の量子ビットを$X$測定すると$100\%$の確率で$(+1,+1)$が観測されるはずです。また、入力側の制御ビットとして$\ket{+}$を用意し、入力側の標的ビット$\ket{+}$に論理$Z$演算子を適用して$\ket{-}$を用意したとすると、この表の2行目から、出力側の制御および標的ビットは$\ket{-}$および$\ket{+}$になります。つまり、0番目と2番目の量子ビットを$X$測定すると$100\%$の確率で$(-1,+1)$が観測されるはずです。同様にこの表の3行目、4行目の入出力関係も確認することができます。

[^6]: これは大丈夫でしょうか。スタビライザー形式で考えてみればすぐにわかると思います。表の1行目は$\<XI,IX\> \rightarrow \<XX, IX\> = \<XI,IX\>$、2行目は$\<XI,-IX\> \rightarrow \<-XX, IX\> = \<-XI, IX\>$等々、を表しています。


### 実装

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

【2021.9.6追記】[qlazy](https://github.com/samn33/qlazy)最新版でのソースコードは[ここ](https://github.com/samn33/qlazy/blob/master/example/py/SurfaceCode/braiding_cnot2.py)に置いてあります。

```Python
from collections import Counter
from qlazypy import Stabilizer

XBASE = {'0':'+', '1':'-'}
OBJECT = {'p':'face', 'd':'vertex'}

def get_common_qid(obj_A, obj_B):

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

def get_path(pos_A, pos_B):

    path = []

    if pos_A[1] < pos_B[1]: h_list = list(range(pos_A[1], pos_B[1] + 1))
    else: h_list = list(reversed(range(pos_B[1], pos_A[1] + 1)))
    for j in h_list: path.append([pos_A[0], j])

    if pos_A[0] < pos_B[0]: v_list = list(range(pos_A[0] + 1, pos_B[0] + 1))
    else: v_list = list(reversed(range(pos_B[0], pos_A[0])))
    for i in v_list: path.append([i, pos_B[1]])

    return path

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):

    i = 0  # generator id
    for face_list in lattice['face']:
        for face in face_list:
            [sb.set_pauli_op(i, q, 'Z') for q in face['dat']]
            i += 1
            sb.set_pauli_op(i, face['anc'], 'Z')
            i += 1

    for vertex_list in lattice['vertex']:
        for vertex in vertex_list:
            [sb.set_pauli_op(i, q, 'X') for q in vertex['dat']]
            i += 1
            sb.set_pauli_op(i, vertex['anc'], 'Z')
            i += 1

def get_chain(pos_list, dtype, 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[OBJECT[dtype]][pos_A[0]][pos_A[1]],
                                    lattice[OBJECT[dtype]][pos_B[0]][pos_B[1]])[0])
    return chain

def move_defect(sb, pos_A, pos_B, path, dtype, lattice, create=False, annihilate=False):

    obj = OBJECT[dtype]
    if create == True:
        obj_A = lattice[obj][pos_A[0]][pos_A[1]]
        obj_B = lattice[obj][pos_B[0]][pos_B[1]]
        q = get_common_qid(obj_A, obj_B)[0]
        if dtype == 'p':
            md = sb.mx(qid=[q])
            if md.last == '1': [sb.z(i) for i in obj_B['dat']]
        elif dtype == 'd':
            md = sb.m(qid=[q])
            if md.last == '1': [sb.x(i) for i in obj_B['dat']]
        
    chain = get_chain(get_path(pos_A, pos_B), dtype, lattice)
    for i in range(1,len(path)):
        # extend defect
        obj_A = lattice[obj][path[i-1][0]][path[i-1][1]]
        obj_B = lattice[obj][path[i][0]][path[i][1]]
        q = get_common_qid(obj_A, obj_B)[0]
        if dtype == 'p':
            md = sb.mx(qid=[q])
            if md.last == '1': [sb.z(i) for i in obj_B['dat']]
        elif dtype == 'd':
            md = sb.m(qid=[q])
            if md.last == '1': [sb.x(i) for i in obj_B['dat']]
            
        # remove defect
        sb.h(obj_A['anc'])
        if dtype == 'p': [sb.cz(obj_A['anc'], target) for target in obj_A['dat']]
        elif dtype == 'd': [sb.cx(obj_A['anc'], target) for target in obj_A['dat']]
        sb.h(obj_A['anc'])
        md = sb.m(qid=[obj_A['anc']])
        if md.last == '1':
            if dtype == 'p': [sb.x(i) for i in chain]
            elif dtype == 'd': [sb.z(i) for i in chain]
            sb.x(obj_A['anc'])

        chain.append(q)

    if annihilate == True:
        obj_A = lattice[obj][pos_A[0]][pos_A[1]]
        obj_B = lattice[obj][path[-1][0]][path[-1][1]]
        q = get_common_qid(obj_A, obj_B)[0]
        if dtype == 'p': md = sb.mx(qid=[q])
        elif dtype == 'd': md = sb.m(qid=[q])
        return md.last
    
    return None

def measure_logical_X(sb, chain_A, chain_B, shots=1):

    mval_list = []
    for _ in range(shots):
        sb_tmp = sb.clone()
        mval_A = sb_tmp.mx(qid=chain_A).last
        mval_B = sb_tmp.mx(qid=chain_B).last
        mval_A_bin = str(sum([int(s) for s in list(mval_A)])%2)
        mval_B_bin = str(sum([int(s) for s in list(mval_B)])%2)
        mval = (XBASE[mval_A_bin] + XBASE[mval_B_bin])
        mval_list.append(mval)
        sb_tmp.free()
        
    return Counter(mval_list)

def operate_logical_Z(sb, lq, lattice):

    if lq == 0: face = lattice['face'][0][0]
    elif lq == 2: face = lattice['face'][0][7]
    elif lq == 3: face = lattice['face'][6][0]
    [sb.z(q) for q in face['dat']]

def operate_logical_cnot(lq, shots=5):

    # set lattice
    lattice_row, lattice_col = 7, 8
    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, gene_num=qubit_num+1)
    initialize(sb, lattice)

    # set logical qubit #0
    p0_pos_A, p0_pos_B = [0,0], [0,1]
    p0_path = [[0,1],[0,2]]
    move_defect(sb, p0_pos_A, p0_pos_B, p0_path, 'p', lattice, create=True)
    if lq[0] == '-': operate_logical_Z(sb, 0, lattice)

    # set logical qubit #1
    d1_pos_A, d1_pos_B = [2,5], [3,5]
    d1_path = [[3,5],[4,5],[5,5]]
    move_defect(sb, d1_pos_A, d1_pos_B, d1_path, 'd', lattice, create=True)

    # set logical qubit #2
    p2_pos_A, p2_pos_B = [0,7], [1,7]
    p2_path = [[1,7],[2,7],[3,7]]
    move_defect(sb, p2_pos_A, p2_pos_B, p2_path, 'p', lattice, create=True)

    # set logical qubit #3
    p3_pos_A, p3_pos_B = [6,0], [6,1]
    p3_path = [[6,1],[6,2]]
    move_defect(sb, p3_pos_A, p3_pos_B, p3_path, 'p', lattice, create=True)
    if lq[1] == '-': operate_logical_Z(sb, 3, lattice)

    # braid logical qubit #0
    p0_pos_A, p0_pos_B = [0,0], [0,2]
    p0_path = [[0,2],[1,2],[2,2],[3,2],[3,3],[3,4],[3,5],[3,6],
               [2,6],[1,6],[0,6],[0,5],[0,4],[0,3],[0,2]]
    move_defect(sb, p0_pos_A, p0_pos_B, p0_path, 'p', lattice)
    
    # braid logical qubit #2
    p2_pos_A, p2_pos_B = [0,7], [3,7]
    p2_path = [[3,7],[3,6],[3,5],[3,4],[3,3],[4,3],[5,3],
              [6,3],[6,4],[6,5],[6,6],[6,7],[5,7],[4,7],[3,7]]
    move_defect(sb, p2_pos_A, p2_pos_B, p2_path, 'p', lattice)

    # braid and annihilate logical qubit #3
    p3_pos_A, p3_pos_B = [6,0], [6,2]
    p3_path = [[6,2],[6,3],[6,4],[6,5],[6,6],[5,6],[4,6],[3,6],
               [3,5],[3,4],[3,3],[3,2],[4,2],[5,2],[6,2],[6,1]]
    mval_p = move_defect(sb, p3_pos_A, p3_pos_B, p3_path, 'p', lattice, annihilate=True)

    # braid and annihilate logical qubit #1
    d1_pos_A, d1_pos_B = [2,5], [5,5]
    d1_path = [[5,5],[4,5],[3,5]]
    mval_d = move_defect(sb, d1_pos_A, d1_pos_B, d1_path, 'd', lattice, annihilate=True)

    if mval_p == '1':
        operate_logical_Z(sb, 0, lattice)
        operate_logical_Z(sb, 2, lattice)
    
    # measure logical qubits: #0 and #2
    chain_0 = get_chain(get_path([0,0],[0,2]), 'p', lattice)
    chain_2 = get_chain(get_path([0,7],[3,7]), 'p', lattice)
    freq = measure_logical_X(sb, chain_0, chain_2, shots=shots)
    print("Input('{0:}') == [CNOT] ==> {1:}".format(lq, freq))

    sb.free()

if __name__ == '__main__':

    operate_logical_cnot('++', shots=10)  # --> '++'
    operate_logical_cnot('+-', shots=10)  # --> '--'
    operate_logical_cnot('-+', shots=10)  # --> '-+'
    operate_logical_cnot('--', shots=10)  # --> '+-'

```

結構長くなってしまったので、ざっくり説明します。operate_logical_cnot関数が今回のメイン部分になります。第１引数として入力状態を文字列（’++’, '+-', '-+', '--'）で与え、第２引数に測定回数を整数値で与えることで、測定結果を表示します。

operation_logical_cnot関数の中身を見てください。

```Python
# set lattice
lattice_row, lattice_col = 7, 8
lattice = create_lattice(lattice_row, lattice_col)
```

で、格子の縦横のサイズを上図で説明したように$7 \times 8$に設定して格子データをcreate_lattice関数で作成します。これは[前回の記事](https://qiita.com/SamN/items/d3600efa7d953916298f)と同じなので説明省略します。

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

で、作成した格子に対応した量子ビット数と生成元数をスタビライザーのコンストラクタStabilizerに与えてインスタンスsbを作成します。そしてinitialize関数でそれを真空状態にします。つまり、面演算子と頂点演算子を格子全体に敷き詰めます。詳細は上の関数定義を見てください。sbのset_pauli_opメソッドを使って$X$または$Z$演算子を各量子ビットにセットしているだけです。

```Python
# set logical qubit #0
p0_pos_A, p0_pos_B = [0,0], [0,1]
p0_path = [[0,1],[0,2]]
move_defect(sb, p0_pos_A, p0_pos_B, p0_path, 'p', lattice, create=True)
if lq[0] == '-': operate_logical_Z(sb, 0, lattice)
```

で、0番目の論理量子ビットを生成します。move_defect関数で、面演算子の座標[0,0],[0,1]の境界にある量子ビットを$X$測定してp型欠陥を対生成して、[0,1]の欠陥を座標[0,2]に移動する処理を行っています。move_defect関数のcreateオプションをTrueにすると生成とその後の移動を実行します。False（デフォルト）の場合、生成しないで（すでに生成されていると見なして）移動だけを実行します。また、後で出てきますがannihilateオプションというのもあってこれにTrueを指定すると、移動した後（欠陥対が隣接しているものと見なして）測定を行います。make_defect関数の詳細については関数定義を見てください。これで論理的な$\ket{+}$が実現されます。最後のif lq[0] == '-'というのはoperate_logical_cnot関数に与えられた0番目の入力状態が'-'だった場合、論理$Z$演算子をかけて反転させる操作です。operate_logical_Z関数の詳細についても関数定義を見てください。

```Python
# set logical qubit #1
d1_pos_A, d1_pos_B = [2,5], [3,5]
d1_path = [[3,5],[4,5],[5,5]]
move_defect(sb, d1_pos_A, d1_pos_B, d1_path, 'd', lattice, create=True)

# set logical qubit #2
p2_pos_A, p2_pos_B = [0,7], [1,7]
p2_path = [[1,7],[2,7],[3,7]]
move_defect(sb, p2_pos_A, p2_pos_B, p2_path, 'p', lattice, create=True)

# set logical qubit #3
p3_pos_A, p3_pos_B = [6,0], [6,1]
p3_path = [[6,1],[6,2]]
move_defect(sb, p3_pos_A, p3_pos_B, p3_path, 'p', lattice, create=True)
if lq[1] == '-': operate_logical_Z(sb, 3, lattice)
```

で。1番目、2番目、3番目の論理量子ビットを生成します。これで初期状態が出来上がりました。

```Python
# braid logical qubit #0
p0_pos_A, p0_pos_B = [0,0], [0,2]
p0_path = [[0,2],[1,2],[2,2],[3,2],[3,3],[3,4],[3,5],[3,6],
           [2,6],[1,6],[0,6],[0,5],[0,4],[0,3],[0,2]]
move_defect(sb, p0_pos_A, p0_pos_B, p0_path, 'p', lattice)
```

で、0番目の論理量子ビット（p型欠陥）を1番目の論理量子ビット（d型欠陥）に巻きつけます。

```Python    
# braid logical qubit #2
p2_pos_A, p2_pos_B = [0,7], [3,7]
p2_path = [[3,7],[3,6],[3,5],[3,4],[3,3],[4,3],[5,3],
          [6,3],[6,4],[6,5],[6,6],[6,7],[5,7],[4,7],[3,7]]
move_defect(sb, p2_pos_A, p2_pos_B, p2_path, 'p', lattice)
```

で、2番目の論理量子ビット（p型欠陥）を1番目の論理量子ビット（d型欠陥）に巻きつけます。

```Python
# braid and annihilate logical qubit #3
p3_pos_A, p3_pos_B = [6,0], [6,2]
p3_path = [[6,2],[6,3],[6,4],[6,5],[6,6],[5,6],[4,6],[3,6],
           [3,5],[3,4],[3,3],[3,2],[4,2],[5,2],[6,2],[6,1]]
mval_p = move_defect(sb, p3_pos_A, p3_pos_B, p3_path, 'p', lattice, annihilate=True)
```

で、3番目の論理量子ビット（p型欠陥）を1番目の論理量子ビット（d型欠陥）に巻きつけて、最後に消滅させます。move_defect関数は消滅時の測定値をリターンするようになっているのでそれをmval_pとして保持しておきます（後で使います）。

```Python
# braid and annihilate logical qubit #1
d1_pos_A, d1_pos_B = [2,5], [5,5]
d1_path = [[5,5],[4,5],[3,5]]
mval_d = move_defect(sb, d1_pos_A, d1_pos_B, d1_path, 'd', lattice, annihilate=True)
```

で、1番目の論理量子ビット（d型欠陥）を測定します。測定値をmval_dとしておきます。

```Python
if mval_p == '1':
    operate_logical_Z(sb, 0, lattice)
    operate_logical_Z(sb, 2, lattice)
```

で、mval_p（3番目の論理量子ビットの消滅時の測定値）の値が-1だった場合（測定指標としては1）だった場合、最後の0番目と2番目の論理量子ビットを反転させます。

```Python
# measure logical qubits: #0 and #2
chain_0 = get_chain(get_path([0,0],[0,2]), 'p', lattice)
chain_2 = get_chain(get_path([0,7],[3,7]), 'p', lattice)
freq = measure_logical_X(sb, chain_0, chain_2, shots=shots)
print("Input('{0:}') == [CNOT] ==> {1:}".format(lq, freq))
```

で、CNOT演算になっているか確認するため、0番目の論理量子ビットと2番目の論理量子ビットを$X$測定します。両方p型欠陥なので欠陥をつなぐ$X$演算子のチェーンを測定すれば良いです。measure_logical_X演算子でそれを実行しています。測定結果はCounter形式でfreqに格納されるので、最後に表示します。以上です。


### 結果

実行結果は以下の通りです。

```
Input('++') == [CNOT] ==> Counter({'++': 10})
Input('+-') == [CNOT] ==> Counter({'--': 10})
Input('-+') == [CNOT] ==> Counter({'-+': 10})
Input('--') == [CNOT] ==> Counter({'+-': 10})
```

4つの入力パターンのすべてについて、$100\%$の確率で正しいCNOT演算の結果が得られました。


## おわりに

こんなBraidingで論理演算ができるのはとても面白いのですが、実際のハードウェア上で実装しようとすると結構大変です。大量な欠陥が格子平面上にあってそれら欠陥同士を巻きつける演算を実施しないといけないのですが、初期配置や巻きつけ方は一通りではないですし、そもそも大量の欠陥を前にして下手な実装をすると回路＝プログラムがスパゲッティ状態になりかねません（というか、スパゲッティ状態にすることで論理演算ができるのでした、うーむ）。おそらくこのあたり、コンパイラの役割なので一般人は気にすることはないのかもしれませんが、これからコンパイラを作ろうっていう人にとっては大問題ですね。やはりBraidingよりもLattice Surgeryの方がスッキリしていて見通しが良いのでしょうか（未勉強ですが）。

ということを本記事の下書きを書きながらツラツラ考えていたら、以下のような発表がありました。

- [量子コンピュータの小型化・高速化を実現する回路圧縮手法を開発−ソフトウエア新技術で大規模量子コンピュータ開発を加速化−](https://www.nii.ac.jp/news/release/2020/1112.html)
- [Effective Compression of Quantum Braided Circuits Aided by ZX-Calculus](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.10.041030)

Lattice SurgeryではなくBraidingをベースにしてZX calculusというものを使うことで効率的に回路圧縮ができるそうです（なんと）。要チェックかもしれません！（が、その前にやることが、、）

以上