クロネッカー積と複合 n 進数

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

タイトルの ”複合n進数” というのは便宜上の用語で正式名称は知らない。 ここでは各桁ごとに繰り上がる数が違う数字列のことを "複合n進数" と呼ぶこととする。 例えば、{ \displaystyle x_1 x_0 } の0桁目*1は2進数、1桁目が3進数だとすると 00, 01, 10, 11, 20, 21 と数が増えていく。

配列をディラック表記で表示するという地味なJuliaのパッケージを作っているときにある問題に直面した

テンソル積(クロネッカー積)で作った配列のi番目ってどの基底の係数になるんだ?

と。

放っておくと忘れそうなので記録に残しておく。

Qubits 系の場合

量子コンピュータで扱われるような n-qubit 系なら話は簡単。 配列のi番目の要素は {
    \displaystyle \ket{i_2}
} ( {
    \displaystyle i_2
} は i の二進数表記を意味する) の係数となる。

例えば 2-qubit 状態を計算基底 { \displaystyle \mathcal{B} = \{ \ket{00}, \ket{01}, \ket{10}, \ket{11}  \} }で展開すると {
    \displaystyle \ket{\psi} = \alpha \ket{00} + \beta \ket{01} + \gamma \ket{10} + \delta \ket{11} =
    \begin{bmatrix}
        \alpha \\ \beta \\ \gamma \\ \delta
    \end{bmatrix}_\mathcal{B}
} となるが、行列の成分を 0 始まりで数えれば 2番目の成分は { \displaystyle \ket{2_2} = \ket{10} } の係数となる。

このようにqubit 系の場合は10進数を2進数に直すだけで基底がわかるので簡単。 問題は qudits系 *2の場合である。

Qudits 系の場合

今、n-qudit系の i 番目の標準基底を {
    \displaystyle \ket{i} = \ket{ i_{n-1} i_{n-2} \dots i_1 i_0 }
} とし、j 桁目の qudit の次元を { \displaystyle d_j } とする。

冒頭で述べたように、qutrit-qubit 系の場合の標準基底は { \displaystyle \mathcal{B} = \{ \ket{00}, \ket{01}, \ket{10}, \ket{11}, \ket{20}, \ket{21}  \} } である。

そして、今私がやりたいことは { \displaystyle i }{ \displaystyle d_j ~(j = 0, \cdots, n-1) } の情報から { \displaystyle i_{n-1} i_{n-2} \dots i_1 i_0 } の各桁の数を求めることである。 全ての qudit の次元が同じならば、qubit 程ではないが簡単に求めることができるが、より一般の状態では各 qudit の次元が違うので中々面倒な計算になる。

関係式を考える

とりあえず表にまとめてみた。
qutrit-qubit の場合

{ \displaystyle i } { \displaystyle i_1 } { \displaystyle i_0 }
0 0 0
1 0 1
2 1 0
3 1 1
4 2 0
5 2 1

qubit-qutrit-qubit の場合

{ \displaystyle i } { \displaystyle i_2 } { \displaystyle i_1 } { \displaystyle i_0 }
0 0 0 0
1 0 0 1
2 0 1 0
3 0 1 1
4 0 2 0
5 0 2 1
6 1 0 0
7 1 0 1
8 1 1 0
9 1 1 1
10 1 2 0
11 1 2 1

表と にらめっこして次のような関係式が成り立つと予測。
{ \displaystyle i =  i_0 + \sum_{k=1}^{n-1}  i_k \prod_{l=0}^{k-1}  d_l }
上記の表の値を具体的に代入してみても良さそう。

これより、 { \displaystyle i_j } は次のように求められそうだと予測。 \begin{align} i_{n-1} &= \mathrm{div} \left(i, ~~\prod_{k=0}^{n-2} d_k \right) \\ i &\leftarrow i - i_{n-1} \prod_{k=0}^{n-2} d_k \end{align}

\begin{align} i_{n-2} &= \mathrm{div} \left(i, ~~\prod_{k=0}^{n-3} d_k \right) \\ i &\leftarrow i - i_{n-2} \prod_{k=0}^{n-3} d_k \\ &~~~~\vdots\\ i_1 &= \mathrm{div}(i, d_0)\\ i_0 &= i - i_1 d_0 \end{align}

これで無事に求めたいものが得られた。

Julia*3 で書いてみると以下のよう

julia> versioninfo()
Julia Version 0.7.0-alpha.217
Commit f90d674057 (2018-06-21 10:01 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i5-4460T CPU @ 1.90GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)
Environment:
  JULIA_SHELL = /usr/bin/zsh

julia> function ind2Nary(m::Int, dims::Vector{Int})
    m = m - 1
    nq = length(dims)
    ar = zeros(Int, nq)
    product = prod(dims[2:end])
    for ith in 1:nq-1
        d = div(m, product)
        m = m - d * product
        product = div(product, dims[ith+1])
        ar[ith] = d
    end
    ar[end] = m
    return ar
end

julia> d = [2,3,2];

julia> for i in 1:prod(d)
           println(i-1, ": ", ind2Nary(i, d))
       end
0: [0, 0, 0]
1: [0, 0, 1]
2: [0, 1, 0]
3: [0, 1, 1]
4: [0, 2, 0]
5: [0, 2, 1]
6: [1, 0, 0]
7: [1, 0, 1]
8: [1, 1, 0]
9: [1, 1, 1]
10: [1, 2, 0]
11: [1, 2, 1]

いい感じ♪

せっかくなのでこれを使って QuantumOptics.jlプルリクを投げたら一応マージしてもらえた。
ちょっとだけ OSS に貢献したかもしれない。

*1:量子情報分野では一番左の桁が0番目ということが多いが、今回は一番右が0番目とする。

*2:qudit: ヒルベルト空間の次元が d

*3:Julia の場合、配列が 1-base であることに注意