量子アルゴリズムの基本：Shorのアルゴリズムの確認（その２）

$$
\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/649a49e91a2a400542d3)で、アルゴリズムの説明をしたので、自作の量子計算シミュレータ[qlazy](https://github.com/samn33/qlazy)で動作確認をします。処理の流れ全体を改めて整理しておきます。

* [STEP.0] 素数判定
* [STEP.1] 因数が自明な場合、計算してアルゴリズム終了
* [STEP.2] aをランダムに選ぶ
* [STEP.3] a,Nの最大公約数gcd(a,N)を計算する
* [STEP.4] a,Nに関する位数を求める(量子位数発見)
    * [STEP.4-1] 第1レジスタを準備
    * [STEP.4-2] 第2レジスタを準備
    * [STEP.4-3] べき剰余の計算
    * [STEP.4-4] 第1レジスタを逆量子フーリエ変換
    * [STEP.4-5] 第1レジスタを測定して位数推定
* [STEP.5] 位数から因数を求める

## 実装

ということで、早速Pythonで実装したコード全体を示します。

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

### コード全体

```python:shor.py
import sys
import random
import math
import numpy as np
from qlazypy import QState

#
# custom gates
#

def swap(self,q0,q1):

    self.cx(q0,q1).cx(q1,q0).cx(q0,q1)
    return self

def hadamard(self,id=None):

    for i in range(len(id)):
        self.h(id[i])
    return self

def iqft(self,id=None):

    dim = len(id)

    for i in range(dim):
        phase = -1.0/2**i
        for j in range(i):
            self.cp(id[j],id[i],phase=phase)
            phase *= 2.0
        self.h(id[i])
            
    i = 0
    while i < dim-1-i:
        self.swap(id[i], id[dim-1-i])
        i += 1
        
    return self

def mod_exp(self,a,N,id_up,id_dn):

    # make unitary matrix: |j>|k>->|j>|a^j k mod N>
    
    bitnum_up = len(id_up)
    bitnum_dn = len(id_dn)
    
    matdim_up = 2**bitnum_up
    matdim_dn = 2**bitnum_dn
    matdim = matdim_up * matdim_dn

    mat = np.array([[0]*matdim]*matdim)
    for j in range(matdim_up):
        for k in range(matdim_dn):
            col = matdim_dn*j + k
            if k < N:
                row = matdim_dn*j + ((a**j)*k)%N
            else:
                row = col
            mat[row][col] = 1.0

    # apply unitary matrix(mat) to the quantum state

    self.apply(mat)
    
    return self

#
# functions
#

def create_register(num):

    return [0]*num

def init_register(*args):

    idx = 0
    for i in range(len(args)):
        for j in range(len(args[i])):
            args[i][j] = idx
            idx += 1
    return idx

def approx_frac(number):

    iter = 10
    eps = 0.0001

    # if input number is integer
    if number == int(number):
        return (int(number),1)
    
    # expand continued fraction
    x = number
    confra = []
    for i in range(iter):
        a = int(x)
        confra.append(a)
        x -= a
        if x < eps:
            break
        x = 1/x

    # approximate fraction (get denominator,numerator)
    denom_0, numer_0 = confra[0], 1
    denom_1, numer_1 = confra[1] * denom_0 + 1, confra[1]
    denom, numer = denom_1, numer_1
    for i in range(2,len(confra)):
        denom, numer = confra[i]*denom_1+denom_0, confra[i]*numer_1+numer_0
        denom_0, denom_1 = denom_1, denom
        numer_0, numer_1 = numer_1, numer

    return (denom, numer)
    
def discover_order(a,N):

    bitnum_dn = len(str(bin(N-1)))-2
    # bitnum_up = 2*bitnum_dn + 1
    bitnum_up = 4
    id_up = create_register(bitnum_up)
    id_dn = create_register(bitnum_dn)
    qubit_num = init_register(id_up,id_dn)
    
    qs = QState(qubit_num)
    qs.hadamard(id_up)
    qs.x(id_dn[0])
    qs.mod_exp(a,N,id_up,id_dn)

    qs.iqft(id_up)
    md = qs.m(id=id_up,shots=100)

    order = 1
    for i in range(len(md.freq_list)):
        if md.freq_list[i] != 0:
            s_r = i/(2**bitnum_up)  # s/r
            (s,r) = approx_frac(s_r)
            if r != 0 and r < N and (a**r)%N == 1 and r%2 == 0:
                order = r
                break
    qs.free()
    
    return r  # return 1 if order discovery failure

def discover_order2(a,N):
    
    for i in range(1,N):
        x = (a**i)%N
        if x == 1:
            break
    return i

def is_prime(number):

    if number == 1:
        return False
    for i in range(2,int(math.sqrt(number)+1)):
        if number%i == 0:
            return False
    return True
        
def base_exponent(number):

    size = len(str(bin(number)))-2
    for exp in reversed(range(1,size)):
        bas = int(math.pow(number,1/exp))
        if bas**exp == number:
            break
    return bas,exp
    
def shor(N):

    # if N <= 1
    if N <= 1:
        print("N must be bigger than 1")
        sys.exit()
        
    # if N is prime
    if is_prime(N) == True:
        print("N is prime")
        sys.exit()
    
    # if N is even, then factor->2
    if N%2 == 0:
        print("N is even")
        return 2
    
    # if N = bas^exp, then factor->bas
    bas,exp = base_exponent(N)
    if exp != 1:
        print("N is {0:}^{1:}, factor -> {0:}".format(bas,exp))
        return bas

    # otherwise
    for _ in range(10):
        a = random.randint(2,N-1)
        print(">> try a = ", a)
        fac = math.gcd(a,N)
        print("gcd({0:},{1:}) -> {2:}".format(a,N,fac))
        if fac != 1:
            print("factor -> ",fac)
            break
        else:
            print("order discovery start")
            r = discover_order(a,N)
            print("order -> ", r)

            if r%2 != 0:
                print("order discovery failure")
                continue

            fac = math.gcd(int(a**(r/2))-1,N)
            if fac != 1 and fac != N:
                print("factor -> ",fac)
                break
            fac = math.gcd(int(a**(r/2))+1,N)
            if fac != 1 and fac != N:
                print("factor -> ",fac)
                break
            print("factor estimation failure")

    return fac

if __name__ == '__main__':

    QState.swap = swap
    QState.hadamard = hadamard
    QState.iqft = iqft
    QState.mod_exp = mod_exp

    print("== input number ==")
    args = sys.argv
    N = int(args[1])
    print("N = ",N)

    print("== Shor's algorithm start ==")
    fac_0 = shor(N)
    fac_1 = N // fac_0

    print("== factorization ==")
    print("N = {0:} = {1:} * {2:}".format(N,fac_0,fac_1))
```

main処理部の、

```python
fac_0 = shor(N)
```

が、Shorのアルゴリズムの本体を呼び出しているところです。この関数shorの中身が本質なので、その中身を見ながら、順に説明していきます。

### 大まかな流れ

#### [STEP.0] 素数判定

まず、

```python
# if N <= 1
if N <= 1:
    print("N must be bigger than 1")
    sys.exit()
```

で、与えられた整数Nが1以下の場合は対象外なのではじいた上で、素数判定です。

```python
# if N is prime
if is_prime(N) == True:
    print("N is prime")
    sys.exit()
```

is_prime関数がTrueを返したら、「素数だよ」と言って終了します。素数判定の関数is_primeは、

```python
def is_prime(number):

    if number == 1:
        return False
    for i in range(2,int(math.sqrt(number)+1)):
        if number%i == 0:
            return False
    return True
```

です。2から順番に割り算していくのですが、$\sqrt{N}+1$まで試せば十分です。というわけで、おバカなアルゴリズムで実装しました。


#### [STEP.1] 因数が自明な場合、計算してアルゴリズム終了

```python
# if N is even, then factor->2
if N%2 == 0:
    print("N is even")
    return 2
```

Nが偶数なら、「因数は2だよ」と言って、2をリターンします。

```python
# if N = bas^exp, then factor->bas
bas,exp = base_exponent(N)
if exp != 1:
    print("N is {0:}^{1:}, factor -> {0:}".format(bas,exp))
    return bas
```

base_exponent関数で、Nをべき乗の形($N=a^b$)で書いたときの整数$a,b$を返します。$b$が1以外であれば、得られた$a$をリターンします。関数base_exponentの中身は、

```python
def base_exponent(number):

    size = len(str(bin(number)))-2
    for exp in reversed(range(1,size)):
        bas = int(math.pow(number,1/exp))
        if bas**exp == number:
            break
    return bas,exp
```

です。意外とあっさり書けました。指数$b$を変化させながら、実際に$N^{1/b}$を計算して、それが整数になっているかどうかを判定していく感じです。整数になったら終了して、そのときの$a$(変数bas)と$b$(変数exp)をリターンします。最終的に指数$b$が1だとすると、$N$はべき乗の形には書けないということなので、その判断もこの関数でできます。

#### [STEP.2] aをランダムに選ぶ

```python
# otherwise
for _ in range(10):
    a = random.randint(2,N-1)
```

forループで10回実行します(10は適当です)。何回かランダムにaを選んで、位数がわかったら、ループを抜けます。

#### [STEP.3] a,Nの最大公約数gcd(a,N)を計算する

```python
fac = math.gcd(a,N)
print("gcd({0:},{1:}) -> {2:}".format(a,N,fac))
if fac != 1:
    print("factor -> ",fac)
    break
else:
    xxx
```

最大公約数は、mathモジュールを使って計算できます。facが1以外だった場合は、「それが因数だよ」と言って、ループを抜けます。

#### [STEP.4] a,Nに関する位数を求める(量子位数発見)

それ以外の場合(facが1の場合)、else節を実行します。というわけで、Shorのアルゴリズムの肝である「量子位数発見」です。

```python
print("order discovery start")
r = discover_order(a,N)
print("order -> ", r)
```

が、大事なところなので、一旦後回しにします。rが得られたとして、先に進みます。

```python
if r%2 != 0:
    print("order discovery failure")
    continue
```

rが奇数になることがあるので、その場合、continueして、別のaを選んで再度同じことをやります。偶数のrが得られたら、次に進みます。

#### [STEP.5] 位数から因数を求める

以下、見ての通りです。

```python
fac = math.gcd(int(a**(r/2))-1,N)
if fac != 1 and fac != N:
    print("factor -> ",fac)
    break
fac = math.gcd(int(a**(r/2))+1,N)
if fac != 1 and fac != N:
    print("factor -> ",fac)
    break
print("factor estimation failure")
```

gcd(int(a**(r/2))-1,N)かgcd(int(a**(r/2))+1,N)のどちらか一方が1でもなくNでもなければ、それを因数だとして、ループを抜けます。そして、得られた因数をリターンします。

### 量子位数発見

それでは、先程説明を省略した「量子位数発見」について見ていきます。関数discover_orderの中身を見ながら、順に説明します。

#### [STEP.4-1] 第1レジスタを準備 / [STEP.4-2] 第2レジスタを準備

まず、必要なビット数を設定します。bitnum_upが第1レジスタのビット数、bitnum_dnが第2レジスタのビット数を表しています。

```python
bitnum_dn = len(str(bin(N-1)))-2
# bitnum_up = 2*bitnum_dn + 1
bitnum_up = 4
```

第2レジスタは、Nのビット長に等しくします。第1レジスタの方は、Nのビット長の2倍よりも大きくすべし、ということらしいのですが、今回量子ビット節約のため4ビットにしました(後で動作確認するくらいの整数であれば、4ビットで大丈夫でした)。

```python
id_up = create_register(bitnum_up)
id_dn = create_register(bitnum_dn)
qubit_num = init_register(id_up,id_dn)
```

各レジスタのビット数に応じて、レジスタ(実態は量子番号のリスト)を設定します。関数create_registerは、以下のようなもので、とりあえず中身が0のリストをリターンします。

```python
def create_register(num):

    return [0]*num
```

中身を入れるのは、以下のinit_register関数です。

```python
def init_register(*args):

    idx = 0
    for i in range(len(args)):
        for j in range(len(args[i])):
            args[i][j] = idx
            idx += 1
    return idx
```

ご覧の通り、引数として与えるリストの数はいくつでも良いです。指定されたリストの順番に番号を順序よく入れていきます。そして全体の量子ビット数をリターンします。

```python
qs = QState(qubit_num)
qs.hadamard(id_up)
qs.x(id_dn[0])
```

第1レジスタ全体にアダマールをかけ、第2レジスタの最初のビットを反転させます。これで、はじめに用意しておく状態の準備ができました。

#### [STEP.4-3] べき剰余の計算

```python
qs.mod_exp(a,N,id_up,id_dn)
```

ここで、問題の「べき剰余」が登場します。本来であれば、[以前の記事](https://qiita.com/SamN/items/cc44c2fb26633567ab5f)で「べき剰余」を実現した量子回路で実行できれば良かったのですが、とても小さな整数の計算にも多くの量子ビットを使ってしまうので、今回は諦めました。代わりにちょっと卑怯なのですが、べき剰余を実行した結果の量子状態を人為的に用意しました。

[前回の記事](https://qiita.com/SamN/items/649a49e91a2a400542d3)で説明したべき剰余後の状態、

```math
\frac{1}{\sqrt{2^m}} \sum_{k=0}^{2^m-1} \ket{k} \ket{a^k \space mod \space N} \tag{1}
```

をつくるために、まず以下のようなユニタリ変換を考えます。

```math
U_{a,N}: \ket{k} \ket{t} \rightarrow \ket{k} \ket{a^{k} t \space mod \space N} \tag{2}
```

このユニタリ変換を状態$\ket{k} \ket{1}$に適用すれば、式(1)の状態が得られます。なので、今回、このユニタリ変換を直接、行列の形にして、それを量子状態に適用するようにしました。

それを、以下のように実装しました。

```python
def mod_exp(self,a,N,id_up,id_dn):

    # make unitary matrix: |k>|t>->|k>|a^k t mod N>
    
    bitnum_up = len(id_up)
    bitnum_dn = len(id_dn)
    
    matdim_up = 2**bitnum_up
    matdim_dn = 2**bitnum_dn
    matdim = matdim_up * matdim_dn

    mat = np.array([[0]*matdim]*matdim)
    for j in range(matdim_up):
        for k in range(matdim_dn):
            col = matdim_dn*j + k
            if k < N:
                row = matdim_dn*j + ((a**j)*k)%N
            else:
                row = col
            mat[row][col] = 1.0

    # apply unitary matrix(mat) to the quantum state

    self.apply(mat)
    
    return self
```

変数matにその行列を表すnumpy配列が格納されます。それをqlazyのQStateクラスのapplyメソッドによって演算するようにしています(applyメソッドはv0.0.18で追加しました)。量子状態の次元は常に2のべき乗なので、行列の次元も2のべき乗にしたいのですが、Nの値によってはそうならないので、若干工夫をしています。一旦、2のべき乗の次元を用意して、上のユニタリ変換で定義されない数については恒等置換するようにしました。これで、べき剰余を実行した結果の状態が得られることになります。

discover_order関数の中身の説明に戻ります。

#### [STEP.4-4] 第1レジスタを逆量子フーリエ変換

次は、

```python
qs.iqft(id_up)
```

ということで、逆量子フーリエ変換です。関数の中身は見ての通りということで説明省略します。

#### [STEP.4-5] 第1レジスタを測定して位数推定

次に、

```python
md = qs.m(id=id_up,shots=100)

order = 1
for i in range(len(md.freq_list)):
    if md.freq_list[i] != 0:
        s_r = i/(2**bitnum_up)  # s/r
        (s,r) = approx_frac(s_r)
        if r != 0 and r < N and (a**r)%N == 1 and r%2 == 0:
            order = r
            break
```

ということで、第1レジスタを測定した結果に基づき、位数を推定します。連分数展開を使うのですが、それをapprox_frac関数で実行しています(アルゴリズムはいろいろな記事で紹介されていると思うので、中身の説明は省略します)。得られた分母が0でなくて、本当に位数になっているのかをチェックして、さらにここで偶数かどうかもチェックするようにしました。OKとなったら、その位数をリターンします。これで位数発見の処理は完了です。


## 動作確認

プログラムは、因数分解したい整数をコマンドラインから指定するようになっています。例えば、

```
python shor.py 123
```

のようにやれば計算してくれます。では、いくつかの整数でやってみます。上で説明したようにaはランダムに選ばれるので、場合によっては(aがたまたま因数だった場合とか)、量子位数発見せずに結果を返してしまいます。それでは面白くないので、何度か実行して量子位数発見を通った場合の結果のみを以下では示すようにします。

### 15の因数分解

```
== input number ==
N =  15
== Shor's algorithm start ==
>> try a =  4
gcd(4,15) -> 1
order discovery start
order ->  2
factor ->  3
== factorization ==
N = 15 = 3 * 5
```

となりました。aとして4が選ばれて、15と共通の約数がない(互いに素)ので、位数発見します。結果は2でした。これに基づき因数を推定した結果、3でした。ということで、15=3*5のように因数分解できました。

### 57の因数分解

```
==input number ==
N =  57
== Shor's algorithm start ==
>> try a =  35
gcd(35,57) -> 1
order discovery start
order ->  16
factor ->  3
== factorization ==
N = 57 = 3 * 19
```

となり、こちらも正しく因数分解できました。

### 119の因数分解

最後に119ではどうでしょうか。

```
== input number ==
N =  119
== Shor's algorithm start ==
>> try a =  108
gcd(108,119) -> 1
order discovery start
order ->  16
factor ->  17
== factorization ==
N = 119 = 17 * 7
```

ちゃんとできました。というわけで、Nのビット長が7ビットまで確認できました。2から127までのすべての数で一応やってみましたが、正しく動作することが確認できました。

## おまけ

今回、量子位数発見を量子計算シミュレータで確認しましたが、この程度の整数であれば、古典コンピュータのおバカなアルゴリズムでも十分できます(もちろん)。例えば、

```python
def discover_order2(a,N):
    
    for i in range(1,N):
        x = (a**i)%N
        if x == 1:
            break
    return i
```

こんな関数で行けます。上のプログラムのdiscover_orderをdiscover_order2に置き換えれば、量子計算をやらないバージョンのShorのアルゴリズムがお試しいただけます。意味ないですが、Shorシミュレータのデバッグには役立ちました。


## おわりに

Shorのアルゴリズムは全体通すととてもややこしいですが、一歩ずつ勉強しながら、何とか理解できたと思います。やはりプログラムを実装しながらというのは、時間はかかるのですが、確実に理解するための１つの良い方法だと改めて感じました。理解が曖昧なところでは実装の手は止まりますし、間違って理解しているところでは確実にバグが出ますので。

今回の本質は、discover_order関数に集約されています。というわけで、このdiscover_orderのところをみなさまお好きな環境(シミュレータ)で置き換えていただけると、勉強になりますし、またShorのアルゴリズム全体を通してお楽しみ(謎)いただけると思いますので、チャレンジしてみてはいかがでしょうか。でっかい整数もこんな感じで因数分解できたよ、ということがあれば、後で教えて下さい！


以上