3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

量子情報理論の基本:量子誤り訂正(スタビライザー符号:4)

Last updated at Posted at 2020-05-30

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

はじめに

前回の記事で、スタビライザー符号の理論が理解できたので、今回はスタビライザー符号の具体例を一つを取り上げ、量子計算シミュレータqlazyを使ってその動作を確認してみます。前回の終わりに「Shor符号」「Steane符号」「5量子ビット符号」の生成元を紹介しました。このうち「Shor符号」「Steane符号」はすでに別記事1で動作確認をしていたので、ここでは「5量子ビット符号」を実装して動作確認してみることにします。

準備

まず、4つの生成元および論理Z演算子を改めて掲載します。

生成元 演算子
$g_{1}$ $X \otimes Z \otimes Z \otimes X \otimes I$
$g_{2}$ $I \otimes X \otimes Z \otimes Z \otimes X$
$g_{3}$ $X \otimes I \otimes X \otimes Z \otimes Z$
$g_{4}$ $Z \otimes X \otimes I \otimes X \otimes Z$
$\bar{Z} \equiv g_{5}$ $Z \otimes Z \otimes Z \otimes Z \otimes Z$

表に入れていませんが、論理X演算子は$X \otimes X \otimes X \otimes X \otimes X$です。

これに基づき量子回路を設計したいのですが、それにはいくつかの前準備が必要です。

$\ket{00000}$から論理基底状態$\ket{0_L}$を作成するために、生成元$g_i$に対応した演算子$g_{i}^{\prime}$を$g_{i}$と反可換でその他のすべての$g_j(j \ne i)$と可換であるように選ぶ必要がありました。上の生成元に対する検査行列2を作って、$g_{i}^{\prime}$の検査行列を作成すれば良いと思い、試行錯誤した結果、以下のようなものをひねり出すことができました3

演算子
$g_{1}^{\prime}$ $Z \otimes I \otimes Z \otimes I \otimes I$
$g_{2}^{\prime}$ $Z \otimes Z \otimes Z \otimes Z \otimes I$
$g_{3}^{\prime}$ $Z \otimes Z \otimes I \otimes Z \otimes Z$
$g_{4}^{\prime}$ $Z \otimes I \otimes Z \otimes Z \otimes I$
$\bar{Z}^{\prime} \equiv g_{5}^{\prime}$ $X \otimes X \otimes X \otimes X \otimes X$

雑音集合としては、以下のように各量子ビットごとにパウリ群の演算子$X,Z,XZ$を作用させるすべてのものを用意しておきます。

雑音 演算子
$E_{0}$ $I \otimes I \otimes I \otimes I \otimes I$
$E_{1}$ $X \otimes I \otimes I \otimes I \otimes I$
$E_{2}$ $Z \otimes I \otimes I \otimes I \otimes I$
$E_{3}$ $XZ \otimes I \otimes I \otimes I \otimes I$
$E_{4}$ $I \otimes X \otimes I \otimes I \otimes I$
$E_{5}$ $I \otimes Z \otimes I \otimes I \otimes I$
$E_{6}$ $I \otimes XZ \otimes I \otimes I \otimes I$
$E_{7}$ $I \otimes I \otimes X \otimes I \otimes I$
$E_{8}$ $I \otimes I \otimes Z \otimes I \otimes I$
$E_{9}$ $I \otimes I \otimes XZ \otimes I \otimes I$
$E_{10}$ $I \otimes I \otimes I \otimes X \otimes I$
$E_{11}$ $I \otimes I \otimes I \otimes Z \otimes I$
$E_{12}$ $I \otimes I \otimes I \otimes XZ \otimes I$
$E_{13}$ $I \otimes I \otimes I \otimes I \otimes X$
$E_{14}$ $I \otimes I \otimes I \otimes I \otimes Z$
$E_{15}$ $I \otimes I \otimes I \otimes I \otimes XZ$

雑音$E_i$が加わった状態に対して生成元を測定した場合の測定値リスト$\{ \beta_{l}^{(i)}\}$も必要です。

