タグ: algorithm error-correction
量子誤り訂正入門(前編)では、3量子ビット bit-flip / phase-flip 符号と Shor の9量子ビット符号を作り、最後に「パリティ演算子=スタビライザ」という名前を与えました。
後編では、このスタビライザを形式的に扱います。そして古典の Hamming 符号から系統的に量子符号を作る CSS 構成 と、その代表例 Steane 符号 を実装します。Steane 符号は、Shor 符号と同じ を9量子ビットより少ない7量子ビットで実現します。
この記事で扱うこと:
スタビライザ形式論 ― スタビライザ、生成子、シンドロームを正式に整理する。
CSS 構成 ― 古典 Hamming 符号から Steane 符号のスタビライザを作る。
Steane 符号の符号化・シンドローム測定・訂正を実装する。
7つの物理 Hadamard が論理 Hadamard になることを確かめる。
前提知識: 前編の内容(シンドローム測定、Pauli エラー、スタビライザという用語)。
# 最新のQamomileをpipからインストールします。
# !pip install qamomile
# # or
# !uv add qamomile1. スタビライザ形式論¶
前編では、 や のようなパリティ演算子を スタビライザ と呼びました。まずこの言葉を正式に整理します。
1.1 スタビライザとは¶
スタビライザ とは、符号空間のすべての状態を変えない Pauli 演算子 のことです。任意の符号語 に対して
が成り立ちます。「符号空間とは、スタビライザの固有値 +1 の固有空間である」と言い換えられます。
たとえば bit-flip 符号の符号語は と です。 をかけると、、 となり、どちらも変わりません。だから は bit-flip 符号のスタビライザです。
1.2 生成子とシンドローム¶
1つの符号にはたくさんのスタビライザがありますが、すべてを並べる必要はありません。いくつかの 生成子(generator) を選べば、残りはその積で得られます。bit-flip 符号なら と の2つが生成子です。
スタビライザ生成子を測定すると、結果は固有値 +1 か -1 になります。
エラーがなければ、状態は符号空間にあり、すべての生成子が +1 を返します。
エラー が入ると、 と 反交換する 生成子の測定値が -1 に変わります。
この (ビットで表せば )のパターンが シンドローム です。前編で補助量子ビットに取り出していた値は、まさにスタビライザ生成子の測定結果でした。どの生成子が -1 になったかを見れば、エラーの位置と種類が分かります。
1.3 前編の符号をスタビライザで見る¶
前編の3つの符号は、すべてスタビライザ生成子で記述できます。
| 符号 | スタビライザ生成子 |
|---|---|
| 3量子ビット bit-flip | |
| 3量子ビット phase-flip | |
| Shor 9量子ビット |
bit-flip 符号は 型の生成子だけ、phase-flip 符号は 型の生成子だけを持ちます。Shor 符号は両方を持ち、 型が エラーを、 型が エラーを検出します。この「 型と 型を別々に持つ」構造が、次節の CSS 構成につながります。
1.4 と符号距離¶
スタビライザ符号は という3つの数で特徴づけられます。
:物理量子ビット数。
:守る論理量子ビット数。生成子1つにつき自由度が1つ減るので、 です。
:符号距離。スタビライザすべてと交換するが、それ自身はスタビライザでない Pauli 演算子 ― これを 論理演算子 と呼びます ― のうち、最小の重み(かかる量子ビット数)です。
距離 の符号は 個までのエラーを訂正できます。任意の単一量子ビットエラーを訂正するには が必要です。前編の3量子ビット符号は 、Shor 符号は でした。これから作る Steane 符号は です。
2. 古典 Hamming 符号から CSS 符号へ¶
CSS 符号(Calderbank-Shor-Steane 符号) は、古典の誤り訂正符号から量子符号を系統的に作る方法です。Steane 符号はその代表例で、古典の Hamming 符号をもとにします。
2.1 古典 Hamming [7,4,3] 符号¶
古典の Hamming [7,4,3] 符号は、7ビットで4ビットの情報を守り、1ビットの誤りを訂正できる古典符号です。この符号は パリティ検査行列 で定義されます。
7ビットの語 が符号語であることは、(各行とのパリティがすべて偶数)と同値です。
2.2 列が誤り位置を指す仕掛け¶
の列をよく見ると、第 列は数 の2進表現になっています(第0列は 001、第1列は 010、…、第6列は 111)。
1ビットの誤りが位置 に入ると、 はちょうど第 列に等しくなります。つまり の3ビットを2進数として読めば、それがそのまま誤りの位置を指します。古典 Hamming 符号は、この仕掛けで誤り位置を一発で特定します。
2.3 CSS 構成: 型と 型のスタビライザ¶
CSS 構成は、このパリティ検査行列 から2種類のスタビライザを作ります。
型スタビライザ: の各行を、1 の立つ位置に を置いた演算子に読み替える。 エラーを検出する。
型スタビライザ:同じ各行を、 を置いた演算子に読み替える。 エラーを検出する。
エラーは 型スタビライザと反交換し、 エラーは 型スタビライザと反交換します。だから 型で エラーの位置を、 型で エラーの位置を、それぞれ古典 Hamming 符号と同じやり方で特定できます。前編の Shor 符号で見た「 と を独立に直す」が、ここでは古典符号から自動的に出てきます。
2.4 Steane 符号の6つの生成子¶
Hamming 行列 の3つの行から、 型・ 型それぞれ3つ、合わせて6つのスタビライザ生成子が得られます。
| 型 | スタビライザ | 検出するエラー |
|---|---|---|
| 型 | ||
| 型 | ||
| 型 | ||
| 型 | ||
| 型 | ||
| 型 |
物理量子ビット7個、生成子6個なので、守れる論理量子ビットは 個。これが Steane 符号です。
実装に入る前に、QamomileとQiskit連携を読み込み、補助関数を用意します。_bits7 / _passes_hamming_checks / _is_steane_zero_wordは、測定結果がHamming符号語やの符号語かを判定するユーティリティです。QECの本筋ではないので、読み飛ばして構いません。
import qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler
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 _bits7(outcome) -> list[int]:
"""測定結果を、量子ビット番号順の7ビットのリストで返す。"""
if isinstance(outcome, (list, tuple)):
return list(outcome)
return [(outcome >> i) & 1 for i in range(7)]
def _passes_hamming_checks(outcome) -> bool:
"""測定結果が Hamming [7,4,3] 符号語(3つのパリティ検査をすべて満たす)かを返す。"""
bits = _bits7(outcome)
h_checks = [
bits[3] ^ bits[4] ^ bits[5] ^ bits[6],
bits[1] ^ bits[2] ^ bits[5] ^ bits[6],
bits[0] ^ bits[2] ^ bits[4] ^ bits[6],
]
return all(check == 0 for check in h_checks)
def _is_steane_zero_word(outcome) -> bool:
"""測定結果が |0_L> の符号語(偶数重みの Hamming 符号語)かを返す。"""
return _passes_hamming_checks(outcome) and sum(_bits7(outcome)) % 2 == 03. 論理 の符号化¶
3.1 論理ゼロは偶数重み Hamming 符号語の重ね合わせ¶
Steane 符号の論理 は、重みが偶数の Hamming 符号語をすべて足し合わせた重ね合わせです。
ここで は Hamming 符号語の集合、 はその重みです。偶数重みの Hamming 符号語は8個あるので、8項の重ね合わせになります。
3.2 符号化回路¶
次の回路は、 から を作ります。3つの 型スタビライザのパターン(、、)を、Hadamard と CNOT で順に書き込みます。
@qmc.qkernel
def encode_steane_zero(data: qmc.Vector[qmc.Qubit]) -> qmc.Vector[qmc.Qubit]:
# X3 X4 X5 X6 のパターンを書き込む。
data[3] = qmc.h(data[3])
data[3], data[4] = qmc.cx(data[3], data[4])
data[3], data[5] = qmc.cx(data[3], data[5])
data[3], data[6] = qmc.cx(data[3], data[6])
# X1 X2 X5 X6 のパターンを書き込む。
data[1] = qmc.h(data[1])
data[1], data[2] = qmc.cx(data[1], data[2])
data[1], data[5] = qmc.cx(data[1], data[5])
data[1], data[6] = qmc.cx(data[1], data[6])
# X0 X2 X4 X6 のパターンを書き込む。
data[0] = qmc.h(data[0])
data[0], data[2] = qmc.cx(data[0], data[2])
data[0], data[4] = qmc.cx(data[0], data[4])
data[0], data[6] = qmc.cx(data[0], data[6])
return data@qmc.qkernel
def encode_zero_and_measure() -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
data = encode_steane_zero(data)
return qmc.measure(data)3.3 符号化の確認¶
符号化器の出力を測定し、観測されるビット列がすべて偶数重みの Hamming 符号語( の符号語)になっているかを確かめます。
print("Encode and measure |0_L>")
exe = transpiler.transpile(encode_zero_and_measure)
result = exe.sample(_seeded_executor, shots=1024).result()
total = sum(count for _, count in result.results)
valid = sum(count for outcome, count in result.results if _is_steane_zero_word(outcome))
print(f" |0_L> codeword ratio: {valid / total:.3f}")
print(f" distinct codewords observed: {len(result.results)}")
# Steane の |0_L> はちょうど 8 つの偶重み Hamming 符号語の均等重ね合わせなので、
# 各ショットは有効な |0_L> 符号語を返し、分布は高々その 8 通りに収まる。
assert total == 1024
assert valid == total
assert len(result.results) <= 8Encode and measure |0_L>
|0_L> codeword ratio: 1.000
distinct codewords observed: 8
4. シンドローム測定と訂正¶
4.1 と を独立にデコードする¶
CSS 符号の利点は、 エラーと エラーを完全に独立に扱えることです。
型スタビライザを測ると、 エラーのシンドロームが得られる。
型スタビライザを測ると、 エラーのシンドロームが得られる。
それぞれ3ビットのシンドロームで、2.2 で見た Hamming 行列の列の仕掛けがそのまま使えます。
4.2 シンドローム表¶
3ビットのシンドローム は、エラーの位置をそのまま2進数で表します。
| エラー位置 | シンドローム |
|---|---|
| なし | |
型スタビライザから得た は エラーの位置を、 型から得たものは エラーの位置を指します。
4.3 実装¶
error_type は 1=X、2=Y、3=Z、error_pos はエラーを入れる量子ビット番号 0..6 です。
@qmc.qkernel
def steane_run(
error_type: qmc.UInt,
error_pos: qmc.UInt,
) -> qmc.Vector[qmc.Bit]:
# データ用に7量子ビット、シンドローム測定用に補助量子ビットを6つ確保する。
data = qmc.qubit_array(7, name="data")
anc = qmc.qubit_array(6, name="anc")
# |0_L> に符号化する。
data = encode_steane_zero(data)
# error_type / error_pos で指定した X / Y / Z エラーを注入する。
for i in qmc.range(7):
if (error_type == 1) & (error_pos == i): # 1: X エラー
data[i] = qmc.x(data[i])
if (error_type == 2) & (error_pos == i): # 2: Y エラー
data[i] = qmc.y(data[i])
if (error_type == 3) & (error_pos == i): # 3: Z エラー
data[i] = qmc.z(data[i])
# Z 型スタビライザ: X エラーのシンドローム (sx_2, sx_1, sx_0) を測る。
data[3], anc[0] = qmc.cx(data[3], anc[0])
data[4], anc[0] = qmc.cx(data[4], anc[0])
data[5], anc[0] = qmc.cx(data[5], anc[0])
data[6], anc[0] = qmc.cx(data[6], anc[0])
sx_2 = qmc.measure(anc[0])
data[1], anc[1] = qmc.cx(data[1], anc[1])
data[2], anc[1] = qmc.cx(data[2], anc[1])
data[5], anc[1] = qmc.cx(data[5], anc[1])
data[6], anc[1] = qmc.cx(data[6], anc[1])
sx_1 = qmc.measure(anc[1])
data[0], anc[2] = qmc.cx(data[0], anc[2])
data[2], anc[2] = qmc.cx(data[2], anc[2])
data[4], anc[2] = qmc.cx(data[4], anc[2])
data[6], anc[2] = qmc.cx(data[6], anc[2])
sx_0 = qmc.measure(anc[2])
# X 型スタビライザ: Z エラーのシンドローム (sz_2, sz_1, sz_0) を測る。
anc[3] = qmc.h(anc[3])
anc[3], data[3] = qmc.cx(anc[3], data[3])
anc[3], data[4] = qmc.cx(anc[3], data[4])
anc[3], data[5] = qmc.cx(anc[3], data[5])
anc[3], data[6] = qmc.cx(anc[3], data[6])
anc[3] = qmc.h(anc[3])
sz_2 = qmc.measure(anc[3])
anc[4] = qmc.h(anc[4])
anc[4], data[1] = qmc.cx(anc[4], data[1])
anc[4], data[2] = qmc.cx(anc[4], data[2])
anc[4], data[5] = qmc.cx(anc[4], data[5])
anc[4], data[6] = qmc.cx(anc[4], data[6])
anc[4] = qmc.h(anc[4])
sz_1 = qmc.measure(anc[4])
anc[5] = qmc.h(anc[5])
anc[5], data[0] = qmc.cx(anc[5], data[0])
anc[5], data[2] = qmc.cx(anc[5], data[2])
anc[5], data[4] = qmc.cx(anc[5], data[4])
anc[5], data[6] = qmc.cx(anc[5], data[6])
anc[5] = qmc.h(anc[5])
sz_0 = qmc.measure(anc[5])
# X 成分の訂正: シンドローム (sx_2, sx_1, sx_0) が指す位置に X をかける。
if (~sx_2) & (~sx_1) & sx_0:
data[0] = qmc.x(data[0])
if (~sx_2) & sx_1 & (~sx_0):
data[1] = qmc.x(data[1])
if (~sx_2) & sx_1 & sx_0:
data[2] = qmc.x(data[2])
if sx_2 & (~sx_1) & (~sx_0):
data[3] = qmc.x(data[3])
if sx_2 & (~sx_1) & sx_0:
data[4] = qmc.x(data[4])
if sx_2 & sx_1 & (~sx_0):
data[5] = qmc.x(data[5])
if sx_2 & sx_1 & sx_0:
data[6] = qmc.x(data[6])
# Z 成分の訂正: シンドローム (sz_2, sz_1, sz_0) が指す位置に Z をかける。
if (~sz_2) & (~sz_1) & sz_0:
data[0] = qmc.z(data[0])
if (~sz_2) & sz_1 & (~sz_0):
data[1] = qmc.z(data[1])
if (~sz_2) & sz_1 & sz_0:
data[2] = qmc.z(data[2])
if sz_2 & (~sz_1) & (~sz_0):
data[3] = qmc.z(data[3])
if sz_2 & (~sz_1) & sz_0:
data[4] = qmc.z(data[4])
if sz_2 & sz_1 & (~sz_0):
data[5] = qmc.z(data[5])
if sz_2 & sz_1 & sz_0:
data[6] = qmc.z(data[6])
return qmc.measure(data)4.4 検証:21通りの単一エラー¶
、、 を7つの量子ビットそれぞれに入れた、計21通りの単一エラーを試します。訂正後、測定されるビット列はすべて の符号語に戻るはずです。
print("Steane code: correct X/Y/Z on all 7 locations")
print(f" {'err':4s} | {'pos':5s} | |0_L> codeword")
print(f" {'-' * 4}-+-{'-' * 5}-+-{'-' * 14}")
for name, error_type in [("X", 1), ("Y", 2), ("Z", 3)]:
for pos in range(7):
exe = transpiler.transpile(
steane_run,
bindings={"error_type": error_type, "error_pos": pos},
)
result = exe.sample(_seeded_executor, shots=128).result()
total = sum(count for _, count in result.results)
valid = sum(
count for outcome, count in result.results if _is_steane_zero_word(outcome)
)
print(f" {name:4s} | q[{pos}] | {valid / total:.3f}")
# Steane は 7 量子ビット上の任意の単一 Pauli エラーを訂正するので、
# 訂正後の状態は完全に |0_L> 符号空間に収まり、各ショットは有効な
# 符号語を返す。
assert total == 128
assert valid == totalSteane code: correct X/Y/Z on all 7 locations
err | pos | |0_L> codeword
-----+-------+---------------
X | q[0] | 1.000
X | q[1] | 1.000
X | q[2] | 1.000
X | q[3] | 1.000
X | q[4] | 1.000
X | q[5] | 1.000
X | q[6] | 1.000
Y | q[0] | 1.000
Y | q[1] | 1.000
Y | q[2] | 1.000
Y | q[3] | 1.000
Y | q[4] | 1.000
Y | q[5] | 1.000
Y | q[6] | 1.000
Z | q[0] | 1.000
Z | q[1] | 1.000
Z | q[2] | 1.000
Z | q[3] | 1.000
Z | q[4] | 1.000
Z | q[5] | 1.000
Z | q[6] | 1.000
比率が 1.000 なら、その単一 Pauli エラーに対して状態が の符号空間に戻ったことを意味します。 のエラーは 成分と 成分の訂正の両方を引き起こしますが、CSS 符号ではこの2つが独立なので、そのまま訂正できます。
5. Transversal な Hadamard ゲート¶
5.1 transversal ゲートとは¶
論理量子ビットに論理ゲートをかけるとき、各物理量子ビットに それぞれ独立に 物理ゲートをかけるだけで済む場合、そのゲートを transversal(横断的) と言います。
transversal なゲートはフォールトトレラント量子計算で重要です。物理ゲートが互いに独立なので、1つの物理ゲートが故障しても、その誤りが1ブロック内の複数の量子ビットに広がりません。
Steane 符号の大きな特徴は、論理 Hadamard が transversal なことです。7つの物理量子ビットそれぞれに をかけるだけで、論理 Hadamard になります。
これは、Steane 符号の 型と 型のスタビライザが同じ Hamming パターンを持つ ― CSS 構成で同じ行列 を使った ― ことの帰結です。
5.2 論理 Hadamard を確かめる¶
であることを、2つの性質で確かめます。
性質1: は を に移す。 は と の重ね合わせです。 が偶数重みの Hamming 符号語からなるのに対し、 は奇数重みの Hamming 符号語からなります。したがって を測ると、偶数・奇数いずれの重みの Hamming 符号語も現れます( なら偶数重みのみ)。
@qmc.qkernel
def transversal_h_to_plus() -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
data = encode_steane_zero(data)
# transversal Hadamard を1回かける: |0_L> -> |+_L>。
data = qmc.h(data)
return qmc.measure(data)print("Transversal H: |0_L> -> |+_L>")
exe = transpiler.transpile(transversal_h_to_plus)
result = exe.sample(_seeded_executor, shots=1024).result()
total = sum(count for _, count in result.results)
hamming = sum(
count for outcome, count in result.results if _passes_hamming_checks(outcome)
)
odd = sum(count for outcome, count in result.results if sum(_bits7(outcome)) % 2 == 1)
print(f" Hamming codeword ratio: {hamming / total:.3f}")
print(f" odd-weight (|1_L>) fraction: {odd / total:.3f}")
# |+_L> は 16 個の Hamming 符号語の均等重ね合わせ。各ショットは何らかの
# Hamming 符号語を返し、奇重み (|1_L>) 半分は約半数 — 1024 ショットの
# 標準誤差 < 0.02 に対して 5% の窓は安全側。
assert total == 1024
assert hamming == total
assert len(result.results) <= 16
assert abs(odd / total - 0.5) < 0.05Transversal H: |0_L> -> |+_L>
Hamming codeword ratio: 1.000
odd-weight (|1_L>) fraction: 0.518
Hamming 符号語の比率は 1.000 で、奇数重みの符号語も約半分現れます。偶数重みしか出ない とは異なる ― 状態が に移っていることが確認できます。
性質2: を2回かけると恒等変換に戻る()。 に を2回かけると、 に戻るはずです。
@qmc.qkernel
def transversal_h_round_trip() -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
data = encode_steane_zero(data)
# transversal Hadamard を2回かける: H^2 = I なので |0_L> に戻る。
data = qmc.h(data)
data = qmc.h(data)
return qmc.measure(data)print("Transversal H round trip: |0_L> -> H -> H -> |0_L>")
exe = transpiler.transpile(transversal_h_round_trip)
result = exe.sample(_seeded_executor, shots=1024).result()
total = sum(count for _, count in result.results)
valid = sum(count for outcome, count in result.results if _is_steane_zero_word(outcome))
print(f" |0_L> codeword ratio: {valid / total:.3f}")
# H^2 = I なので往復で |0_L> がそのまま復元される — 各ショットは |0_L> 符号語。
assert total == 1024
assert valid == totalTransversal H round trip: |0_L> -> H -> H -> |0_L>
|0_L> codeword ratio: 1.000
比率は 1.000 ― 2回の transversal Hadamard で に戻りました。7つの物理 が、確かに論理 として働いています。
6. まとめ¶
この記事では、スタビライザ形式論と Steane 符号を扱いました。
スタビライザ形式論 ― 符号空間を Pauli 演算子(スタビライザ)の +1 固有空間として捉え、生成子の測定値としてシンドロームを定義した。
CSS 構成 ― 古典 Hamming [7,4,3] 符号のパリティ検査行列から、 型と 型のスタビライザを作った。
Steane 符号 ― の符号化、6つのスタビライザによるシンドローム測定、21通りの単一 Pauli エラーの訂正を実装した。
transversal な Hadamard ― 7つの物理 が論理 になることを確かめた。
Steane 符号は、Shor 符号と同じ を9量子ビットより少ない7量子ビットで実現し、CSS 構成と transversal な Clifford ゲートのきれいな例になっています。
その先¶
表面符号(surface code) ― スタビライザを2次元格子上の局所的な演算子にした符号。現在の超伝導量子コンピュータでの誤り訂正の主役で、シンドローム測定を繰り返し行う点が新しい。
フォールトトレラント量子計算 ― 符号化したまま論理ゲートを実行し、誤りを増幅させずに計算を進める枠組み。transversal なゲートはその出発点です。