時空間符号を用いた低オーバーヘッドのエラー検出
所要時間の目安:Heronプロセッサ(ibm_kingston または同等のもの)で4分(注:これはあくまで目安です。 (実行時間は環境によって異なる場合があります。)
学習成果
このチュートリアルを修了すると、以下の内容を理解できるようになります:
- 時空パウリチェックがクリフォード回路の論理エラーをどのように検出するか、また、そのシンドロームを用いたポストセレクションが、サンプリングされた分布の忠実度をどのように向上させるか。
- パッケージ
qiskit-pauliceを使用して、get_check_qubits、NoiseModel、およびを用いて、ハードウェア効率の高いチェックを自動的に検出して挿入する方法add_pauli_checks。 - 安定化状態の安定化子状態をサンプリングし、チェックシンドロームを用いてポストセレクションを行うことで、その忠実度を推定する方法。
- IBM Quantum® ハードウェア上でエラー検出ワークフロー全体を実行し、ノイズを含むフィデリティとポストセレクション後のフィデリティを比較する方法。
前提条件
以下のトピックについて、あらかじめ理解しておくことをお勧めします:
- ユーティリティ規模の量子コンピューティングにおけるハードウェアの基礎。
- クリフォード形式とスタビライザー形式。これには、スタビライザー群が純粋なスタビライザー状態をどのように記述するかも含まれる。
背景
Simon Martiel および Ali Javadi-Abhari による「 時空間符号を用いた低オーバーヘッドのエラー検出」 [1] では、 完全なエラー訂正と軽量なエラー軽減の中間に位置する、クリフォード支配回路における論理エラーを検出する手法が提案されている。 このアイデアは、van den Bergらによる「 コヒーレント・パウリ・チェックによるシングルショット誤差の低減」 [2] で提案されたコヒーレント・パウリ・チェック(CPC)に基づいている。 どちらのアプローチにおいても、クリフォード「ペイロード」回路がアンシラ量子ビットと量子もつれ状態を形成し、特定の不変量を検証する。 アンシラの測定により、実行中にエラーが検出されたかどうかを示すシンドロームが生成されます。 エラーが検出されなかったサンプルのみを保持することで、サンプリングされた分布の忠実度は向上するが、その代償としてポストセレクション率は低下する。
コヒーレント・パウリチェックと時空間チェックの主な違いは、測定の対象となる演算子にある。 コヒーレント・パウリチェックは、時間局在的で重みが大きい演算子を測定する。 「ヘビーヘックス」のような接続性が制限された量子ビットトポロジーでは、こうしたチェックには多くのSWAPゲートが必要となり、その結果、回路が深くなりすぎて、実際には実行できなくなることがよくあります。 その代わりに、チェックを時空間コードとして実装することで、各チェックがペイロード回路全体に空間的・時間的に分散されることになる。 これにより、量子ビットや処理深度のオーバーヘッドを低く抑えつつ、論理エラーの検出において高い効果を維持できる、ハードウェア効率に優れた符号化が実現される。
このパッケージ qiskit-paulice の機能
このパッケージ qiskit-paulice は、これらのチェックの構築を自動化するため、手作業で作成する必要がなくなります。 その主な役割は、回路内の各箇所において、エラー検出率を最大化し、かつ量子ビットのオーバーヘッドを最小化するような、有効な時空間パウリチェックを見つけ出し、挿入することです。 チェックは、その演算子がペイロード回路の論理動作を変更しない場合に有効であり、エンタングルメントゲートの使用数が少ない場合は低負荷であり、チェック自体がもたらすノイズに比べてエラーの大部分を検出できる場合は効果的である。 このパッケージは、ノイズモデルに基づいて候補チェックを評価し、最適なものを回路に組み込みます。 このチュートリアルでは、次の3つのAPIメソッドを使用します:
get_check_qubitsバックエンドの結合マップを検査し、ターゲット量子ビットとアンシラ量子ビットのペアを返します。 の確認には を使用target_qubits[i]しますancilla_qubits[i]。NoiseModel.from_backendバックエンドのベンチマークデータから、大まかなノイズモデルを構築します。 このモデルは候補のチェックを評価するため、学習済みの正確なノイズモデルは必要ありません。 学習済みパウリ・リンドブラッド・モデルについては、を参照のことNoiseModel.from_pauli_lindblad_maps。add_pauli_checks回路内のチェック箇所を検出し、チェックを挿入します。 これは、チェックの数が増加していくCheckedCircuitオブジェクトのシーケンスを返し、各オブジェクトは、測定されたビット列をシンドロームベクトルに写像するget_postselection_method関数を提供します。 この引cost数は、チェックのスコアを算出する関数(gamma、事後選択された逆ノイズチャネルのサンプリングオーバーヘッド、またはLER、論理誤り率)を選択します。 この引数methodにより、検索戦略(windowed、genetic、またはwindowed_genetic)が選択されます。 このチュートリアルでは とcost="gamma"を使用しておりmethod="windowed"、これらを組み合わせることで、決定論的かつ再現性のあるチェックの選択が可能になります。
スタビライザーサンプリングから忠実度を推定する
エラー検出がどの程度機能しているかを測定するには、回路が理想的に生成するスタビライザー状態 と、ハードウェアが実際に出力するノイズの混入した状態 とを比較して、その忠実度を推定することができます。 純粋な安定子状態 への射影は、その安定子群 の 個の要素に対する一様平均に等しい:
これをフィデリティに代入すると、フィデリティは となり、これは に関するすべての安定化子 の平均期待値として表される:
より大規模な問題の場合、 のすべての安定化子列を列挙することは現実的ではないため、ランダムな標本からフィデリティを推定することができます。 から の安定化因子 を均一にランダムに抽出すると、不偏推定量が得られる:
クリフォード回路はスタビライザー状態を生成するため、そのスタビライザーのサンプリングされた期待値から、そのフィデリティを直接推定することができます。 このチュートリアルでは、まずシミュレータ上で簡単な回路を用いてワークフローの手順を解説し、その後、より大規模で複雑な回路を用いてハードウェア上で同じワークフローを実行します。 回路にクリフォード演算以外の演算が増えるにつれて、有効なチェックの数は急速に減少するため、この手法はクリフォード演算が支配的な回路において最も効果を発揮する。
要件
このチュートリアルを始める前に、以下のものがインストールされていることを確認してください:
- Qiskit SDK v2.0 またはそれ以降、 可視化機能をサポートしたもの
- Qiskit Runtime v0.40 またはそれ以降 (
pip install qiskit-ibm-runtime) - Qiskit Aer v0.17 以降 (
pip install qiskit-aer) - Qiskit Paulice (
pip install qiskit-paulice) - tqdm (
pip install tqdm)
セットアップ
必要なライブラリをインポートし、インポートでは利用できないヘルパー関数を定義します。 この関 random_clifford_circuit 数は、ブリックワーク方式によるランダムなクリフォードペイロードを構築し、バックエンドのカップリングマップを検索 find_check_layout して、利用可能なアンシラが多数あり、誤差の少ない量子ビット経路を見つけ出し、出 NoiseLearner 力をノイズ qiskit-paulice モデルに変換 learned_noise_model し、スタビライザーが計算基底で測定されるように回路を回転 append_basis_rotation させ、サンプリングされたカウント数からスタビライザーの期待値を計算 expectation し、実行中のフィデリティ推定値を追跡 cum_mean_sem します。
# Standard library imports
import random
import time
# External libraries
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
# Qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Clifford, Pauli, PauliLindbladMap, PauliList
from qiskit.result import sampled_expectation_value
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.visualization import plot_coupling_map
# Qiskit Aer
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import ReadoutError, depolarizing_error
# Qiskit IBM Runtime
from qiskit_ibm_runtime import NoiseLearner, QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
# Qiskit Paulice
from qiskit_paulice import add_pauli_checks
from qiskit_paulice.layout import get_check_qubits
from qiskit_paulice.noise_models import NoiseModeldef random_clifford_circuit(
num_qubits: int, depth: int, rng: np.random.Generator
) -> QuantumCircuit:
"""Brickwork random Clifford on `num_qubits`, with `depth` CZ layers."""
qc = QuantumCircuit(num_qubits)
qc.h(range(num_qubits))
for d in range(depth):
for i in range(d % 2, num_qubits - 1, 2):
qc.cz(i, i + 1)
for q in range(num_qubits):
if rng.integers(0, 2):
qc.sx(q)
if rng.integers(0, 2):
qc.s(q)
if rng.integers(0, 2):
qc.sx(q)
return qc
def find_check_layout(
backend,
num_qubits: int,
rng: np.random.Generator,
num_trials: int = 200,
max_gate_error: float = 0.03,
max_readout_error: float = 0.2,
) -> list[int]:
"""Find a low-error path of `num_qubits` qubits with many available ancillas.
Builds random self-avoiding walks on the coupling map, excluding the qubits
and two-qubit gates whose reported errors exceed the thresholds, and keeps
the path that offers the most target and ancilla pairs. Ties are broken by
the lower average two-qubit gate error along the path.
"""
target = backend.target
gate_2q = next(
name for name in ("cz", "ecr", "cx") if name in target.operation_names
)
# Collect per-edge gate errors and per-qubit readout errors
edge_error = {}
for qubits, props in target[gate_2q].items():
edge = tuple(sorted(qubits))
if props is not None and props.error is not None:
edge_error[edge] = min(edge_error.get(edge, 1.0), props.error)
readout_error = {
qubit: target["measure"][(qubit,)].error
for (qubit,) in target["measure"]
}
# Keep only the edges whose gate and readout errors are acceptable
adjacency = {}
for (q1, q2), error in edge_error.items():
if (
error <= max_gate_error
and readout_error.get(q1, 1.0) <= max_readout_error
and readout_error.get(q2, 1.0) <= max_readout_error
):
adjacency.setdefault(q1, set()).add(q2)
adjacency.setdefault(q2, set()).add(q1)
# Random self-avoiding walks; keep the path with the most check pairs
starts = sorted(adjacency)
best_path = None
best_score = (-1, float("inf"))
for _ in range(num_trials):
path = [starts[rng.integers(len(starts))]]
while len(path) < num_qubits:
options = sorted(adjacency[path[-1]] - set(path))
if not options:
break
path.append(options[rng.integers(len(options))])
if len(path) < num_qubits:
continue
num_pairs = len(get_check_qubits(backend.coupling_map, path)[0])
mean_error = float(
np.mean(
[edge_error[tuple(sorted(e))] for e in zip(path, path[1:])]
)
)
if num_pairs > best_score[0] or (
num_pairs == best_score[0] and mean_error < best_score[1]
):
best_path, best_score = path, (num_pairs, mean_error)
if best_path is None:
raise RuntimeError(
"No connected low-error path found. Relax the error thresholds."
)
return best_path
def learned_noise_model(layer_errors, layout: list[int]) -> NoiseModel:
"""Build a `NoiseModel` from `NoiseLearner` results.
`NoiseLearner` reports one `PauliLindbladError` per entangling layer, whose
generators are indexed against that layer's own physical qubits, while
`NoiseModel.from_pauli_lindblad_maps` expects `PauliLindbladMap`s indexed the
way `NoiseModel.from_backend` indexes them: by position in `layout`. This
translates between the two and drops generators that fall outside `layout`.
"""
phys_to_virt = {phys: virt for virt, phys in enumerate(layout)}
maps = []
for layer in layer_errors:
if layer.error is None:
continue
terms = []
for pauli, rate in zip(
layer.error.generators, layer.error.rates, strict=True
):
label, indices = [], []
for local, phys in enumerate(layer.qubits):
x, z = bool(pauli.x[local]), bool(pauli.z[local])
if not (x or z):
continue
if phys not in phys_to_virt:
break # generator reaches outside the layout, so skip it
label.append("Y" if x and z else "X" if x else "Z")
indices.append(phys_to_virt[phys])
else:
if label:
terms.append(
("".join(label), tuple(indices), float(rate))
)
# Each map needs a 2-qubit generator to define an entangling layer
if any(len(t[1]) == 2 for t in terms):
maps.append(
PauliLindbladMap.from_sparse_list(
terms, num_qubits=len(layout)
)
)
if not maps:
raise RuntimeError(
"No usable layer errors. Check that the learner ran on this layout."
)
return NoiseModel.from_pauli_lindblad_maps(maps)
def append_basis_rotation(
circuit: QuantumCircuit, pauli: Pauli
) -> QuantumCircuit:
"""Strip measurements, append basis rotations for `pauli`, and re-measure."""
out = circuit.remove_final_measurements(inplace=False)
for q in range(pauli.num_qubits):
if pauli.x[q]:
if pauli.z[q]:
out.sdg(q)
out.h(q)
out.measure_all()
return out
def expectation(counts: dict, pauli: Pauli) -> float:
"""Expectation value of `pauli` from counts measured in the Z basis.
Pads with identity on any qubits beyond the support of `pauli`, such as the
check ancillas that appear in the postselected counts.
"""
if not counts:
return float("nan")
n = pauli.num_qubits
sign = -1 if int(pauli.phase) % 4 == 2 else 1
total = len(next(iter(counts)))
label = "".join(
"Z" if q < n and (pauli.x[q] or pauli.z[q]) else "I"
for q in range(total - 1, -1, -1)
)
return sign * sampled_expectation_value(counts, label)
def cum_mean_sem(values: np.ndarray):
"""Cumulative mean and standard error of the mean, ignoring NaNs."""
valid = ~np.isnan(values)
total = np.cumsum(np.where(valid, values, 0.0))
total_sq = np.cumsum(np.where(valid, values**2, 0.0))
count = np.maximum(np.cumsum(valid).astype(float), 1)
mean = total / count
sem = np.sqrt(np.maximum(total_sq / count - mean**2, 0) / count)
return np.where(np.cumsum(valid) > 0, mean, np.nan), sem小規模シミュレータの例
このセクションでは、ノイズの多いシミュレータを用いたワークフロー全体について解説します。 この手法では、バックエンドのベンチマークデータを用いて量子ビットの配置とノイズモデルを選択し、チェックを自動的に見つけ出し、サンプリングされた分布に対してポストセレクションを適用することで、フィデリティの向上を示す。
ステップ1:古典的な入力を量子問題に写像する
ペイロード回路は、浅い一次元ブリックワーク型ランダム・クリフォード回路である。 この回路はクリフォード回路であるため、サンプリングされたスタビライザーの期待値からそのフィデリティを直接推定できるスタビライザー状態を生成します。 次のステップでチェック箇所が視覚的に把握しやすいよう、まずは単純な回路から始めましょう。
num_qubits = 12
depth = 4
seed = 1764
rng = np.random.default_rng(seed)
np.random.seed(seed)
circuit = random_clifford_circuit(num_qubits, depth, rng)
circuit.measure_all()
circuit.draw("mpl", fold=-1, scale=0.6)Output:
ステップ 2:量子ハードウェアでの実行に向けた最適化
回路をハードウェアにマッピングすることで、物理的な量子ビットの配置、候補となるチェックを評価するノイズモデル、およびチェックそのものが決定されます。
まず、バックエンドを選択し、そのカップリングマップから、「セットアップ」セクションで定義されたヘルパー find_check_layout を含む1次元クビット配置を検索します。 このヘルパーは、誤差が最も大きいゲートや読み出しを回避するランダムな自己回避経路を生成し、ターゲットとアンシラの組み合わせが最も多い経路を選択します。 この検索処理は、バックエンド自体から接続状況やエラーデータを読み取るため、どの IBM Quantum QPUでも同じコードを実行できます。 その後、この関 get_check_qubits 数はターゲットとアンシラのペアを返します。ここで、のチェックにはが使用 target_qubits[i] されます ancilla_qubits[i]。
以下の結合グラフでは、緑色の量子ビットがペイロード量子ビットであり、オレンジ色の量子ビットがチェック機能を実装するアンシラです。 隣接するアンシラを持つ量子ビットは、チェックのターゲット量子ビットとして使用される。
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(f"Backend: {backend.name}")
# Search for a low-error path, then pair each target qubit with a neighboring ancilla
layout = find_check_layout(backend, num_qubits, rng)
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)
num_checks = len(target_qubits)
print(f"Target qubits: {target_qubits}")
print(f"Ancilla qubits: {ancilla_qubits}")
plot_coupling_map(
num_qubits=backend.num_qubits,
qubit_coordinates=getattr(
backend.configuration(), "qubit_coordinates", None
),
coupling_map=backend.configuration().coupling_map,
figsize=(12, 12),
qubit_color=[
"#4CAF50"
if i in set(layout)
else "#FF9800"
if i in set(ancilla_qubits)
else "#DDDDDD"
for i in backend.coupling_map.graph.node_indices()
],
qubit_size=220,
line_width=2,
font_size=90,
)Output:
Backend: ibm_boston
Target qubits: [105, 107, 108, 123, 125, 141, 143]
Ancilla qubits: [104, 97, 109, 122, 126, 140, 144]
バックエンドとレイアウトが決定したら、ペイロードを命令セットアーキテクチャ(ISA)回路にトランスパイルします。 必要なのは、レイアウトを設定し、ゲートをバックエンドのネイティブゲートセットに変換することだけです。
pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=layout
)
circuit_isa = pm.run(circuit)
circuit_isa.draw("mpl", fold=-1, scale=0.6)Output:
次に、バックエンドにおけるゲートノイズおよび読み出しノイズが実行にどのような影響を与えるかをモデル化します。 ノイズモデルによって、回路のどの部分でチェックを行うと最も多くのエラーを検出できるかが決まります。 より精度の高いモデルを使用すれば検出精度は向上しますが、通常、QPUからサンプリングを行ってそのようなモデルを学習させる必要はありません。 以下のモデルは、ベンチマーク qiskit-ibm-runtime データから、ゲートノイズおよび読み出しノイズに対する一様脱分極チャネルを推定するものである。
noise_model = NoiseModel.from_backend(
backend, layout, uniform_gate_noise=True
)
print(noise_model)Output:
NoiseModel(gate_noise=0.001079865281450939, readout_noise=0.006001790364583333, idling_noise=None)
それでは、回路にチェック機能を追加しましょう。 この関 add_pauli_checks 数は、クリフォード・ペイロード、対象クビットのリスト、およびノイズモデルを受け取ります。 この引 ancilla_qubits 数は、各ターゲットとどの物理アンシラをペアにするかを関数に指定します。 チェックは、対象となる量子ビットが登場する順に追加されるため、チェック対象の回路の最終的なレイアウトは となります layout + ancilla_qubits。 チェックのi回数を減らして出力回路を動作させるには、最終的なレイアウトは となります layout + ancilla_qubits[:i]。
の出力は、チェックの数が徐々に増えていく一連の回路であり、チェックが全くない状態から、各ターゲット量子ビットに対して1つのチェックが行われる状態までを網羅 add_pauli_checks しています。 この可視化により、チェックが指定されたターゲットとアンシラのペアを使用していることが確認できます。 適切なチェックを見つける方法の詳細については、参考文献 [1] の補足情報の第II節から第IV節を参照のこと。
checked = add_pauli_checks(
circuit_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
)
print(f"Physical layout of payload and ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)Output:
Physical layout of payload and ancillas: [108, 107, 106, 105, 117, 125, 124, 123, 136, 143, 142, 141, 104, 97, 109, 122, 126, 140, 144]
Checked circuit:
ステップ 3: Qiskit primitives を使用して実行する
ゲートノイズの影響を可視化するには、ペイロードの深さを増やし、そのスタビライザーの一部をサンプリングします。 各安定化演算子は、一般的に他の安定化演算子と量子ビットごとに可換ではないため、1つの検証手順を2つの異なる安定化演算子に適用することはできない。 安定化子を「通勤集合」ごとにまとめるのではなく、各安定化子について個別に、適切な検証集合を見つける。 安定化器を均一に無作為にサンプリングすることで、偏りのない忠実度の推定値が得られる。
より複雑な回路を構築し、その安定化素子からランダムにサンプルを抽出してください。
depth = 24
num_stabilizers = 20
num_shots = 1_000
circuit = random_clifford_circuit(num_qubits, depth, rng)
# Build the full stabilizer group, then sample from it uniformly at random
circ_no_meas = circuit.remove_final_measurements(inplace=False)
stabilizer_group = PauliList([Pauli("I" * num_qubits)])
for generator in (
Pauli(label) for label in Clifford(circ_no_meas).to_labels(mode="S")
):
stabilizer_group = stabilizer_group + stabilizer_group.compose(generator)
keep = np.where(
stabilizer_group.x.any(axis=1) | stabilizer_group.z.any(axis=1)
)[0]
chosen = np.random.default_rng(seed).choice(
keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = [stabilizer_group[int(i)] for i in chosen]
two_qubit_depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
f"Sampled {len(stabilizers)} stabilizers of a {circuit.num_qubits}-qubit "
f"circuit with two-qubit depth {two_qubit_depth}: "
f"{{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)Output:
Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {ZXIIXZYYXIZZ, XXXYIIZYXIII, ...}
サンプリングされた各スタビライザーについて、そのスタビライザーが計算基底で測定されるように回路を回転させ、バックエンドにトランスパイルし、適切なチェックセットを見つける。 各スタビライザーについて、ターゲットとアンシラのペアを一緒にシャッフルし、各ターゲットがそれぞれのアンシラを維持するようにする。 チェックは、対象となる量子ビットが指定された順序で順次確定され、一度確定したチェックは、その後さらにチェックが追加されても変更されないことに注意してください。
noisy_circuits = []
checked_circuits = []
depths_2q = []
t0 = time.time()
for i, pauli in enumerate(tqdm(stabilizers)):
noisy_circuits.append(pm.run(append_basis_rotation(circuit, pauli)))
# Shuffle target and ancilla pairs together so each target keeps its ancilla
targets, ancillas = zip(
*random.sample(
list(zip(target_qubits, ancilla_qubits, strict=True)),
k=len(target_qubits),
),
strict=True,
)
checked_circuits.append(
add_pauli_checks(
noisy_circuits[-1],
list(targets),
noise_model,
ancilla_qubits=list(ancillas),
cost="gamma",
method="windowed",
seed=seed + 1 + i,
)
)
depths_2q.append(
checked_circuits[-1][-1].circuit.depth(lambda x: len(x.qubits) == 2)
)
print(
f"Added {num_checks} checks to {len(stabilizers)} circuits "
f"in {(time.time() - t0):.0f}s."
)
print(
f"On average, two-qubit depth increased from "
f"{circuit.depth(lambda x: len(x.qubits) == 2)} to {int(np.mean(depths_2q))} "
f"when adding {num_checks} checks."
)Output:
100%|██████████| 20/20 [00:15<00:00, 1.29it/s]
Added 7 checks to 20 circuits in 15s.
On average, two-qubit depth increased from 24 to 33 when adding 7 checks.
Qiskit Aer を使用して、ベアペイロードと検証済み回路をサンプルとして実行してみてください。 このシミュレータは、チェックの採点に使用されたものと同じ脱分極モデルを採用しているため、チェックの対象となるノイズは、シミュレータが適用するノイズそのものです。
aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
p = noise_model.readout_noise
aer_nm.add_all_qubit_readout_error(ReadoutError([[1 - p, p], [p, 1 - p]]))
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)
counts = []
for i, checked_circ_result in enumerate(tqdm(checked_circuits)):
noisy_counts = (
noisy_sim.run(
noisy_circuits[i], shots=num_shots, seed_simulator=seed * i + 1
)
.result()
.get_counts()
)
checked_counts_per_variant = []
for k, ck in enumerate(checked_circ_result):
variant_counts = (
noisy_sim.run(
ck.circuit, shots=num_shots, seed_simulator=seed * i + 2 + k
)
.result()
.get_counts()
)
checked_counts_per_variant.append(variant_counts)
counts.append((noisy_counts, checked_counts_per_variant))Output:
100%|██████████| 20/20 [00:17<00:00, 1.13it/s]
ステップ4:後処理を行い、結果を所望の従来の形式で出力する
各チェックでは、1つのアンシラと1つのターゲットの間でエンタングルメントゲートが使用されます。 このancillaは から開始されるため、 によってその入力が安定化されます。 をチェック回路に前方伝播させると、出力としてパウリ演算子が得られ、その非単位項がチェックのサポートを定義する。 チェックは、そのサポート内のビットのパリティが偶数である場合に合格となります。 すべてのチェックに合格した場合にのみ、サンプルが保持されます。
各 は get_postselection_method 、測定されたビット列をシンドロームベクトルに写像する関数を返します CheckedCircuit 。 すべてのチェックにおいて症候群がゼロであるサンプルを残し、残りは除外する。 次のグラフは、チェックの回数を増やすと、選別後の残存率が低下することを示しています。 ポストセレクション率が低いほど、目標の精度に達するにはより多くのショットが必要となるため、検出能力とサンプリングコストの間にはトレードオフが存在する。 この率は収束しているように見受けられ、これは追加のチェックを行っても検出能力の向上は限定的であることを示唆している。
rate_per_variant = []
kept_per_stab = []
for i, (_, checked_counts_per_variant) in enumerate(counts):
rates = []
kept_at_num_checks = None
for k, variant_counts in enumerate(checked_counts_per_variant):
ps_fn = checked_circuits[i][k].get_postselection_method()
kept = {
bs: n for bs, n in variant_counts.items() if not ps_fn(bs).any()
}
rates.append(sum(kept.values()) / num_shots)
if k == num_checks:
kept_at_num_checks = kept
rate_per_variant.append(rates)
kept_per_stab.append(kept_at_num_checks)
max_len = max(len(s) for s in rate_per_variant)
rates_arr = np.full((len(rate_per_variant), max_len), np.nan)
for i, s in enumerate(rate_per_variant):
rates_arr[i, : len(s)] = s
ks = np.arange(max_len)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, rates_arr.T, color="#ff8c00", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.nanmedian(rates_arr, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set_xlabel("Checks committed")
ax.set_ylabel("Postselection rate")
ax.set_ylim((0, 1.05))
ax.set_title(
f"Per-stabilizer postselection rate ({len(rates_arr)} stabilizers)"
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()Output:
それでは、ノイズを含む素の状態と、事後選択された状態の忠実度を比較してみましょう。 エラーが検出されなかったサンプルのみを事後選択することで、すべてのスタビライザーの期待値が上昇し、その結果、推定フィデリティも向上する。 ポスト選択された値は、生データよりも少ないサンプル数で算出されるにもかかわらず、期待値はより正確であり、サンプリング分散も小さくなります。 また、選択後の平均率は、ノイズを含む忠実度に近似していることにも注目してください。 チェックによって誤ったサンプルのほぼすべてが検出される場合、予想されるのは次のような現象です。すなわち、すべてのチェックに合格するサンプルの割合が、誤りのないサンプルの割合に近づいていくのです。これが、ノイズを含む状態の忠実度となります。
results = []
for i, ((noisy_counts, _), kept) in enumerate(
zip(counts, kept_per_stab, strict=True)
):
results.append(
(
expectation(noisy_counts, stabilizers[i]),
expectation(kept, stabilizers[i]),
sum(kept.values()) / num_shots,
)
)
fidelity_noisy = float(np.nanmean([r[0] for r in results]))
fidelity_postsel = float(np.nanmean([r[1] for r in results]))
psr = float(np.mean([r[2] for r in results]))
print(
f"ideal fidelity: 1.0\n"
f"noisy fidelity: {fidelity_noisy:.4f}\n"
f"postselected fidelity: {fidelity_postsel:.4f}\n"
f"mean postselection rate: {psr:.3f}"
)
evs_ideal = np.ones(len(results))
evs_noisy = np.array([r[0] for r in results])
evs_post = np.array([r[1] for r in results])
idx = np.arange(len(results))
def strip(ax, ys, color, label):
m, s = np.nanmean(ys), np.nanstd(ys)
ax.axhspan(
m - s, m + s, color=color, alpha=0.15, label=f"{label} mean and std"
)
ax.axhline(
m, color=color, linewidth=1, linestyle="--", label=f"{label} fidelity"
)
fig, ax = plt.subplots(figsize=(8, 4))
ax.axhline(np.nanmean(evs_ideal), color="black", linewidth=1.5, label="ideal")
strip(ax, evs_noisy, "red", "noisy")
strip(ax, evs_post, "green", "postselected")
ax.scatter(idx, evs_noisy, color="red", s=22, alpha=0.7, label="noisy EVs")
ax.scatter(
idx,
evs_post,
color="green",
s=22,
alpha=0.7,
label="postselected EVs",
)
ax.set_xlabel("stabilizer index")
ax.set_ylabel(r"$\langle G \rangle$")
ax.set_ylim((-0.1, 1.1))
ax.set_title("Per-stabilizer expectation values")
ax.legend(loc="lower left")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
M = np.arange(1, len(results) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
(evs_ideal, "black", "ideal"),
(evs_noisy, "red", "noisy"),
(evs_post, "green", "postselected"),
]:
cm, sem = cum_mean_sem(ys)
ax.plot(M, cm, color=color, linewidth=1.5, label=label)
ax.fill_between(M, cm - sem, cm + sem, color=color, alpha=0.15)
ax.set_xlabel("number of stabilizers averaged")
ax.set_ylabel("running fidelity estimate")
ax.set_title("Fidelity convergence versus number of stabilizers")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()Output:
ideal fidelity: 1.0
noisy fidelity: 0.7899
postselected fidelity: 0.9679
mean postselection rate: 0.780
ガンマスコアは、モデル化されたノイズチャネルのうち、どの程度の部分がチェックによって検出されなかったかを示します。 ガンマスコアを「コミットされたチェックの数」に対してプロットすると、チェックが追加されるごとに検出能力が向上していく様子がわかります。 の値は、チェックによってモデル化されたノイズがすべて捕捉されていることを意味 1.0 します。 チェックのコミット数が増える 1.0 につれて、曲線は の方向に下がっていきます。これは、チェックが1つ追加されるごとに、残りの未検出エラーの一部が捕捉されることを示しています。
stab_scores = [
[variant.cost for variant in checked_circ_result]
for checked_circ_result in checked_circuits
]
max_len = max(len(s) for s in stab_scores)
scores = np.full((len(stab_scores), max_len), np.nan)
for i, s in enumerate(stab_scores):
scores[i, : len(s)] = s
ks = np.arange(max_len)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, scores.T, color="#4682b4", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.nanmedian(scores, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set_xlabel("Checks committed")
ax.set_ylabel("Gamma")
ax.set_yscale("log")
ax.set_title(f"Per-stabilizer gamma curves ({len(scores)} stabilizers)")
ax.legend()
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()Output:
大規模なハードウェアの例
同じワークフローが、より大容量でより深いペイロードを備えたハードウェア上でも実行されます。 このセクションでは、シミュレータの例で使用したバックエンドを再利用しますが、独自のターゲットおよびアンシラペア、パスマネージャを備えた新しい20キュービットのレイアウトを構築し、その回路を単一のジョブとしてQPUに送信します。 このサイズでは、ほとんどのショットで少なくとも1回のチェックがトリガーされるため、ポストセレクション率は低く、各回路では十分なサンプル数を確保するために多量のショット予算が必要となります。 したがって、この例では、予算を少数のサンプリングされたスタビライザーに集中させています。これは依然として不偏の忠実度推定値ですが、シミュレータの例のように多数のスタビライザーの平均値を用いた場合よりも精度が粗くなります。
シミュレータの例と比べて、1つ異なる点があります。それは、キャリブレーションデータから一様な脱分極チャネルを推定する代わりに、このセクションではノイズモデルを学習し、その結果を用いてモデル qiskit-paulice を構築する点 NoiseLearner です NoiseModel.from_pauli_lindblad_maps。 学習済みパウリ・リンドブラッドモデルは、すべてのエッジでノイズの程度が等しいと仮定するのではなく、この特定のレイアウトにおけるノイズの空間的構造を捉えているため、チェックの配置評価は、QPUに影響を与えるノイズにより近い条件に基づいて行われる。 ノイズの学習にはQPUのサンプリングが必要であり、QPU全体のサンプリング予算においてこれを考慮に入れる必要があります。
以下のパラメータは、量子ビット数、反復回数、安定化演算子の数、および実行回数を設定します。 ポストセレクション率の逆数 hw_num_shots に比例する:3%の率の場合、40,000ショットのうち、1回路あたり約1,200個のポストセレクションされたサンプルが残る。 この hw_num_stabilizers 値を大きくすると、忠実度の推定精度が向上しますが、その代償としてジョブあたりの回路数が増加し、各回路には同じショット予算が必要となります。
手順 1~4(1つのコードブロックにまとめました)
次のセルでは、シミュレータの例と同じ4つの手順が実行されます。 より大規模なペイロードを構築し、いくつかのスタビライザーをサンプリングします(ステップ1)。 レイアウトを選択し、そのレイアウト上でノイズモデルを学習させ、各スタビライザーについて完全にチェック済みの回路を特定します(ステップ2);ベア回路とチェック済み回路の両方を含む1つのSamplerジョブを提出します(ステップ3);そして、スタビライザーごとおよび平均値について、チェック済みカウントをポストセレクションし、ノイズの影響を受けたフィデリティ推定値とポストセレクション後のフィデリティ推定値を比較します(ステップ4)。 この規模では、シミュレータの例のように安定化子群全体を列挙することは現実的ではないため、セルは安定化子のランダムな部分集合をサンプリングして、忠実度の推定値を算出する。
ステップ2ではシミュレーターの例よりも多くの処理が行われていることに注意してください。ノイズモデルを学習すると、独自のノイズモデルが送信されます。 NoiseLearner サンプラージョブの前にジョブが実行されるため、セルは合計で 2つのジョブを実行します。 後でそれらを見つけられるように TUT_ASPC 、タグと TUT_ASPC_LEARN が付いています。 求人へのタグ付けに関する詳細は、「 求人タグによる整理と検索」 をご覧ください。
# -------------------------Step 1: build a larger payload and sample stabilizers-------------------------
hw_num_qubits = 20
hw_depth = 36
hw_num_stabilizers = 10
hw_num_shots = 40_000
hw_circuit = random_clifford_circuit(hw_num_qubits, hw_depth, rng)
hw_no_meas = hw_circuit.remove_final_measurements(inplace=False)
# Enumerating all 2^n stabilizers is infeasible at this size, so draw each
# stabilizer by composing a random subset of the group generators
hw_generators = [
Pauli(label) for label in Clifford(hw_no_meas).to_labels(mode="S")
]
sample_rng = np.random.default_rng(seed)
hw_stabilizers = []
while len(hw_stabilizers) < hw_num_stabilizers:
mask = sample_rng.integers(0, 2, hw_num_qubits).astype(bool)
if not mask.any():
continue # skip the identity
stabilizer = Pauli("I" * hw_num_qubits)
for generator, chosen in zip(hw_generators, mask, strict=True):
if chosen:
stabilizer = stabilizer.compose(generator)
hw_stabilizers.append(stabilizer)
# -------------------------Step 2: find a 20-qubit layout, learn its noise, and add checks-------------------------
# A single bad coupler or bad-readout qubit on the path drags every
# stabilizer down, so search harder and with tighter error thresholds
hw_layout = find_check_layout(
backend,
hw_num_qubits,
rng,
num_trials=500,
max_gate_error=0.015,
max_readout_error=0.05,
)
hw_target_qubits, hw_ancilla_qubits = get_check_qubits(backend, hw_layout)
hw_pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=hw_layout
)
print(f"Layout with {len(hw_target_qubits)} check pairs: {hw_layout}")
# ----- learn a Pauli-Lindblad noise model on this layout -----
# The simulator example scored checks against a uniform depolarizing channel
# inferred from calibration data. Here, learn the noise instead: NoiseLearner
# runs its own job on the QPU and returns a Pauli-Lindblad channel per unique
# entangling layer, so the checks are placed against the noise this layout
# actually has, including its spatial structure. All the sampled stabilizers
# share the same entangling layers and differ only in their final basis
# rotation, so learning on the bare payload covers all of them.
learner = NoiseLearner(
mode=backend,
options={
"max_layers_to_learn": 4,
"num_randomizations": 32,
"shots_per_randomization": 128,
"environment": {"job_tags": ["TUT_ASPC_LEARN"]},
},
)
learner_job = learner.run([hw_pm.run(hw_circuit)])
print(f"Submitted noise-learner job {learner_job.job_id()}")
hw_layer_errors = learner_job.result().data
# To see how much the learned model helps, swap the next line for the
# simulator example's uniform model - a one-line change:
# hw_noise_model = NoiseModel.from_backend(backend, hw_layout, uniform_gate_noise=True)
hw_noise_model = learned_noise_model(hw_layer_errors, hw_layout)
# NoiseLearner characterizes gate noise only, so keep the readout estimate
# from calibration data rather than leaving it unset
hw_noise_model.readout_noise = NoiseModel.from_backend(
backend, hw_layout, uniform_gate_noise=True
).readout_noise
print(
f"Learned {len(hw_layer_errors)} layers; "
f"readout noise {hw_noise_model.readout_noise:.5f}"
)
# ----- add the fully checked circuit per stabilizer -----
hw_noisy_circuits = []
hw_checked_circuits = []
for i, pauli in enumerate(tqdm(hw_stabilizers)):
bare = hw_pm.run(append_basis_rotation(hw_circuit, pauli))
hw_noisy_circuits.append(bare)
variants = add_pauli_checks(
bare,
hw_target_qubits,
hw_noise_model,
ancilla_qubits=hw_ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed + 1 + i,
)
hw_checked_circuits.append(variants[-1]) # keep the fully checked circuit
# -------------------------Step 3: submit one Sampler job with the bare and checked circuits-------------------------
sampler = Sampler(mode=backend)
sampler.options.default_shots = hw_num_shots
sampler.options.environment.job_tags = ["TUT_ASPC"]
pubs = hw_noisy_circuits + [cc.circuit for cc in hw_checked_circuits]
job = sampler.run(pubs)
print(f"Submitted job {job.job_id()} with {len(pubs)} circuits")
# -------------------------Step 4: postselect and compare fidelity-------------------------
result = job.result()
n_stab = len(hw_stabilizers)
hw_results = []
for i in range(n_stab):
noisy_counts = result[i].join_data().get_counts()
checked_counts = result[n_stab + i].join_data().get_counts()
ps_fn = hw_checked_circuits[i].get_postselection_method()
kept = {bs: c for bs, c in checked_counts.items() if not ps_fn(bs).any()}
hw_results.append(
(
expectation(noisy_counts, hw_stabilizers[i]),
expectation(kept, hw_stabilizers[i]),
sum(kept.values()) / sum(checked_counts.values()),
)
)
hw_fidelity_noisy = float(np.nanmean([r[0] for r in hw_results]))
hw_fidelity_postsel = float(np.nanmean([r[1] for r in hw_results]))
hw_psr = float(np.mean([r[2] for r in hw_results]))
print(
f"noisy fidelity estimate: {hw_fidelity_noisy:.4f}\n"
f"postselected fidelity estimate: {hw_fidelity_postsel:.4f}\n"
f"mean postselection rate: {hw_psr:.4f} "
f"(~{int(round(hw_psr * hw_num_shots))} kept shots per circuit)"
)
# Per-stabilizer breakdown. The postselection rate varies from stabilizer to
# stabilizer, so a stabilizer whose postselected value barely moves is usually
# one whose checks rejected little; the kept-shot count says how much of the
# gap is statistics rather than signal.
print("\nper-stabilizer results:")
print(
f"{'idx':>3} {'noisy':>8} {'postsel':>8} {'psr':>7} {'kept shots':>10}"
)
for i, (noisy, post, psr_i) in enumerate(hw_results):
print(
f"{i:>3} {noisy:>8.4f} {post:>8.4f} {psr_i:>7.4f} "
f"{int(round(psr_i * hw_num_shots)):>10}"
)
hw_noisy = np.array([r[0] for r in hw_results])
hw_post = np.array([r[1] for r in hw_results])
idx = np.arange(n_stab)
fig, ax = plt.subplots(figsize=(9, 4))
ax.axhline(1.0, color="black", linewidth=1.5, label="ideal")
strip(ax, hw_noisy, "red", "noisy")
strip(ax, hw_post, "green", "postselected")
ax.scatter(idx, hw_noisy, color="red", s=22, alpha=0.7, label="noisy EVs")
ax.scatter(
idx,
hw_post,
color="green",
s=22,
alpha=0.7,
label="postselected EVs",
)
ax.set_xlabel("stabilizer index")
ax.set_ylabel(r"$\langle G \rangle$")
ax.set_ylim((-0.1, 1.1))
ax.set_xticks(idx)
ax.set_title("Per-stabilizer expectation values on hardware")
# Outside the axes so it cannot hide a data point
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()Output:
Layout with 11 check pairs: [153, 152, 151, 138, 131, 130, 129, 118, 109, 110, 111, 98, 91, 90, 89, 78, 69, 70, 71, 58]
Submitted noise-learner job d9f4mncjeosc73fjfmkg
Learned 4 layers; readout noise 0.00470
100%|██████████| 10/10 [01:01<00:00, 6.18s/it]
Submitted job d9f4s04jeosc73fjftkg with 20 circuits
noisy fidelity estimate: 0.3685
postselected fidelity estimate: 0.6869
mean postselection rate: 0.2851 (~11404 kept shots per circuit)
per-stabilizer results:
idx noisy postsel psr kept shots
0 0.3769 0.6918 0.3247 12987
1 0.3745 0.6760 0.2999 11995
2 0.3659 0.6389 0.3549 14196
3 0.3821 0.7060 0.2660 10641
4 0.3653 0.7475 0.2531 10124
5 0.3752 0.7022 0.2698 10791
6 0.3508 0.7144 0.2711 10842
7 0.3485 0.7087 0.2381 9523
8 0.3825 0.6289 0.2928 11711
9 0.3630 0.6549 0.2808 11232
この規模の回路の場合、ほとんどのサンプルには少なくとも1つのエラーが検出されるため、ポストセレクション率は低く、ポストセレクションによってほとんどのショットが除外されてしまう。 すべてのチェックに合格したサンプルは、素の回路よりもはるかに優れた期待値を示しており、各スタビライザーごとの値は、ノイズの多いベースラインから明確に区別されています。 忠実度の推定精度を高めるには、回路ごとのショット数の上限を同じまま、より多くのスタビライザーをサンプリングします。 ポストセレクション率を高めるには、回路の深さを減らすか、チェックの数を減らす。ペイロードを拡大するには、ポストセレクション率の逆数に応じてショット予算を拡大する。
次のステップ
この作品に興味を持たれた方は、以下の資料もご参照ください:
- 量子誤り訂正の導入として、 反復コードに関するチュートリアル。
- チェック検出API全体のドキュメント
qiskit-paulice、およびソースコードが格納されているパッケージの GitHub リポジトリ。 - 「空間時間符号を用いた低オーバーヘッドのエラー検出」 という論文は、チェックの理論的背景について論じている。
参照
- [1] Martiel, S., & Javadi-Abhari, A. (2025). 時空間符号を用いた低オーバーヘッドのエラー検出。 arXiv プレプリント arXiv:2504.15725。
- [2] ヴァン・デン・ベルグ, E., Bravyi, S., ガンベッタ, J. M., Jurcevic, P., Maslov, D. および Temme, K. (2023). コヒーレント・パウリチェックによるシングルショット誤差の低減。 『Physical Review Research 』, 5(3), 033193.