g_l E_i = \beta_{l}^{(i)} E_i g_l \tag{1}

で計算できます。つまり、$g_l$と$E_i$が可換の場合$\beta_{l}^{(i)}=+1$で反可換の場合$\beta_{l}^{(i)}=-1$です。以下に各雑音に対する測定値リストを示します。2列目では測定値$+1$を$+$、測定値$-1$を$-$として記載しています。また、測定値が$+1$というのは$\ket{0}$、測定値が$-1$というのは$\ket{1}$のことなので、3列目ではその指標($0$または$1$)を示しています。量子回路として実装する際には、測定値は指標として得られるので3列目の$(0,1)$系列を参照した方が良いです。

雑音 $(g_1, g_2, g_3, g_4)$の測定値 測定値の指標
$E_{0}$ $(+,+,+,+)$ $(0,0,0,0)$
$E_{1}$ $(+,+,+,-)$ $(0,0,0,1)$
$E_{2}$ $(-,+,-,+)$ $(1,0,1,0)$
$E_{3}$ $(-,+,-,-)$ $(1,0,1,1)$
$E_{4}$ $(-,+,+,+)$ $(1,0,0,0)$
$E_{5}$ $(+,-,+,-)$ $(0,1,0,1)$
$E_{6}$ $(-,-,+,-)$ $(1,1,0,1)$
$E_{7}$ $(-,-,+,+)$ $(1,1,0,0)$
$E_{8}$ $(+,+,-,+)$ $(0,0,1,0)$
$E_{9}$ $(-,-,-,+)$ $(1,1,1,0)$
$E_{10}$ $(+,-,-,+)$ $(0,1,1,0)$
$E_{11}$ $(-,+,+,-)$ $(1,0,0,1)$
$E_{12}$ $(-,-,-,-)$ $(1,1,1,1)$
$E_{13}$ $(+,+,-,-)$ $(0,0,1,1)$
$E_{14}$ $(+,-,+,+)$ $(0,1,0,0)$
$E_{15}$ $(+,-,-,-)$ $(0,1,1,1)$

前準備はこれで完了です。では、量子回路を以下に示します。

量子回路

まず、論理的な基底状態$\ket{0_L}$を物理的な基底状態$\ket{00000}$から得る回路を示します。

|0> --H---*---H---M
|0> --H---|-------|-------*---H---M
|0> --H---|-------|-------|-------|-------*---H---M
|0> --H---|-------|-------|-------|-------|-------|-------*---H---M
|0> --H---|-------|-------|-------|-------|-------|-------|-------|-------*---H---M
          |       |       |       |       |       |       |       |       |       |
|0> ----|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---
|0> ----|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---
|0> ----|g1 |---|g1'|---|g2 |---|g2'|---|g3 |---|g3'|---|g4 |---|g4'|---|g5 |---|g5'|---  |0L>
|0> ----|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---
|0> ----|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---|   |---

次に、未知な1量子状態$\ket{\psi}$を量子テレポーテーションの手法を使って符号化します。

|psi> ----------*---H-----------M
                |               |
|0>   --H---*---X---------M     |
            |             |     |
      ------X-------------X-----Z---
      ------X-------------X-----Z---
|0L>  ------X-------------X-----Z--- |psi_L>
      ------X-------------X-----Z---
      ------X-------------X-----Z---

符号化状態$\ket{\psi_L}$が得られたので、雑音を付加します。ここで、Eiは何らかの雑音を表します。

        ---|  |---
        ---|  |---
|psi_L> ---|Ei|--- |psi_L'>
        ---|  |---
        ---|  |---

最後に、誤り検知のためのシンドローム測定を行い、回復のため雑音の逆演算を行えば、状態が元に戻ります。

     |0> --H---*--------------------------H-----M
     |0> --H---|-------*------------------H-----M
     |0> --H---|-------|-------*----------H-----M
     |0> --H---|-------|-------|------*---H-----M
               |       |       |      |         |
         ----|   |---|   |---|   |--|   |-----|   |----
         ----|   |---|   |---|   |--|   |-----|   |----
