タグ: algorithm error-correction
量子ビットはノイズで簡単に状態が乱れます。量子誤り訂正(Quantum Error Correction; QEC) は、1つの論理量子ビットを複数の物理量子ビットに分散させて情報を守る技術です。
この記事は QEC 入門の前編です。次の3つの符号を Qamomile の @qkernel で実装します。
3量子ビット bit-flip 符号 ― 単一のビット反転()エラーを訂正する。
3量子ビット phase-flip 符号 ― 単一の位相反転()エラーを訂正する。
Shor の9量子ビット符号 ― 任意の単一量子ビットエラー(, , )を訂正する。
後編の スタビライザ形式論と Steane 符号 では、これらを統一的に扱う枠組みを説明します。
前提知識: @qkernel、CNOT ゲート、測定。未習の場合は 最初の量子カーネル を先にご覧ください。
# 最新のQamomileをpipからインストールします。
# !pip install qamomile
# # or
# !uv add qamomile1. なぜ量子の誤り訂正は難しいのか¶
実際の量子誤り訂正に入る前に、量子の場合は古典と何が違って難しいのかを説明します。
1.1 古典の反復符号¶
古典コンピュータでビット をノイズから守る、いちばんシンプルな方法が 反復符号 です。
符号化: 0 → 000 1 → 111
計算: 符号化したまま処理する
復元: 最後に3ビットを読み、多数決をとる途中で1ビットが反転しても(000 → 010 など)、多数決で元の値に戻せます。
1.2 符号化は量子でもそのまま作れる¶
同じ発想を量子に持ち込みます。論理状態
を3量子ビットに広げます。CNOT ゲートを2つ使うと、次の状態が作れます。
これは状態を3つ複製したもの()ではなく、3量子ビットにまたがる エンタングルメント(量子もつれ) です。符号化はこのように問題なく作れます。
1.3 本当の壁は「訂正のしかた」¶
詰まるのは符号化ではなく 訂正 です。
古典の反復符号は、訂正のときに3ビットを 読んで 多数決をとります。古典のビットは読んでも壊れません。
量子で「読む」ことは 測定 であり、測定は重ね合わせを壊します。 をそのまま測れば か に確定し、 と は失われます。
計算の最後の読み出しなら、古典と同じく「全部測って多数決」で構いません。困るのは計算の途中で訂正したいとき ― 続きの計算のために状態を保ったまま、エラーだけを直したい場面です。
そこで量子誤り訂正は、データそのものを測らず、「どこに、どんなエラーが起きたか」だけ を測ります。このエラーの位置と種類を表す情報を シンドローム(syndrome) と呼びます。 シンドロームはもともと医学用語で「症候群」、つまり複数の症状の組み合わせを指す言葉です。医師が病気そのものを直接見ずに症状から診断するように、エラー自体を測らず、その「症状」だけからエラーの正体を突き止める ― この呼び名はそこにちなんでいます。
シンドロームは補助量子ビット(ancilla)を経由して取り出すので、論理状態()には触れません。この操作が シンドローム測定 です。
1.4 もう1つの違い:位相エラー¶
古典と量子には、もう1つ違いがあります。
古典のエラーはビット反転()だけです。量子ビットには 位相エラー があります。 の符号だけが反転する
というエラーは、計算基底で測っても確率が変わらず、多数決では検出できません。
さらに量子のエラーは 連続的 で、ビットがわずかに回転するような中間的なエラーも無数にありえます。これを1つずつ訂正するのは無理に見えますが、実は離散的なエラーだけ考えれば十分です。
ここでは簡単に2段階の説明をしておきます。 まず、1量子ビットへのどんなエラーも、行列としては (何もしない)・・・ の線形結合で書けます。 たとえば微小回転は と が少しずつ混ざったものです。このエラーを符号化された状態に作用させると、状態は「エラーなし」「 エラー」「 エラー」… が重ね合わさったものになります。
次に、この状態のシンドロームを測ると、重ね合わさっていたケースのうち1つに 収縮 します。測定後に残るのは「量子ビット に エラー」のような離散的なエラーが1つだけです。連続的なエラーが、シンドローム測定によって離散的な Pauli エラー に変わるのです(この収縮は次節以降で実際に確認します)。
そのため訂正すべきは完全な ・・ だけです。しかも なので、 と さえ訂正できれば も自動的にカバーされます。
1.5 量子誤り訂正の流れ¶
量子誤り訂正は次の流れで進みます。
符号化 → エラー発生 → シンドローム測定 → 訂正符号化: 1つの論理量子ビットを、もつれを使って複数の物理量子ビットに広げる。
シンドローム測定: 論理状態を測らず、エラーの位置と種類だけを補助量子ビットに取り出す。
訂正: シンドロームに応じて Pauli ゲートを当て、エラーを打ち消す。
次節からは、いちばんシンプルな3量子ビット bit-flip 符号で、この流れを実装していきます。
実装に入る前に、QamomileとQiskit連携を読み込み、補助関数を2つ用意します。_first_bit_distributionと_sample_first_bitは、カーネルをコンパイル・実行して先頭ビットの0/1集計を返すだけのユーティリティです。QECの本筋ではないので、読み飛ばして構いません。
import math
import os
import qamomile.circuit as qmc
from qamomile.circuit import SampleResult
from qamomile.qiskit import QiskitTranspiler
docs_test_mode = os.environ.get("QAMOMILE_DOCS_TEST") == "1"
default_shots = 64 if docs_test_mode else 256
superposition_shots = 512 if docs_test_mode else 2000
transpiler = QiskitTranspiler()
# ドキュメントの出力を再現可能にするため、シード付きのバックエンドを用意します。
from qiskit_aer import AerSimulator
_seeded_backend = AerSimulator(seed_simulator=42, max_parallel_threads=1)
_seeded_executor = transpiler.executor(backend=_seeded_backend)
def _first_bit_distribution(result: SampleResult) -> dict[int, int]:
"""先頭の測定ビットについて、0 と 1 の出現回数を返す。"""
counts = {0: 0, 1: 0}
for outcome, count in result.results:
bit = outcome[0] if isinstance(outcome, (list, tuple)) else outcome & 1
counts[bit] += count
return counts
def _sample_first_bit(
kernel,
*,
bindings: dict[str, object] | None = None,
parameters: list[str] | None = None,
runtime_bindings: dict[str, object] | None = None,
shots: int = default_shots,
) -> dict[int, int]:
"""カーネルをコンパイル・実行し、先頭ビットの 0/1 出現回数を返す。"""
executable = transpiler.transpile(
kernel,
bindings=bindings or {},
parameters=parameters or [],
)
job = executable.sample(
_seeded_executor,
shots=shots,
bindings=runtime_bindings or {},
)
return _first_bit_distribution(job.result())2. 3量子ビット bit-flip 符号¶
最初に作るのは bit-flip 符号 です。ビット反転()エラー1つだけを訂正する、いちばんシンプルな量子誤り訂正符号です。1.5 で見た流れ ― 符号化 → エラー → シンドローム測定 → 訂正 ― を、この符号で一通り実装します。
2.1 狙うエラー:ビット反転¶
エラーは量子ビットを と反転させます。古典のビット反転にあたるエラーです。bit-flip 符号は、この単一の エラーを訂正することを目標にします。
2.3 符号化回路¶
符号化は、データ量子ビット から , へ CNOT を1つずつかけるだけです。
@qmc.qkernel
def encode_3qubit_bitflip(
q0: qmc.Qubit, q1: qmc.Qubit, q2: qmc.Qubit
) -> tuple[qmc.Qubit, qmc.Qubit, qmc.Qubit]:
q0, q1 = qmc.cx(q0, q1)
q0, q2 = qmc.cx(q0, q2)
return q0, q1, q2が なら、2つの CNOT が , も反転させて になります。 が なら何も起こらず のままです。 が重ね合わせ なら、結果は になります。
2.4 シンドローム測定: パリティ¶
エラーの位置を知るために、2つの パリティ(2量子ビットが同じ値かどうか)を測ります。
― と が同じ値か
― と が同じ値か
符号空間( と )では3量子ビットはすべて同じ値なので、どちらのパリティも「等しい」を返します。 エラーが1つ入ると、その位置に応じてパリティのパターンが変わります。
| エラー | シンドローム | 訂正 |
|---|---|---|
| なし | なし | |
3つの エラーはそれぞれ異なるシンドロームを示すので、シンドロームからエラーの位置が一意に決まります。
を測るには、補助量子ビットを1つ用意し、CX(data[i], anc) と CX(data[j], anc) をかけてから anc を測定します。測るのは補助量子ビットだけで、データ量子ビットには触れません。
具体例で追ってみましょう。符号化された状態 の2番目の量子ビット に エラーが入ったとします。 は を反転させるので、状態は
になります。ここに で初期化した補助量子ビットを2つ加え、シンドローム測定を行います。CX は制御量子ビットの値を補助量子ビットへ XOR で足し込むので、
:補助量子ビット1に が入る。 では 、 では 。
:補助量子ビット2に が入る。 では 、 では 。
どちらの項でも補助量子ビットの値は同じ になり、状態は次のように変化します。
重ね合わせの2つの項が 同じ 補助量子ビットの値を与える点が重要です。そのため補助量子ビットを測定しても、 と の重ね合わせは壊れません。 測定で得られるのはシンドローム だけで、表を見るとこれは の エラーを指しています。
2.5 訂正¶
シンドロームが分かれば、訂正は検出したエラーと同じ を同じ位置にもう一度かけるだけです。 は2回かけると元に戻る()ので、エラーの と訂正の が打ち消し合います。
さきほどの例を続けます。シンドローム は の エラーを表すので、 に をかけると、
と、符号化された状態がそのまま復元されます。上の表の「訂正」列が、各シンドロームに対応する訂正です。
2.6 実装と実行:論理 ¶
ここまでを1つの @qkernel にまとめます。error_pos でエラーを入れる位置を指定し、符号化・エラー注入・シンドローム測定・訂正までを行います。
@qmc.qkernel
def bitflip_syndrome_run(
error_pos: qmc.UInt,
theta: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
# データ用に3量子ビット、シンドローム測定用に補助量子ビットを2つ確保する。
data = qmc.qubit_array(3, name="data")
anc = qmc.qubit_array(2, name="anc")
# 論理状態を ry(theta) で用意し、3量子ビットに符号化する。
data[0] = qmc.ry(data[0], theta)
data[0], data[1], data[2] = encode_3qubit_bitflip(data[0], data[1], data[2])
# error_pos で指定した位置に X エラーを注入する(error_pos=3 ならエラーなし)。
for i in qmc.range(3):
if error_pos == i:
data[i] = qmc.x(data[i])
# シンドローム測定 1: Z0 Z1 パリティを anc[0] に取り出す。
data[0], anc[0] = qmc.cx(data[0], anc[0])
data[1], anc[0] = qmc.cx(data[1], anc[0])
s0 = qmc.measure(anc[0])
# シンドローム測定 2: Z0 Z2 パリティを anc[1] に取り出す。
data[0], anc[1] = qmc.cx(data[0], anc[1])
data[2], anc[1] = qmc.cx(data[2], anc[1])
s1 = qmc.measure(anc[1])
# シンドローム (s0, s1) からエラー位置を特定し、同じ位置に X をかけて訂正する。
if s0 & s1: # (1, 1) -> data[0]
data[0] = qmc.x(data[0])
if s0 & ~s1: # (1, 0) -> data[1]
data[1] = qmc.x(data[1])
if ~s0 & s1: # (0, 1) -> data[2]
data[2] = qmc.x(data[2])
return qmc.measure(data)error_pos はコンパイル時に決まるパラメータです。値 0, 1, 2 はその位置に エラーを注入します。値 3 はどの分岐にも一致しないので、「エラーなし」を意味します。
まず論理 を用意します(theta の ry ゲート)。訂正が正しく働けば、data[0] は常に 1 になるはずです。
bitflip_cases = [
("no error", 3),
("X on data[0]", 0),
("X on data[1]", 1),
("X on data[2]", 2),
]
if docs_test_mode:
bitflip_cases = [
("no error", 3),
("X on data[1]", 1),
]
print("3-qubit bit-flip code: logical |1>")
for label, error_pos in bitflip_cases:
counts = _sample_first_bit(
bitflip_syndrome_run,
bindings={"error_pos": error_pos},
parameters=["theta"],
runtime_bindings={"theta": math.pi},
)
print(f" {label:14s}: data[0]=0 -> {counts[0]:3d}, data[0]=1 -> {counts[1]:3d}")
# 純粋な |1> 入力に対する単一 X 訂正は完全に決定的で、各ショットが
# data[0] = 1 を返す。
assert counts[0] == 0
assert counts[1] == counts[0] + counts[1]3-qubit bit-flip code: logical |1>
no error : data[0]=0 -> 0, data[0]=1 -> 256
X on data[0] : data[0]=0 -> 0, data[0]=1 -> 256
X on data[1] : data[0]=0 -> 0, data[0]=1 -> 256
X on data[2] : data[0]=0 -> 0, data[0]=1 -> 256
print("3-qubit bit-flip code: superposition input")
for label, error_pos in bitflip_cases:
counts = _sample_first_bit(
bitflip_syndrome_run,
bindings={"error_pos": error_pos},
parameters=["theta"],
runtime_bindings={"theta": math.pi / 3},
shots=superposition_shots,
)
total = counts[0] + counts[1]
print(f" {label:14s}: P(data[0]=1) = {counts[1] / total:.3f}")
assert abs(counts[1] / total - 0.25) < (0.08 if docs_test_mode else 0.05)
assert total == superposition_shots3-qubit bit-flip code: superposition input
no error : P(data[0]=1) = 0.269
X on data[0] : P(data[0]=1) = 0.269
X on data[1] : P(data[0]=1) = 0.269
X on data[2] : P(data[0]=1) = 0.269
2.8 限界:位相エラーには無力¶
bit-flip 符号が訂正できるのは エラーだけです。位相エラー には無力です。
理由は パリティの測り方にあります。 エラーは や の符号を変えるだけで、ビットの値は変えません。 パリティはビットが等しいかどうかしか見ないので、 エラーが入ってもシンドロームは のまま ― エラーを検出できないのです。
エラーを訂正するには、別の符号が必要です。次節では、Hadamard ゲートを使って bit-flip 符号を「位相の世界」に移し替えた phase-flip 符号 を作ります。
3. 3量子ビット phase-flip 符号¶
bit-flip 符号は エラーしか訂正できませんでした。次は位相エラー を訂正する符号を作ります。一から設計し直すのではなく、bit-flip 符号を「基底を変えて」流用します。
3.1 狙うエラー:位相反転¶
エラーは の符号だけを反転させます(、)。1.4 で見たとおり、計算基底で測っても見えないエラーです。phase-flip 符号は、この単一の エラーを訂正することを目標にします。
3.2 鍵となる等式: が と を入れ替える¶
Hadamard ゲート は、次の関係を満たします。
つまり で挟むと、 エラーと エラーが入れ替わります。bit-flip 符号は エラーを訂正できました。各量子ビットを で基底変換すれば、その符号はそのまま エラーを訂正する符号になります。これが phase-flip 符号です。
3.3 符号空間¶
、 なので、bit-flip 符号の符号語 、 を3量子ビットすべての で変換すると、phase-flip 符号の論理状態が得られます。
bit-flip 符号で エラーが を入れ替えたのと同じように、phase-flip 符号では エラーが を入れ替えます()。
3.4 符号化回路¶
符号化は、bit-flip 符号の符号化に続けて、3量子ビットすべてに をかけるだけです。
@qmc.qkernel
def encode_3qubit_phaseflip(
q0: qmc.Qubit, q1: qmc.Qubit, q2: qmc.Qubit
) -> tuple[qmc.Qubit, qmc.Qubit, qmc.Qubit]:
# bit-flip 符号で符号化したあと、3量子ビットすべてを H で X 基底に移す。
q0, q1, q2 = encode_3qubit_bitflip(q0, q1, q2)
q0 = qmc.h(q0)
q1 = qmc.h(q1)
q2 = qmc.h(q2)
return q0, q1, q23.5 シンドローム測定: パリティ¶
bit-flip 符号では パリティ を測りました。phase-flip 符号では と が入れ替わっているので、 パリティ を測ります。
を測るには、補助量子ビットを に用意し( をかける)、それを制御として2つのデータ量子ビットへ CNOT し、再び をかけてから測定します。シンドロームとエラーの対応は bit-flip 符号と同じ形で、訂正が から に変わるだけです。
| エラー | シンドローム | 訂正 |
|---|---|---|
| なし | なし | |
3.6 実装と実行¶
ここまでを1つの @qkernel にまとめます。今回は論理 を用意します。
@qmc.qkernel
def phaseflip_syndrome_run(error_pos: qmc.UInt) -> qmc.Vector[qmc.Bit]:
# データ用に3量子ビット、シンドローム測定用に補助量子ビットを2つ確保する。
data = qmc.qubit_array(3, name="data")
anc = qmc.qubit_array(2, name="anc")
# 論理 |0_L> = |+++> に符号化する。
data[0], data[1], data[2] = encode_3qubit_phaseflip(data[0], data[1], data[2])
# error_pos で指定した位置に Z エラーを注入する(error_pos=3 ならエラーなし)。
for i in qmc.range(3):
if error_pos == i:
data[i] = qmc.z(data[i])
# シンドローム測定 1: X0 X1 パリティを anc[0] に取り出す(H で X 基底に変換)。
anc[0] = qmc.h(anc[0])
anc[0], data[0] = qmc.cx(anc[0], data[0])
anc[0], data[1] = qmc.cx(anc[0], data[1])
anc[0] = qmc.h(anc[0])
s0 = qmc.measure(anc[0])
# シンドローム測定 2: X0 X2 パリティを anc[1] に取り出す。
anc[1] = qmc.h(anc[1])
anc[1], data[0] = qmc.cx(anc[1], data[0])
anc[1], data[2] = qmc.cx(anc[1], data[2])
anc[1] = qmc.h(anc[1])
s1 = qmc.measure(anc[1])
# シンドローム (s0, s1) からエラー位置を特定し、同じ位置に Z をかけて訂正する。
if s0 & s1: # (1, 1) -> data[0]
data[0] = qmc.z(data[0])
if s0 & ~s1: # (1, 0) -> data[1]
data[1] = qmc.z(data[1])
if ~s0 & s1: # (0, 1) -> data[2]
data[2] = qmc.z(data[2])
# data[0] は |+> に戻っているので、H で |0> に直してから測定する。
data[0] = qmc.h(data[0])
return qmc.measure(data)訂正が終わると data[0] は に戻ります。 はそのまま測ると 0 と 1 が半々になるので、最後に data[0] へ を1つかけて に直してから測定します。訂正が正しく働けば、data[0] は常に 0 になるはずです。
phaseflip_cases = [
("no error", 3),
("Z on data[0]", 0),
("Z on data[1]", 1),
("Z on data[2]", 2),
]
if docs_test_mode:
phaseflip_cases = [
("no error", 3),
("Z on data[1]", 1),
]
print("3-qubit phase-flip code: logical |0_L> = |+++>")
for label, error_pos in phaseflip_cases:
counts = _sample_first_bit(
phaseflip_syndrome_run,
bindings={"error_pos": error_pos},
)
print(f" {label:14s}: data[0]=0 -> {counts[0]:3d}, data[0]=1 -> {counts[1]:3d}")
# 単一 Z 訂正が完全に効いた後、最後の H が data[0] を |0> に戻すので
# 各ショットで 0 が読まれる。
assert counts[1] == 0
assert counts[0] == counts[0] + counts[1]3-qubit phase-flip code: logical |0_L> = |+++>
no error : data[0]=0 -> 256, data[0]=1 -> 0
Z on data[0] : data[0]=0 -> 256, data[0]=1 -> 0
Z on data[1] : data[0]=0 -> 256, data[0]=1 -> 0
Z on data[2] : data[0]=0 -> 256, data[0]=1 -> 0
3.7 限界:片方のエラーしか直せない¶
phase-flip 符号は エラーを訂正できますが、今度は エラーを訂正できません。bit-flip 符号を基底変換したので、訂正できるエラーも入れ替わっただけです。
bit-flip 符号は だけ、phase-flip 符号は だけ ― どちらも片方のエラーしか訂正できません。しかし現実のノイズは も も、さらに両方が同時に起きる も引き起こします。次節では、この2つの符号を組み合わせて、任意の単一量子ビットエラーを訂正する Shor の9量子ビット符号 を作ります。
4. Shor の9量子ビット符号¶
bit-flip 符号と phase-flip 符号を組み合わせれば、 と の両方を訂正できそうです。それを実現するのが Shor の9量子ビット符号 です。
4.1 アイデア:2つの符号を入れ子にする¶
Shor 符号は、符号化を2段階で行います。
1量子ビットを phase-flip 符号で3量子ビットに符号化する。
その3量子ビットを、それぞれさらに bit-flip 符号で3量子ビットに符号化する。
こうして 1 → 3 → 9 量子ビットへと広がります。「符号の中にもう一段符号を入れる」この構成を 連接符号(concatenated code) と呼びます。外側の phase-flip 層が エラーを、内側の bit-flip 層が エラーを担当します。
4.2 9量子ビットを3ブロックで見る¶
9量子ビットを、3つの ブロック に分けて見ます。
(q0, q1, q2) (q3, q4, q5) (q6, q7, q8)各ブロックが内側の bit-flip 符号です。そして3ブロックの代表である が、外側の phase-flip 符号を構成します。
4.3 符号化回路¶
符号化は、外側の phase-flip 符号化を にかけてから、各ブロックを bit-flip 符号化するだけです。
@qmc.qkernel
def encode_shor(q: qmc.Vector[qmc.Qubit]) -> qmc.Vector[qmc.Qubit]:
# 外側: q[0], q[3], q[6] を phase-flip 符号で符号化する。
q[0], q[3], q[6] = encode_3qubit_phaseflip(q[0], q[3], q[6])
# 内側: 3つのブロックをそれぞれ bit-flip 符号で符号化する。
q[0], q[1], q[2] = encode_3qubit_bitflip(q[0], q[1], q[2])
q[3], q[4], q[5] = encode_3qubit_bitflip(q[3], q[4], q[5])
q[6], q[7], q[8] = encode_3qubit_bitflip(q[6], q[7], q[8])
return q4.4 シンドローム測定¶
Shor 符号のシンドロームは8ビットあり、2種類に分かれます。
ブロック内の パリティ(
anc[0]〜anc[5]、ブロックあたり2個):これは bit-flip 符号のシンドローム測定そのもので、各ブロックの中で エラーの位置を特定します。ブロックをまたぐ パリティ(
anc[6],anc[7]): と の2つです。これは phase-flip 符号のシンドローム測定にあたり、 エラーを含むブロックを特定します。
4.5 エラーはなぜ直るのか¶
Shor 符号は と だけでなく エラーも訂正できます。 なので、 エラーは 成分と 成分の両方を含んでいます。
ブロック内の パリティが 成分を、ブロック間の パリティが 成分を、それぞれ独立に検出します。 訂正と 訂正が両方とも当たることで、 エラーが打ち消されます。
4.6 実装と実行¶
ここまでを1つの @qkernel にまとめます。error_type は 1=X、2=Y、3=Z を表し、error_pos はエラーを入れる量子ビットの番号です。
訂正が終わっても、論理状態はまだ9量子ビットに符号化されたままです。この実演では論理ビットを q[0] から直接読めるように、最後に符号化の逆回路をかけます。この逆符号化は結果を確認するための手順で、訂正そのものはシンドローム測定とフィードバックで完了しています。
@qmc.qkernel
def shor_syndrome_run(
error_type: qmc.UInt,
error_pos: qmc.UInt,
theta: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
# データ用に9量子ビット、シンドローム測定用に補助量子ビットを8つ確保する。
q = qmc.qubit_array(9, name="q")
anc = qmc.qubit_array(8, name="anc")
# 論理状態を ry(theta) で用意し、9量子ビットに符号化する。
q[0] = qmc.ry(q[0], theta)
q = encode_shor(q)
# error_type / error_pos で指定した X / Y / Z エラーを注入する。
for i in qmc.range(9):
if (error_type == 1) & (error_pos == i): # 1: X エラー
q[i] = qmc.x(q[i])
if (error_type == 2) & (error_pos == i): # 2: Y エラー
q[i] = qmc.y(q[i])
if (error_type == 3) & (error_pos == i): # 3: Z エラー
q[i] = qmc.z(q[i])
# ブロック内 Z パリティ: ブロック b では anc[2b]=Z(3b,3b+1)、anc[2b+1]=Z(3b,3b+2)。
for b in qmc.range(3):
q[3 * b], anc[2 * b] = qmc.cx(q[3 * b], anc[2 * b])
q[3 * b + 1], anc[2 * b] = qmc.cx(q[3 * b + 1], anc[2 * b])
q[3 * b], anc[2 * b + 1] = qmc.cx(q[3 * b], anc[2 * b + 1])
q[3 * b + 2], anc[2 * b + 1] = qmc.cx(q[3 * b + 2], anc[2 * b + 1])
# ブロック間 X パリティ: anc[6] は q[0..5]、anc[7] は q[3..8] にかかる。
for p in qmc.range(2):
anc[6 + p] = qmc.h(anc[6 + p])
for i in qmc.range(6):
anc[6 + p], q[3 * p + i] = qmc.cx(anc[6 + p], q[3 * p + i])
anc[6 + p] = qmc.h(anc[6 + p])
# X 成分の訂正: ブロックごとに、シンドロームを測定して X エラー位置を特定し訂正する。
for b in qmc.range(3):
s0 = qmc.measure(anc[2 * b])
s1 = qmc.measure(anc[2 * b + 1])
if s0 & s1: # (1, 1) -> ブロック内 0 番目
q[3 * b] = qmc.x(q[3 * b])
if s0 & ~s1: # (1, 0) -> ブロック内 1 番目
q[3 * b + 1] = qmc.x(q[3 * b + 1])
if ~s0 & s1: # (0, 1) -> ブロック内 2 番目
q[3 * b + 2] = qmc.x(q[3 * b + 2])
# Z 成分の訂正: Z エラーを含むブロックを特定して代表量子ビットに Z をかける。
phase_s0 = qmc.measure(anc[6])
phase_s1 = qmc.measure(anc[7])
if phase_s0 & ~phase_s1:
q[0] = qmc.z(q[0])
if phase_s0 & phase_s1:
q[3] = qmc.z(q[3])
if ~phase_s0 & phase_s1:
q[6] = qmc.z(q[6])
# 検証用: 符号化の逆回路をかけ、論理ビットを q[0] に集める。
for b in qmc.range(3):
q[3 * b], q[3 * b + 1] = qmc.cx(q[3 * b], q[3 * b + 1])
q[3 * b], q[3 * b + 2] = qmc.cx(q[3 * b], q[3 * b + 2])
q[3 * b] = qmc.h(q[3 * b])
q[0], q[3] = qmc.cx(q[0], q[3])
q[0], q[6] = qmc.cx(q[0], q[6])
return qmc.measure(q)各ブロックから代表として1つずつ( をブロック0、 をブロック1、 をブロック2)エラーを入れて試します。論理 が保たれていれば、q[0] は常に 1 になるはずです。
shor_cases = [
("X", 1, 0),
("Y", 2, 4),
("Z", 3, 8),
]
if docs_test_mode:
shor_cases = [
("Y", 2, 4),
]
print("Shor 9-qubit code: logical |1>")
print(f" {'error':6s} | {'pos':5s} | P(q[0]=1)")
print(f" {'-' * 6}-+-{'-' * 5}-+-{'-' * 9}")
for name, error_type, error_pos in shor_cases:
counts = _sample_first_bit(
shor_syndrome_run,
bindings={"error_type": error_type, "error_pos": error_pos},
parameters=["theta"],
runtime_bindings={"theta": math.pi},
)
total = counts[0] + counts[1]
print(f" {name:6s} | q[{error_pos}] | {counts[1] / total:.3f}")
# 純粋な |1> 入力は Shor 符号の下で単一 Pauli エラーを完全に乗り越え、
# q[0] は各ショットで 1 を返す。
assert counts[0] == 0
assert counts[1] == totalShor 9-qubit code: logical |1>
error | pos | P(q[0]=1)
-------+-------+----------
X | q[0] | 1.000
Y | q[4] | 1.000
Z | q[8] | 1.000
4.7 なぜ「任意の単一エラー」を訂正できるのか¶
Shor 符号は , , の3つを訂正できます。これがそのまま「任意の単一量子ビットエラーを訂正できる」ことを意味します。
1.4 で見たように、どんなエラーも行列としては の線形結合で書け、シンドローム測定がその重ね合わせを1つの離散的な Pauli エラーに収縮させます。収縮後に残るのは , , のいずれか(またはエラーなし)だけで、Shor 符号はそのすべてを訂正できます。だから連続的なものも含めた任意の単一量子ビットエラーに対応できるのです。
形式的な扱いは後編の スタビライザ形式論と Steane 符号 で改めて整理します。
5. ここまでの共通パターン¶
3つの符号 ― bit-flip、phase-flip、Shor ― を作ってきました。実はどれも、同じ4段階の型に従っています。
符号空間への符号化: 1つの論理量子ビットを、もつれを使って複数の物理量子ビットに広げる。
パリティ測定: データを直接測らず、複数量子ビットのパリティ( や )を補助量子ビットに取り出す。
シンドローム: パリティの測定結果から、エラーの位置と種類を読み取る。
フィードバック訂正: シンドロームに応じて Pauli ゲートを当て、エラーを打ち消す。
符号が変わっても、この骨組みは変わりません。違うのは「どのパリティを測るか」だけです。
5.1 パリティ演算子の名前:スタビライザ¶
ここまで測ってきたパリティ演算子 ― や など ― には名前があります。スタビライザ(stabilizer) です。
スタビライザは、符号空間のすべての状態を変えない Pauli 演算子です。正しい符号語に作用させても状態はそのまま(固有値 +1)。エラーが入ると、そのエラーと反交換するスタビライザの測定値が -1 に変わり、それがシンドロームのビットになります。
bit-flip 符号の も、phase-flip 符号の も、Shor 符号の8つのパリティも、すべてスタビライザです。3つの符号は「スタビライザを測ってシンドロームを得る」という1つの考え方の、異なる現れ方だったわけです。この見方を スタビライザ形式論 と呼び、後編で本格的に扱います。
5.2 3つの符号のまとめ¶
3つの符号を、スタビライザと合わせてまとめます。
| 符号 | スタビライザ生成子 | 訂正できるエラー | |
|---|---|---|---|
| 3量子ビット bit-flip | 単一の | ||
| 3量子ビット phase-flip | 単一の | ||
| Shor 9量子ビット | 単一の |
は符号を表す記法です。 は物理量子ビット数、 は守る論理量子ビット数、 は 符号距離 を表します。 は「任意の単一量子ビットエラーを訂正するには が必要」という指標で、3量子ビット符号は ― 特定の種類( または )のエラーしか直せません。Shor 符号は で、任意の単一エラーを直せます。距離の正確な定義は後編で扱います。
6. やってみよう¶
理解を試すために、コードを少し変えて動かしてみましょう。
エラー位置を変える: 各
*_syndrome_runのerror_posをいろいろな値にして、訂正が効くことを確かめる。わざと失敗させる: 以下で、bit-flip 符号の限界を体感します。
わざと失敗させる¶
bit-flip 符号が訂正できるのは、単一の エラーまでです。2か所に エラーが入るとどうなるでしょうか。
次のカーネルは、論理 ()に data[0] と data[1] の2か所の エラーを入れ、いつもどおりシンドローム測定と訂正を行います。
@qmc.qkernel
def bitflip_two_errors(theta: qmc.Float) -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(3, name="data")
anc = qmc.qubit_array(2, name="anc")
data[0] = qmc.ry(data[0], theta)
data[0], data[1], data[2] = encode_3qubit_bitflip(data[0], data[1], data[2])
# 2か所に X エラーを注入する(単一エラー訂正の能力を超える)。
data[0] = qmc.x(data[0])
data[1] = qmc.x(data[1])
# シンドローム測定と訂正は bitflip_syndrome_run と同じ。
data[0], anc[0] = qmc.cx(data[0], anc[0])
data[1], anc[0] = qmc.cx(data[1], anc[0])
s0 = qmc.measure(anc[0])
data[0], anc[1] = qmc.cx(data[0], anc[1])
data[2], anc[1] = qmc.cx(data[2], anc[1])
s1 = qmc.measure(anc[1])
if s0 & s1:
data[0] = qmc.x(data[0])
if s0 & ~s1:
data[1] = qmc.x(data[1])
if ~s0 & s1:
data[2] = qmc.x(data[2])
return qmc.measure(data)print("bit-flip code with TWO X errors (logical |1>)")
counts = _sample_first_bit(
bitflip_two_errors,
parameters=["theta"],
runtime_bindings={"theta": math.pi},
)
print(f" data[0]=0 -> {counts[0]:3d}, data[0]=1 -> {counts[1]:3d}")
# 失敗モード: 2 か所の X エラーはシンドロームを単一 X エラーと取り違えさせ、
# 「訂正」が論理 |1> を論理 |0> に裏返してしまう。各ショットで data[0] = 0 が
# 決定的に読まれる — これが d=1 の意味。
assert counts[1] == 0
assert counts[0] == counts[0] + counts[1]bit-flip code with TWO X errors (logical |1>)
data[0]=0 -> 256, data[0]=1 -> 0
data[0] は 1 ではなく 0 になります。訂正が状態を直すどころか、論理 を論理 に変えてしまいました。
2か所に が入った では、シンドロームは から「1か所だけ違う」ように見え、訂正が残り1か所にも をかけて にしてしまうためです。これが「単一エラーまで」という限界 ― 5.2 の表で bit-flip 符号が だったことの実際の意味です。
7. まとめ¶
この記事では、3つの量子誤り訂正符号を実装しました。
3量子ビット bit-flip 符号 ― パリティで単一の エラーを訂正する。
3量子ビット phase-flip 符号 ― パリティで単一の エラーを訂正する。
Shor の9量子ビット符号 ― 2つを連接し、任意の単一量子ビットエラー()を訂正する。
共通する骨組みは「符号空間への符号化 → パリティ(スタビライザ)測定 → シンドローム → フィードバック訂正」でした。
次へ¶
後編 スタビライザ形式論と Steane 符号 では、ここで名前だけ与えたスタビライザを形式的に扱います。古典符号から系統的に量子符号を作る CSS 構成 と、 を Shor 符号の9量子ビットより少ない7量子ビットで実現する Steane 符号 が主役です。