🔖

量子コンピュータのシミュレータをRustで作る Part2

2022/04/26に公開

量子コンピュータのシミュレータをRustで作るの続きです。

2量子ビットケート(CNOTゲート)

次に2量子ビットケートのCNOTゲートを実装してみます。
回路図とゲートの行列の手計算を行います。

※初期状態を\ket{00}としてしまいましたが気にしないでください。。

CNOT

CNOTのブラケット表記は以下になります。

\ket{0}\bra{0} \otimes I + \ket{1}\bra{1} \otimes X \\

1番目のビットが\bra{0}の場合に、\ket{0}になって、2番目のビットに対しては何もしない。(Iゲートを適用)
1番目のビットが\bra{1}の場合に、\ket{1}になって、2番目のビットに対してXゲートを適用する。
行列で計算してみると以下になります。

\ket{0}\bra{0} \otimes I + \ket{1}\bra{1} \otimes X \\ = \begin{pmatrix} 1 \\ 0 \\ \end{pmatrix} \begin{pmatrix} 1 & 0 \end{pmatrix} \otimes I + \begin{pmatrix} 1 \\ 0 \\ \end{pmatrix} \begin{pmatrix} 1 & 0 \end{pmatrix} \otimes X \\ = \begin{pmatrix} 1 & 0 \\ 0 & 0 \\ \end{pmatrix} \otimes I + \begin{pmatrix} 0 & 0 \\ 0 & 1 \\ \end{pmatrix} \otimes X \\ = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ \end{pmatrix} + \begin{pmatrix} 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ \end{pmatrix} \\ = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ \end{pmatrix}

3量子ビットの回路に対してCNOTを適用する

次に3量子ビットの回路に対して、1番目と2番目に対してCNOTを適用してみます。


※Xゲートを置くのが、面倒なので初期状態を\ket{001}にしています。

ブラケット表記

(I \otimes I \otimes \ket{0}\bra{0} + I \otimes X \otimes \ket{1}\bra{1})\ket{001} \\

量子状態を適用しようとすると、右から適用する形になるため、このようなブラケット表記となります。
※ここら辺ややこしくて自分が間違っているかも知れない。。

行列表記