|psi_L'> ----|g1 |---|g2 |---|g3 |--|g4 |-----|E+ |---- |psi>
         ----|   |---|   |---|   |--|   |-----|   |----
         ----|   |---|   |---|   |--|   |-----|   |----

量子回路は以上です。

実装

それではqlazyによる実装例を示します。

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

import numpy as np
from qlazypy import QState

def logic_x(self, qid):

    [self.x(q) for q in qid]
    return self
    
def logic_z(self, qid):

    [self.z(q) for q in qid]
    return self
    
def ctr_logic_x(self, q, qid):

    [self.cx(q, qtar) for qtar in qid]
    return self

def ctr_logic_z(self, q, qid):

    [self.cz(q, qtar) for qtar in qid]
    return self

def ctr_g1(self, q, qid):

    self.cx(q, qid[0]).cz(q, qid[1]).cz(q, qid[2]).cx(q, qid[3])
    return self

def ctr_g2(self, q, qid):

    self.cx(q, qid[1]).cz(q, qid[2]).cz(q, qid[3]).cx(q, qid[4])
    return self

def ctr_g3(self, q, qid):

    self.cx(q, qid[0]).cx(q, qid[2]).cz(q, qid[3]).cz(q, qid[4])
    return self
    
def ctr_g4(self, q, qid):

    self.cz(q, qid[0]).cx(q, qid[1]).cx(q, qid[3]).cz(q, qid[4])
    return self

def encode(self, phase, qid_anc, qid_cod):

    # make logical zero state: |0>_L

    # g1
    self.h(qid_anc[0]).ctr_g1(qid_anc[0], qid_cod).h(qid_anc[0])
    self.m(qid=[qid_anc[0]])
    mval = self.m_value(binary=True)
    if mval == '1': self.z(qid_cod[0]).z(qid_cod[2])
    self.reset(qid=[qid_anc[0]])

    # g2
    self.h(qid_anc[1]).ctr_g2(qid_anc[1], qid_cod).h(qid_anc[1])
    self.m(qid=[qid_anc[1]])
    mval = self.m_value(binary=True)
    if mval == '1': self.z(qid_cod[0]).z(qid_cod[1]).z(qid_cod[2]).z(qid_cod[3])
    self.reset(qid=[qid_anc[1]])
    
    # g3
    self.h(qid_anc[2]).ctr_g3(qid_anc[2], qid_cod).h(qid_anc[2])
    self.m(qid=[qid_anc[2]])
    mval = self.m_value(binary=True)
    if mval == '1': self.z(qid_cod[0]).z(qid_cod[1]).z(qid_cod[3]).z(qid_cod[4])
    self.reset(qid=[qid_anc[2]])
    
    # g4
    self.h(qid_anc[3]).ctr_g4(qid_anc[3], qid_cod).h(qid_anc[3])
    self.m(qid=[qid_anc[3]])
    mval = self.m_value(binary=True)
    if mval == '1': self.z(qid_cod[0]).z(qid_cod[2]).z(qid_cod[3])
    self.reset(qid=[qid_anc[3]])

    # logical z
    self.h(qid_anc[4]).ctr_logic_z(qid_anc[4], qid_cod).h(qid_anc[4])
    self.m(qid=[qid_anc[4]])
    mval = self.m_value(binary=True)
    if mval == '1':
        self.x(qid_cod[0]).x(qid_cod[1]).x(qid_cod[2]).x(qid_cod[3]).x(qid_cod[4])
    self.reset(qid=[qid_anc[4]])

    # make random quantum state and encode (with quantum teleportation)

    self.reset(qid=qid_anc)
    self.u3(qid_anc[0], alpha=phase[0], beta=phase[1], gamma=phase[2]) # input quantum state
    print("* input quantum state")
    self.show(qid=[qid_anc[0]])
    
    self.h(qid_anc[1])
    self.ctr_logic_x(qid_anc[1], qid_cod).cx(qid_anc[0], qid_anc[1])
    self.h(qid_anc[0])
    self.m(qid=qid_anc[0:2])
    mval = self.m_value(binary=True)
    if mval == '00': pass
    elif mval == '01': self.logic_x(qid_cod)
    elif mval == '10': self.logic_z(qid_cod)
    elif mval == '11': self.logic_x(qid_cod).logic_z(qid_cod)
    self.reset(qid=qid_anc)
    
    return self

def add_noise(self, q, qid, kind):

    if kind == 'X': self.x(qid[q])
    elif kind == 'Z': self.z(qid[q])
    elif kind == 'XZ': self.z(qid[q]).x(qid[q])
    return self

def correct_err(self, qid_anc, qid_cod):

    self.reset(qid=qid_anc)

    # syndrome
    self.h(qid_anc[0]).ctr_g1(qid_anc[0], qid_cod).h(qid_anc[0])
    self.h(qid_anc[1]).ctr_g2(qid_anc[1], qid_cod).h(qid_anc[1])
    self.h(qid_anc[2]).ctr_g3(qid_anc[2], qid_cod).h(qid_anc[2])
    self.h(qid_anc[3]).ctr_g4(qid_anc[3], qid_cod).h(qid_anc[3])
    self.m(qid=qid_anc[0:4])
    mval = self.m_value(binary=True)
    print("* syndrome =", mval)

    # recovery
    if mval == '0000': pass
    elif mval == '0001': self.x(qid_cod[0])
    elif mval == '1010': self.z(qid_cod[0])
    elif mval == '1011': self.z(qid_cod[0]).x(qid_cod[0])
    elif mval == '1000': self.x(qid_cod[1])
    elif mval == '0101': self.z(qid_cod[1])
    elif mval == '1101': self.z(qid_cod[1]).x(qid_cod[1])
    elif mval == '1100': self.x(qid_cod[2])
    elif mval == '0010': self.z(qid_cod[2])
    elif mval == '1110': self.z(qid_cod[2]).x(qid_cod[2])
    elif mval == '0110': self.x(qid_cod[3])
    elif mval == '1001': self.z(qid_cod[3])
    elif mval == '1111': self.z(qid_cod[3]).x(qid_cod[3])
    elif mval == '0011': self.x(qid_cod[4])
    elif mval == '0100': self.z(qid_cod[4])
    elif mval == '0111': self.z(qid_cod[4]).x(qid_cod[4])

    return self

if __name__ == '__main__':

    QState.add_methods(logic_x, logic_z, ctr_logic_x, ctr_logic_z,
                       ctr_g1, ctr_g2, ctr_g3, ctr_g4,
                       encode, add_noise, correct_err)

    # create registers
    qid_anc = QState.create_register(5)
    qid_cod = QState.create_register(5)
    qnum = QState.init_register(qid_anc, qid_cod)

    # parameters for input quantum state (U3 gate params)
    phase = [np.random.rand(), np.random.rand(), np.random.rand()]

    # encode quantum state
    qs_ini = QState(qnum)
    qs_ini.encode(phase, qid_anc, qid_cod)
    qs_fin = qs_ini.clone()

    # noise
    q = np.random.randint(0, len(qid_cod))
    kind = np.random.choice(['X','Z','XZ'])
    print("* noise '{:}' to #{:} qubit".format(kind, q))
    qs_fin.add_noise(q, qid_cod, kind)

    # error correction
    qs_fin.correct_err(qid_anc, qid_cod)

    # result
    fid = qs_ini.fidelity(qs_fin, qid=qid_cod)
    print("* fidelity = {:.6f}".format(fid))
    
    QState.free_all(qs_ini, qs_fin)

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

QState.add_methods(logic_x, logic_z, ctr_logic_x, ctr_logic_z,
                   ctr_g1, ctr_g2, ctr_g3, ctr_g4,
                   encode, add_noise, correct_err)

で、上で示したカスタム・メソッドを量子状態を表すQStateクラスのメソッドとして登録します。

# create registers
qid_anc = QState.create_register(5)
qid_cod = QState.create_register(5)
qnum = QState.init_register(qid_anc, qid_cod)