(I \otimes I \otimes \ket{0}\bra{0} + I \otimes X \otimes \ket{1}\bra{1})\ket{001} \\ = \begin{pmatrix} \begin{pmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{pmatrix} + \begin{pmatrix} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ \end{pmatrix} \end{pmatrix} \ket{001} \\ = \begin{pmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ \end{pmatrix} \ket{001} \\ = \begin{pmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ \end{pmatrix} \begin{pmatrix} 0 \\ 1 \\ 0 \\ 0 \\ 0 \\ 0 \\ 0 \\ 0 \\ \end{pmatrix} \\ = \begin{pmatrix} 0 \\ 0 \\ 0 \\ 1 \\ 0 \\ 0 \\ 0 \\ 0 \\ \end{pmatrix}

プログラム実行

use qustV2::circuit::QuantumCircuit;
use qustV2::gates::double::DoubleGateApplicator;
use qustV2::gates::single::SingleGateApplicator;

fn main() {
    let n = 3;
    let mut circuit = QuantumCircuit::new(n);
    circuit.X(0);
    circuit.CNOT(0, 1);
    circuit.update_quantum_state();
    println!("{}", circuit);
}

プログラム実行の場合、初期状態を\ket{000}としているため、1ビット目にまずXゲートを適用して、\ket{001}を作ってから、CNOTを適用しています。

* Qubit Count  : 3
* Dimension    : 8
* State vector :
000> 0+0i
001> 0+0i
010> 0+0i
011> 1+0i
100> 0+0i
101> 0+0i
110> 0+0i
111> 0+0i

手計算と一致しました。

2量子ビットゲートの場合の状態ベクトルのインデックス

2量子ビットゲートの場合、状態ベクトルは以下のように取ります。

idx bi0 bi1 bi2 bi3
0-1 0 2 1 3
4 6 5 7
1-2 0 4 2 6
1 5 3 7
  • idx: 量子ビットゲートの開始インデックス(0-1の場合、1番目と2番目にゲートを配置する)
  • bi0: 0基底として取得する状態ベクトルのインデックス
  • bi1: 1基底として取得する状態ベクトルのインデックス
  • bi2: 2基底として取得する状態ベクトルのインデックス
  • bi3: 3基底として取得する状態ベクトルのインデックス

2量子ビットの場合は、4個ずつ状態ベクトルから取得してゲートを適用します。

ソースコード

肝となる状態ベクトルから取得するインデックスは、以下のようになります。

pub fn indices_vec(
    index: usize,
    qubits_tgt: &Vec<usize>,
    masks: &[usize],
) -> Vec<usize> {
    let mut qubits = qubits_tgt.to_owned();
    qubits.sort();
    let mut res = Vec::with_capacity(qubits.len());
    let mask = masks[0];
    let mask_low = masks[1];
    let mask_high = masks[2];
    let basis_0 = (index & mask_low) + ((index & mask_high) << qubits.len());
    let target_mask1 = 1usize << qubits[1];
    let target_mask2 = 1usize << qubits[0];
    let basis_1 = basis_0 + target_mask1;
    let basis_2 = basis_0 + target_mask2;
    let basis_3 = basis_1 + target_mask2;
    res.push(basis_0);
    res.push(basis_1);
    res.push(basis_2);
    res.push(basis_3);

    res
}

maskを取る関数であるmask_vec()は、1量子ビットゲートと共有です。
基本となるlet basis_0 = (index & mask_low) + ((index & mask_high) << qubits.len())も1量子ビットゲートと同じです。
そこから、target_mask1target_mask2で幅を取ります。

let target_mask1 = 1usize << qubits[1];
let target_mask2 = 1usize << qubits[0];
let basis_1 = basis_0 + target_mask1;
let basis_2 = basis_0 + target_mask2;
let basis_3 = basis_1 + target_mask2;

後付の制御ビット

CNOTゲートは、制御ビットとXゲートの組み合わせで作ることができます。
以下のように、Xゲートに対して、制御ビットをつけることが可能です。

use qustV2::circuit::QuantumCircuit;
use qustV2::gates::gate::GateOp;
use qustV2::gates::single::{SingleGateApplicator, X};

fn main() {
    let n = 3;
    let mut circuit = QuantumCircuit::new(n);
    // |001>にする
    circuit.X(0);

    // Xゲートに制御ビットを付与する
    let mut x_gate = X(1);
    x_gate.add_control_qubit(0);
    circuit.add_gate(x_gate);
    
    circuit.update_quantum_state();
    println!("{}", circuit);
}

実行すると、CNOTと同じ出力になります。

* Qubit Count  : 3
* Dimension    : 8
* State vector :
000> 0+0i
001> 0+0i
010> 0+0i
011> 1+0i
100> 0+0i
101> 0+0i
110> 0+0i
111> 0+0i

制御ビット対応のindices_vec()

以下が、制御ビットに対応したindices_vec()です。

pub fn indices_vec(
    index: usize,
    qubits_ctl: &Vec<usize>,
    qubits_tgt: &Vec<usize>,
    masks: &[usize],
) -> Vec<usize> {
    let mut qubits = qubits_tgt.to_owned();
    qubits.sort();
    let mut res = Vec::with_capacity(qubits.len());
    let mask = masks[0];
    let mask_low = masks[1];
    let mask_high = masks[2];
    if qubits.len() == 1 {
        if qubits_ctl.len() > 0 {
            // TODO: 複数制御ビット対応
            let control_mask = 1usize << qubits_ctl[0];
            let qsize = qubits_ctl.len() + qubits_tgt.len();
            let basis_0 = (index & mask_low) + ((index & mask_high) << qsize) + control_mask;
            let target_mask = 1usize << qubits_tgt[0];
            let basis_1 = basis_0 + target_mask;
            res.push(basis_0);
            res.push(basis_1);
        } else {
            let basis_0 = (index & mask_low) + ((index & mask_high) << qubits.len());
            let basis_1 = basis_0 + mask;
            res.push(basis_0);
            res.push(basis_1);
        }
    } else if qubits.len() == 2 {
        let basis_0 = (index & mask_low) + ((index & mask_high) << qubits.len());
        let target_mask1 = 1usize << qubits[1];
        let target_mask2 = 1usize << qubits[0];
        let basis_1 = basis_0 + target_mask1;
        let basis_2 = basis_0 + target_mask2;
        let basis_3 = basis_1 + target_mask2;
        res.push(basis_0);
        res.push(basis_1);
        res.push(basis_2);
        res.push(basis_3);
    } else {
        // TODO
        unimplemented!();
    }

    res
}

まだ、複数制御ビットには対応していないです。。
そして、ターゲットビットは1ビットしか対応していないです。。

以下の箇所が該当箇所です。

// TODO: 複数制御ビット対応
let control_mask = 1usize << qubits_ctl[0];
let qsize = qubits_ctl.len() + qubits_tgt.len();
let basis_0 = (index & mask_low) + ((index & mask_high) << qsize) + control_mask;
let target_mask = 1usize << qubits_tgt[0];
let basis_1 = basis_0 + target_mask;
res.push(basis_0);
res.push(basis_1);

ベースとなる基底をlet basis_0 = (index & mask_low) + ((index & mask_high) << qsize) + control_maskとします。
qsizeは、ターゲットビットと制御ビットをあわせたサイズとなります。
control_maskは、1usize << qubits_ctl[0]で計算された値になります。
制御ビットのインデックス分、左シフトします。

ゲート間の隙間

現状、後から付与する制御ビットは隣接ビットのみの制限があります。
つまり、Xゲートを2番目のビットに適用する場合、制御ビットは1番目か3番目に配置する必要があります。
間をあけて配置する場合は、その分マスクして、幅を取る必要があります。

例えば、qulacsのmid_maskがそれにあたるかと思います。

このようにゲートに隙間があっても大丈夫なように状態ベクトルのインデックスを取るようにできれば、回路の最適化で小さい回路をまとめてしまう場合に便利な気がします。
基本的には、小さい回路同士を先にまとめてしまう方が全体の計算量が下がるはずです。
今後の展開としてこのような最適化を行っていきたいところです。
最近はテンソルネットワークが流行っているようなので、その考え方を取り入れて最適化したいものです。

今後の展開

大きいサイズの量子ビットに対応するために、分散化と並列化を行いたいと考えています。
量子ゲートのサイズと開始インデックスで、状態ベクトルのインデックスの取り方が決まります。
そのため、複数ノードに分散させた量子シミュレータを考えた場合、できるだけノード間通信を少なくするために
回路を最適化することが考えられます。
スーパーコンピュータ「富岳」のテクノロジーを活用し、36量子ビットの世界最速量子シミュレータの開発に成功 : 富士通で、富士通が、qulacsをOpenMPI対応させて分散並列化したようです。
論文を読んだ限りでは、[2203.16044] mpiQulacs: A Distributed Quantum Computer Simulator for A64FX-based Cluster SystemsどうやらSWAPゲートを入れることで、回路を最適化してノード間通信を減らしているようです。
自分もテンソルネットワークを駆使して、いろいろと最適化したいもんです。。

Discussion