で、これから使用するレジスタを生成します。これにより、qid_anc=[0,1,2,3,4], qid_cod=[5,6,7,8,9], qnum=10という具合に変数値が設定されます4。qid_ancは5個の補助量子ビットです。上で示した量子回路において、論理基底状態を作成するために5個、量子テレポーテーションでの符号状態作成に2個、誤り訂正のために4個の補助量子ビットが必要です。各ステップの直前で状態をリセットできるので5個の補助量子ビットがあれば十分です。qid_codは符号状態を記述するための量子ビットです。5量子ビット符号なので5量子ビット用意しておけば良いです。

# parameters for input quantum state (U3 gate params)
phase = [np.random.rand(), np.random.rand(), np.random.rand()]

で、符号状態を適当に作成するため3個のランダムパラメータphaseを生成しています(U3ゲート演算するための3つのパラメータです)。

# encode quantum state
qs_ini = QState(qnum)
qs_ini.encode(phase, qid_anc, qid_cod)
qs_fin = qs_ini.clone()

で、まず補助量子ビットも含めた初期状態を作成して、それに対してencodeメソッドで符号状態に変換します。qs_iniをqs_finにコピーしていますが、これは最終的に元の状態と誤り訂正後の状態が一致しているかどうか評価するためのものです。

では、encodeメソッドの中身を見てみます。

# g1
self.h(qid_anc[0]).ctr_g1(qid_anc[0], qid_cod).h(qid_anc[0])
self.m(qid=[qid_anc[0]])
mval = self.m_value(binary=True)
if mval == '1': self.z(qid_cod[0]).z(qid_cod[2])
self.reset(qid=[qid_anc[0]])

 ...
 

で、論理基底状態$\ket{0_L}$を作成します。上の量子回路で示した通りの演算を順に実行します。間接測定の測定値(の指標)はmval変数に入ります。これが"0"だった場合何もせず、"1"だった場合$g_{1}^{\prime} = ZIZII$を演算します。終わったら使用した0番目の補助量子ビットをリセットします。以下、同様のことを他のすべての生成元と論理Z演算子に対して実行します。

self.u3(qid_anc[0], alpha=phase[0], beta=phase[1], gamma=phase[2]) # input quantum state
print("* input quantum state")
self.show(qid=[qid_anc[0]])

で、0番目の補助量子ビットにphaseを引数としたU3ゲートを演算することでランダムな1量子ビット状態を作ります。そして、その状態を表示します。

self.h(qid_anc[1])
self.ctr_logic_x(qid_anc[1], qid_cod).cx(qid_anc[0], qid_anc[1])
self.h(qid_anc[0])
self.m(qid=qid_anc[0:2])
mval = self.m_value(binary=True)
if mval == '00': pass
elif mval == '01': self.logic_x(qid_cod)
elif mval == '10': self.logic_z(qid_cod)
elif mval == '11': self.logic_x(qid_cod).logic_z(qid_cod)
self.reset(qid=qid_anc)

で、量子テレポーテーションの回路を実行します。mvalには2つの測定を実行した結果が入り、その4パターンに応じて、何もしない/論理X/論理Z/論理XZ演算子を実行すれば、先程ランダムに作成した1量子ビット状態から5量子ビットの符号状態を得ることができます。

main処理部に戻ります。

# noise
q = np.random.randint(0, len(qid_cod))
kind = np.random.choice(['X','Z','XZ'])
print("* noise '{:}' to #{:} qubit".format(kind, q))
qs_fin.add_noise(q, qid_cod, kind)

で、5量子ビットの符号状態に対してランダムにノイズを加えます。量子ビットをランダムに選び、さらに雑音のタイプをX/Z/XZの中からランダムに選びます。実際の処理はadd_noiseメソッドで行います。詳細は関数定義を見てください。

# error correction
qs_fin.correct_err(qid_anc, qid_cod)

で、誤り訂正を実行します。

correct_errメソッドの中身を見てみます。

self.reset(qid=qid_anc)

で、まず補助量子ビットをすべてリセットします。

# syndrome
self.h(qid_anc[0]).ctr_g1(qid_anc[0], qid_cod).h(qid_anc[0])
self.h(qid_anc[1]).ctr_g2(qid_anc[1], qid_cod).h(qid_anc[1])
self.h(qid_anc[2]).ctr_g3(qid_anc[2], qid_cod).h(qid_anc[2])
self.h(qid_anc[3]).ctr_g4(qid_anc[3], qid_cod).h(qid_anc[3])
self.m(qid=qid_anc[0:4])
mval = self.m_value(binary=True)
print("* syndrome =", mval)

で、誤り検知の測定(シンドローム測定)を行います。その測定値(4桁のバイナリ文字列)が変数mvalに入ります。このバイナリ文字列に応じて、

# recovery
if mval == '0000': pass
elif mval == '0001': self.x(qid_cod[0])
elif mval == '1010': self.z(qid_cod[0])
elif mval == '1011': self.z(qid_cod[0]).x(qid_cod[0])
elif mval == '1000': self.x(qid_cod[1])
elif mval == '0101': self.z(qid_cod[1])
elif mval == '1101': self.z(qid_cod[1]).x(qid_cod[1])
elif mval == '1100': self.x(qid_cod[2])
elif mval == '0010': self.z(qid_cod[2])
elif mval == '1110': self.z(qid_cod[2]).x(qid_cod[2])
elif mval == '0110': self.x(qid_cod[3])
elif mval == '1001': self.z(qid_cod[3])
elif mval == '1111': self.z(qid_cod[3]).x(qid_cod[3])
elif mval == '0011': self.x(qid_cod[4])
elif mval == '0100': self.z(qid_cod[4])
elif mval == '0111': self.z(qid_cod[4]).x(qid_cod[4])

で、雑音の逆演算を行います。これで符号状態は元に戻るはずです。

main処理部に戻ります。

# result
fid = qs_ini.fidelity(qs_fin, qid=qid_cod)
print("* fidelity = {:.6f}".format(fid))

で、元の量子状態と誤り訂正後の量子状態との違いを見るためのフィデリティを計算して表示します5。誤り訂正が成功していればfidは1.0になるはずです。

動作確認

では、上のプログラムを実行してみます。

* input quantum state
c[0] = +0.3382-0.0000*i : 0.1143 |++
c[1] = -0.6657+0.6652*i : 0.8857 |++++++++++
* noise 'Z' to #4 qubit
* syndrome = 0100
* fidelity = 1.000000

適当に設定された1量子状態が符号化され、4番目の量子ビットに雑音'Z'が加わったのですが、シンドローム測定により誤りのパターンが'0100'($E_{14}=IIIIZ$)であることがわかり、逆演算を施した結果、フィデリティが1.000000となりました。というわけで、誤り訂正は成功しました。

もう一回実行すると、

* input quantum state
c[0] = +0.7701-0.0000*i : 0.5931 |+++++++
c[1] = -0.1945+0.6075*i : 0.4069 |+++++
* noise 'X' to #1 qubit
* syndrome = 1000
* fidelity = 1.000000

となり、先程とは別の状態に対して別のパターンの雑音が付加されましたが、この場合も誤り訂正は成功です。何度も実行してみましたが、失敗することはありませんでした。というわけで、めでたし、めでたし、です。

おわりに

これで、4回にわたって続けてきた「スタビライザー符号」シリーズは一応完了です。スタビライザー符号の標準形とか論理基底状態をユニタリ演算だけで作成する方法など、興味深い積み残し課題はあるのですが、先に進みたいと思います。次回は、「フォールトトレラントな量子計算」を予定しています。

以上

  1. これこれです。

  2. 以前の記事を参照。

  3. もしかしたら汎用的でスマートな手続きがあるのかもしれませんが、できたのでよしとしました。

  4. もちろん、こんなクラス・メソッドを使わないで直接変数値をセットしても良いです。複雑な量子回路を相手にする場合、こうした方が楽に実装できると思います。いまの例はそれほど複雑ではないのでどっちでもいいですが。

  5. fidelityメソッドの引数qidを指定すると、特定の量子ビット番号リストで表される部分系についてのフィデリティを計算します。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?