การตรวจจับข้อผิดพลาดแบบ low-overhead ด้วย spacetime code
ประมาณการการใช้งาน: 4 นาทีบนโปรเซสเซอร์ Heron (ibm_kingston หรือเทียบเท่า) (หมายเหตุ: นี่เป็นเพียงการประมาณการเท่านั้น เวลาการรันจริงของคุณอาจแตกต่างกัน)
ผลลัพธ์การเรียนรู้
-
spacetime Pauli checks ตรวจจับ logical errors ใน Clifford circuits อย่างไร และการทำ postselect บน syndrome ของพวกมันช่วยเพิ่ม fidelity ของการแจกแจงที่สุ่มตัวอย่างได้อย่างไร
-
วิธีใช้แพ็กเกจ
qiskit-pauliceเพื่อค้นหาและแทรก checks ที่มีประสิทธิภาพสำหรับฮาร์ดแวร์โดยอัตโนมัติด้วยget_check_qubits,NoiseModel, และadd_pauli_checks -
วิธีประมาณค่า fidelity ของสถานะ stabilizer โดยการสุ่มตัวอย่าง stabilizer ของมันและทำ postselect บน syndrome ของ check
-
วิธีรันขั้นตอนการทำงานตรวจจับข้อผิดพลาดแบบเต็มรูปแบบบนฮาร์ดแวร์ IBM Quantum® และเปรียบเทียบ fidelity ที่มีสัญญาณรบกวนกับที่ผ่านการ postselect
ข้อกำหนดเบื้องต้น
-
พื้นฐานฮาร์ดแวร์ สำหรับ quantum computing ระดับ utility-scale
-
Clifford และ stabilizer formalism รวมถึงวิธีที่ stabilizer group อธิบายสถานะ pure stabilizer
ความเป็นมา
Low-overhead error detection with spacetime codes [1] โดย Simon Martiel และ Ali Javadi-Abhari นำเสนอวิธีการตรวจจับ logical errors ใน circuits ที่เน้น Clifford เป็นหลัก ซึ่งอยู่ระหว่างการแก้ไขข้อผิดพลาดแบบเต็มรูปแบบกับการบรรเทาข้อผิดพลาดแบบเบา แนวคิดนี้ต่อยอดจาก coherent Pauli checks (CPC) จาก Single-shot error mitigation by coherent Pauli checks [2] โดย van den Berg และคณะ ในทั้งสองแนวทาง วงจร Clifford "payload" จะถูกพัวพันกับ ancilla qubits เพื่อตรวจสอบ invariant บางอย่าง การวัด ancilla จะได้ syndrome ที่รายงานว่ามีการตรวจพบข้อผิดพลาดระหว่างการทำงานหรือไม่ การเก็บเฉพาะตัวอย่างที่ไม่มีการตรวจพบข้อผิดพลาดจะช่วยเพิ่ม fidelity ของการแจกแจงที่สุ่มตัวอย่าง โดยแลกกับอัตราการ postselection ที่ลดลง
ความแตกต่างหลักระหว่าง coherent Pauli checks กับ spacetime checks คือตัวดำเนินการที่พวกมันวัด coherent Pauli checks วัด operator ที่มี weight สูงและระบุตำแหน่งตามเวลา บนโทโพโลยีของ qubit ที่มีการเชื่อมต่อจำกัด เช่น heavy hex checks เหล่านั้นต้องใช้ SWAP gates จำนวนมากและมักทำให้วงจรลึกเกินไปที่จะรันได้จริง การนำ checks ไปใช้เป็น spacetime codes แทนจะกระจาย check แต่ละอันไปทั่ว payload circuit ทั้งในเชิงพื้นที่และเวลา สิ่งนี้ให้ encoding ที่มีประสิทธิภาพสำหรับฮาร์ดแวร์ซึ่งยังคงมีประสิทธิผลในการตรวจจับ logical errors ในขณะที่รักษา overhead ของ qubit และความลึกให้ต่ำ
สิ่งที่แพ็กเกจ qiskit-paulice ทำ
แพ็กเกจ qiskit-paulice ทำการสร้าง checks เหล่านี้โดยอัตโนมัติ ดังนั้นคุณจึงไม่ต้องสร้างมันด้วยมือ บทบาทหลักของมันคือค้นหาและแทรก spacetime Pauli checks ที่ถูกต้องในตำแหน่งของวงจรที่เพิ่มการตรวจจับข้อผิดพลาดสูงสุดในขณะที่ลด overhead ของ qubit ให้น้อยที่สุด check จะ ถูกต้อง เมื่อ operator ของมันไม่เปลี่ยนแปลงการทำงานเชิงตรรกะของ payload circuit, มี weight ต่ำ เมื่อใช้ entangling gates น้อย และ มีประสิทธิผล เมื่อตรวจจับข้อผิดพลาดได้ในสัดส่วนที่มาก เทียบกับสัญญาณรบกวนที่ check เองนำมา แพ็กเกจให้คะแนน check ที่เป็นตัวเลือกโดยเทียบกับ noise model และเลือก check ที่ดีที่สุดใส่ในวงจร บทเรียนนี้ใช้เมธอด API สามตัว:
-
get_check_qubitsตรวจสอบ coupling map ของ backend และคืนค่าคู่ target qubit และ ancilla qubit check บนtarget_qubits[i]ใช้ancilla_qubits[i] -
NoiseModel.from_backendสร้าง noise model แบบคร่าวๆ จากข้อมูล benchmark ของ backend โมเดลนี้ใช้ให้คะแนน check ที่เป็นตัวเลือก ดังนั้นจึงไม่จำเป็นต้องใช้ noise model ที่แม่นยำและเรียนรู้มา สำหรับ Pauli-Lindblad model ที่เรียนรู้มา ดูที่NoiseModel.from_pauli_lindblad_maps -
add_pauli_checksค้นหาและแทรก checks ลงในวงจร มันคืนค่าลำดับของอ็อบเจกต์CheckedCircuitที่มีจำนวน checks เพิ่มขึ้นเรื่อยๆ และแต่ละอ็อบเจกต์ให้get_postselection_methodที่แม็ป bitstring ที่วัดได้ไปยัง syndrome vector อาร์กิวเมนต์costเลือกฟังก์ชันที่ให้คะแนน check (gammaซึ่งเป็น sampling overhead ของ postselected inverse noise channel หรือLERซึ่งเป็น logical error rate) อาร์กิวเมนต์methodเลือกกลยุทธ์การค้นหา (windowed,genetic, หรือwindowed_genetic) บทเรียนนี้ใช้cost="gamma"และmethod="windowed"ซึ่งรวมกันแล้วให้การเลือก check ที่เป็น deterministic และทำซ้ำได้
ประมาณ fidelity จากการสุ่มตัวอย่าง stabilizer
เพื่อวัดว่าการตรวจจับข้อผิดพลาดทำงานได้ดีเพียงใด คุณสามารถประมาณ fidelity ของสถานะ stabilizer ที่วงจรเตรียมไว้ในอุดมคติ เทียบกับสถานะที่มีสัญญาณรบกวน ที่ฮาร์ดแวร์ให้ผลลัพธ์จริง projector บนสถานะ pure stabilizer เท่ากับค่าเฉลี่ยแบบสม่ำเสมอเหนือสมาชิก ตัวของ stabilizer group ของมัน:
การแทนค่านี้ลงในสูตร fidelity ให้ fidelity ของ เป็นค่าเฉลี่ยของ expectation value ของ stabilizer ทุกตัว เทียบกับ :
สำหรับปัญหาที่ใหญ่ขึ้น การนับ stabilizer ทั้งหมด ตัวนั้นทำไม่ได้จริง ดังนั้นคุณสามารถประมาณ fidelity จากตัวอย่างแบบสุ่มได้ การสุ่มเลือก stabilizers อย่างสม่ำเสมอจาก ให้การประมาณที่ไม่มีอคติ:
เนื่องจากวงจร Clifford เตรียมสถานะ stabilizer คุณจึงสามารถประมาณ fidelity ของมันได้โดยตรงจาก expectation value ที่สุ่มตัวอย่างของ stabilizer ของมัน บทเรียนนี้จะเดินผ่านขั้นตอนการทำงานบนตัวจำลองด้วยวงจรขนาดเล็กก่อน จากนั้นรันขั้นตอนเดียวกันบนฮาร์ดแวร์ด้วยวงจรที่ใหญ่และลึกกว่า เมื่อวงจรมี non-Clifford operations มากขึ้น จำนวน checks ที่ถูกต้องจะลดลงอย่างรวดเร็ว ดังนั้นวิธีนี้จึงเหมาะที่สุดกับวงจรที่เน้น Clifford เป็นหลัก
ข้อกำหนด
ก่อนเริ่มบทเรียนนี้ ตรวจสอบให้แน่ใจว่าคุณติดตั้งสิ่งต่อไปนี้แล้ว:
-
Qiskit SDK v2.0 ขึ้นไป พร้อมการรองรับ visualization
-
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)
การตั้งค่า
นำเข้าไลบรารีที่จำเป็นและกำหนดฟังก์ชันช่วยเหลือที่ไม่มีให้ในรูปแบบ import ฟังก์ชัน random_clifford_circuit สร้าง brickwork random Clifford payload แบบสุ่ม, find_check_layout ค้นหา coupling map ของ backend สำหรับเส้นทาง qubit ที่มีข้อผิดพลาดต่ำและมี ancilla ว่างจำนวนมาก, learned_noise_model แปลงผลลัพธ์ของ NoiseLearner ให้เป็น noise model ของ qiskit-paulice, append_basis_rotation หมุนวงจรเพื่อให้ stabilizer ถูกวัดใน computational basis, expectation คำนวณ expectation value ของ stabilizer จากผลนับที่สุ่มตัวอย่าง และ cum_mean_sem ติดตามการประมาณ fidelity แบบสะสม
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime qiskit-paulice tqdm
# 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 NoiseModel
def 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
ตัวอย่างตัวจำลองขนาดเล็ก
ส่วนนี้เดินผ่านขั้นตอนการทำงานแบบเต็มบนตัวจำลองที่มีสัญญาณรบกวน โดยใช้ข้อมูล benchmark ของ backend เพื่อเลือก qubit layout และ noise model ค้นหา checks โดยอัตโนมัติ และใช้ postselection บนการแจกแจงที่สุ่มตัวอย่างเพื่อแสดงการปรับปรุง fidelity
ขั้นตอนที่ 1: แม็ปอินพุตแบบคลาสสิกไปยังปัญหาควอนตัม
payload circuit คือวงจร brickwork random Clifford แบบมิติเดียวที่ตื้น เนื่องจากวงจรเป็น Clifford มันจึงเตรียมสถานะ stabilizer ที่คุณสามารถประมาณ fidelity ได้โดยตรงจาก expectation value ของ stabilizer ที่สุ่มตัวอย่าง เริ่มด้วยวงจรที่ตื้นเพื่อให้ checks นั้นมองเห็นได้ง่ายในขั้นตอนถัดไป
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)
ขั้นตอนที่ 2: ปรับให้เหมาะสมสำหรับการรันบนฮาร์ดแวร์ควอนตัม
การแม็ปวงจรไปยังฮาร์ดแวร์กำหนด physical qubit layout, noise model ที่ให้คะแนน check ที่เป็นตัวเลือก และตัว checks เอง
ก่อนอื่น เลือก backend และค้นหา coupling map ของมันเพื่อหา qubit layout แบบมิติเดียวด้วยฟังก์ชันช่วยเหลือ find_check_layout ที่กำหนดไว้ในส่วน Setup ฟังก์ชันช่วยเหลือสร้าง self-avoiding walks แบบสุ่มที่หลีกเลี่ยง gates และ readouts ที่มีข้อผิดพลาดสูงสุด และเก็บเส้นทางที่ให้คู่ target และ ancilla มากที่สุด เนื่องจากการค้นหาอ่านข้อมูล connectivity และข้อผิดพลาดจาก backend เอง โค้ดเดียวกันจึงรันได้บน IBM Quantum QPU ใดๆ จากนั้นฟังก์ชัน get_check_qubits จะคืนค่าคู่ target และ ancilla โดย check บน target_qubits[i] ใช้ ancilla_qubits[i]
ใน coupling graph ที่ตามมา qubit สีเขียวคือ payload qubits และ qubit สีส้มคือ ancilla ที่ใช้งาน checks qubit ที่มี ancilla อยู่ติดกันจะถูกใช้เป็น target qubits สำหรับ checks
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,
)
Backend: ibm_boston
Target qubits: [105, 107, 108, 123, 125, 141, 143]
Ancilla qubits: [104, 97, 109, 122, 126, 140, 144]

เมื่อเลือก backend และ layout แล้ว ให้ transpile payload ให้เป็นวงจร instruction set architecture (ISA) จำเป็นต้องตั้งค่า layout และแปล gates ให้เป็น native gate set ของ backend เท่านั้น
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)

ถัดไป จำลองว่าสัญญาณรบกวนจาก gate และ readout บน backend ส่งผลต่อการทำงานอย่างไร noise model กำหนดว่าตรงไหนในวงจรที่ check จับข้อผิดพลาดได้มากที่สุด โมเดลที่แม่นยำมากขึ้นจะช่วยเพิ่มการตรวจจับ แต่โดยปกติแล้วไม่จำเป็นต้องเรียนรู้โมเดลด้วยการสุ่มตัวอย่าง QPU โมเดลที่ตามมาอนุมาน uniform depolarizing channel สำหรับสัญญาณรบกวนของ gate และ readout จากข้อมูล benchmark ของ qiskit-ibm-runtime
noise_model = NoiseModel.from_backend(
backend, layout, uniform_gate_noise=True
)
print(noise_model)
NoiseModel(gate_noise=0.001079865281450939, readout_noise=0.006001790364583333, idling_noise=None)
ตอนนี้เพิ่ม checks ลงในวงจร ฟังก์ชัน add_pauli_checks รับ Clifford payload, รายการ target qubits, และ noise model อาร์กิวเมนต์ ancilla_qubits บอกฟังก์ชันว่า physical ancilla ตัวใดจะจับคู่กับ target ตัวใด checks จะถูกเพิ่มตามลำดับที่ target qubits ปรากฏ ดังนั้น layout สุดท้ายของวงจรที่ผ่านการ check คือ layout + ancilla_qubits หากต้องการรันวงจรผลลัพธ์ที่มี checks น้อยกว่า (i) layout สุดท้ายคือ layout + ancilla_qubits[:i]
ผลลัพธ์ของ add_pauli_checks คือลำดับของวงจรที่มีจำนวน checks เพิ่มขึ้นเรื่อยๆ ตั้งแต่ไม่มี checks จนถึงมี check หนึ่งตัวบน target qubit ทุกตัว ภาพที่แสดงยืนยันว่า checks ใช้คู่ target และ ancilla ตามที่ระบุไว้ สำหรับรายละเอียดในการค้นหา checks ที่ดี ดู Sections II ถึง IV ของข้อมูลเสริมในเอกสารอ้างอิง [1]
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)
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
เพื่อให้เห็นผลของสัญญาณรบกวนจาก gate ได้ชัดเจนขึ้น ให้เพิ่มความลึกของ payload และสุ่มตัวอย่างชุดย่อยของ stabilizer ของมัน โดยทั่วไปแล้ว stabilizer แต่ละตัวไม่ qubit-wise commuting กับตัวอื่น ดังนั้น checks ชุดเดียวจึงไม่ถูกต้องสำหรับ stabilizer สองตัวที่ต่างกัน แทนที่จะจัดกลุ่ม stabilizer เป็นชุดที่ commute กัน ให้หาชุด checks ที่ดีสำหรับแต่ละ stabilizer แยกกัน การสุ่มตัวอย่าง stabilizer อย่างสม่ำเสมอให้การประมาณ fidelity ที่ไม่มีอคติ
สร้างวงจรที่ลึกขึ้นและสุ่มตัวอย่าง stabilizer ของมัน
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]}, ...}}"
)
Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {ZXIIXZYYXIZZ, XXXYIIZYXIII, ...}
สำหรับ stabilizer ที่สุ่มตัวอย่างแต่ละตัว ให้หมุนวงจรเพื่อให้ stabilizer ถูกวัดใน computational basis, transpile ลงบน backend, และหาชุด checks ที่ดี คู่ target และ ancilla จะถูกสับเปลี่ยนร่วมกันสำหรับแต่ละ stabilizer เพื่อให้ target แต่ละตัวยังคงมี ancilla ของตัวเอง จำไว้ว่า checks จะถูกยืนยันตามลำดับที่ target qubits ถูกให้มา และ check ที่ยืนยันแล้วจะไม่เปลี่ยนแปลงเมื่อมีการเพิ่ม checks มากขึ้น
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."
)
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.
สุ่มตัวอย่าง payload แบบเปล่าและวงจรที่ผ่านการ check ด้วย Qiskit Aer ตัวจำลองใช้ depolarizing model เดียวกันกับที่ใช้ให้คะแนน checks ดังนั้นสัญญาณรบกวนที่ checks กำหนดเป้าหมายจึงเป็นสัญญาณรบกวนที่ตัวจำลองนำมาใช้
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))
100%|██████████| 20/20 [00:17<00:00, 1.13it/s]
ขั้นตอนที่ 4: ประมวลผลภายหลังและคืนผลลัพธ์ในรูปแบบคลาสสิกที่ต้องการ
check แต่ละตัวใช้ entangling gates ระหว่าง ancilla หนึ่งตัวกับ target หนึ่งตัว ancilla เริ่มต้นที่ ดังนั้น จึงเป็น stabilizer ของสถานะอินพุต การส่งต่อ ไปข้างหน้าผ่านวงจรที่ผ่านการ check จะได้ Pauli operator บนเอาต์พุตที่พจน์ที่ไม่ใช่ identity ของมันกำหนดขอบเขตของ check check จะผ่านเมื่อบิตในขอบเขตของมันมี parity คู่ ตัวอย่างจะถูกเก็บไว้ก็ต่อเมื่อ check ทุกตัวผ่านเท่านั้น
get_postselection_method ของแต่ละ CheckedCircuit คืนค่าฟังก์ชันที่แม็ป bitstring ที่วัดได้ไปยัง syndrome vector เก็บตัวอย่างที่มี syndrome เป็นศูนย์สำหรับ check ทุกตัว และทิ้งที่เหลือ กราฟที่ตามมาแสดงให้เห็นว่าการเพิ่ม checks มากขึ้นทำให้อัตราการ postselection ลดลง อัตราการ postselection ที่ต่ำลงต้องการ shots มากขึ้นเพื่อให้ได้ความแม่นยำเป้าหมาย ดังนั้นจึงมีการแลกเปลี่ยนระหว่างความสามารถในการตรวจจับและต้นทุนการสุ่มตัวอย่าง อัตรานี้ดูเหมือนจะลู่เข้า ซึ่งบ่งชี้ว่า checks เพิ่มเติมมีส่วนช่วยด้านความสามารถในการตรวจจับน้อยลง
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()
ตอนนี้เปรียบเทียบ fidelity ของสถานะที่มีสัญญาณรบกวนแบบเปล่ากับสถานะที่ผ่านการ postselect การ postselect เฉพาะตัวอย่างที่ไม่มีการตรวจพบข้อผิดพลาดจะเพิ่ม expectation value ของ stabilizer ทุกตัว และด้วยเหตุนี้จึงเพิ่ม fidelity ที่ประมาณได้ ค่าที่ผ่านการ postselect ใช้ตัวอย่างน้อยกว่าค่าดิบ แต่ expectation values แม่นยำกว่าและ variance ที่สุ่มตัวอย่างได้ต่ำกว่า สังเกตด้วยว่าอัตราการ postselection เฉลี่ยใกล้เคียงกับ fidelity ที่มีสัญญาณรบกวน นี่คือสิ่งที่คุณคาดหวังเมื่อ checks ตรวจจับตัวอย่างที่มีข้อผิดพลาดได้เกือบทั้งหมด: สัดส่วนของตัวอย่างที่ผ่าน check ทุกตัวจะเข้าใกล้สัดส่วนของตัวอย่างที่ไม่มีข้อผิดพลาด ซึ่งก็คือ fidelity ของสถานะที่มีสัญญาณรบกวน
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()
ideal fidelity: 1.0
noisy fidelity: 0.7899
postselected fidelity: 0.9679
mean postselection rate: 0.780

คะแนน gamma รายงานว่าสัญญาณรบกวนที่จำลองไว้เท่าใดที่ยังคงไม่ถูกตรวจจับโดย checks การพล็อตคะแนน gamma เทียบกับจำนวน checks ที่ยืนยันแล้วแสดงให้เห็นว่าความสามารถในการตรวจจับดีขึ้นอย่างไรเมื่อเพิ่ม check แต่ละตัว ค่า 1.0 หมายความว่า checks จับสัญญาณรบกวนที่จำลองไว้ทั้งหมด เส้นโค้งจะลดลงไปทาง 1.0 เมื่อยืนยัน checks มากขึ้น ซึ่งแสดงว่า check เพิ่มเติมแต่ละตัวจับส่วนหนึ่งของข้อผิดพลาดที่ยังไม่ถูกตรวจจับที่เหลืออยู่
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()
ตัวอย่างฮาร์ดแวร์ขนาดใหญ่
ขั้นตอนการทำงานเดียวกันรันบนฮาร์ดแวร์ด้วย payload ที่ใหญ่และลึกกว่า ส่วนนี้ใช้ backend เดียวกันกับตัวอย่างตัวจำลองอีกครั้ง แต่สร้าง layout 20 qubit ใหม่พร้อมคู่ target และ ancilla และ pass manager ของตัวเอง จากนั้นส่งวงจรไปยัง QPU ในงานเดียว ที่ขนาดนี้ shots ส่วนใหญ่จะกระตุ้น check อย่างน้อยหนึ่งตัว ดังนั้นอัตราการ postselection จึงต่ำ และแต่ละวงจรต้องการงบประมาณ shot จำนวนมากเพื่อให้มีตัวอย่างเพียงพอที่รอดมาได้ ตัวอย่างนี้จึงมุ่งงบประมาณไปที่ stabilizer ที่สุ่มตัวอย่างเพียงไม่กี่ตัว นี่ยังคงเป็นการประมาณ fidelity ที่ไม่มีอคติ แต่หยาบกว่าค่าเฉลี่ยของตัวอย่างตัวจำลองที่ใช้ stabilizer จำนวนมาก
สิ่งหนึ่งที่เปลี่ยนไปเมื่อเทียบกับตัวอย่างตัวจำลอง: แทนที่จะอนุมาน uniform depolarizing channel จากข้อมูล calibration ส่วนนี้ เรียนรู้ noise model ด้วย NoiseLearner และสร้างโมเดล qiskit-paulice จากผลลัพธ์ด้วย NoiseModel.from_pauli_lindblad_maps Pauli-Lindblad model ที่เรียนรู้มาจับโครงสร้างเชิงพื้นที่ของสัญญาณรบกวนบน layout เฉพาะนี้ แทนที่จะสมมติว่าทุก edge มีสัญญาณรบกวนเท่ากัน ดังนั้นตำแหน่งของ check จึงถูกให้คะแนนเทียบกับสัญญาณรบกวนที่คล้ายกับที่ส่งผลต่อ QPU มากกว่า การเรียนรู้สัญญาณรบกวนต้องสุ่มตัวอย่าง QPU และควรถูกนับรวมในงบประมาณการสุ่มตัวอย่าง QPU โดยรวม
พารามิเตอร์ที่ตามมากำหนดจำนวน qubit, ความลึก, จำนวน stabilizer, และจำนวน shots ปรับขนาด hw_num_shots ตามส่วนกลับของอัตราการ postselection: ที่อัตรา 3% การใช้ 40,000 shots จะเหลือตัวอย่างที่ผ่านการ postselect ประมาณ 1,200 ตัวอย่างต่อวงจร เพิ่ม hw_num_stabilizers เพื่อให้การประมาณ fidelity แม่นยำขึ้น โดยแลกกับวงจรมากขึ้นต่องาน ซึ่งแต่ละวงจรต้องการงบประมาณ shot เท่ากัน
ขั้นตอนที่ 1-4 (บีบอัดลงในบล็อกโค้ดเดียว)
เซลล์ต่อไปนี้รันสี่ขั้นตอนเดียวกันกับตัวอย่างตัวจำลอง มันสร้าง payload ที่ใหญ่ขึ้นและสุ่มตัวอย่าง stabilizer ไม่กี่ตัว (ขั้นตอนที่ 1) เลือก layout เรียนรู้ noise model บนมัน และหาวงจรที่ผ่านการ check เต็มรูปแบบสำหรับแต่ละ stabilizer (ขั้นตอนที่ 2) ส่งงาน Sampler หนึ่งงานที่มีทั้งวงจรเปล่าและวงจรที่ผ่านการ check (ขั้นตอนที่ 3) และทำ postselect กับผลนับที่ผ่านการ check เพื่อเปรียบเทียบการประมาณ fidelity ที่มีสัญญาณรบกวนและที่ผ่านการ postselect ต่อ stabilizer และโดยเฉลี่ย (ขั้นตอนที่ 4) ที่ขนาดนี้ การนับ stabilizer group ทั้งหมดเหมือนตัวอย่างตัวจำลองนั้นทำไม่ได้จริง ดังนั้นเซลล์จึงสุ่มเลือกชุดย่อยของ stabilizer เพื่อคำนวณการประมาณ fidelity
โปรดสังเกตว่าขั้นตอนที่ 2 ทำมากกว่าในตัวอย่างตัวจำลองที่นี่: การเรียนรู้ noise model ส่งงาน NoiseLearner ของตัวเองก่อนงาน Sampler ดังนั้นเซลล์จึงรันทั้งหมด สอง งาน พวกมันมีแท็ก TUT_ASPC_LEARN และ TUT_ASPC เพื่อให้คุณสามารถหาพวกมันได้ในภายหลัง ดู Organize and search by job tags สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับการติดแท็กงาน
# -------------------------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()
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
สำหรับวงจรขนาดนี้ ตัวอย่างส่วนใหญ่มีข้อผิดพลาดที่ถูกตรวจพบอย่างน้อยหนึ่งจุด ดังนั้นอัตราการ postselection จึงต่ำและการ postselect ทิ้ง shots ส่วนใหญ่ไป ตัวอย่างที่ผ่าน check ทุกตัวให้ expectation value ที่ดีกว่าวงจรเปล่ามาก และค่าต่อ stabilizer แยกออกจาก baseline ที่มีสัญญาณรบกวนได้อย่างชัดเจน เพื่อให้การประมาณ fidelity แม่นยำขึ้น ให้สุ่มตัวอย่าง stabilizer มากขึ้นด้วยงบประมาณ shot ต่อวงจรเท่าเดิม เพื่อเพิ่มอัตราการ postselection ให้ลดความลึกของวงจรหรือยืนยัน checks น้อยลง เพื่อขยายไปสู่ payload ที่ใหญ่ขึ้น ให้ปรับงบประมาณ shot ตามส่วนกลับของอัตราการ postselection
ขั้นตอนถัดไป
ถ้าคุณพบว่างานนี้น่าสนใจ คุณอาจสนใจเนื้อหาต่อไปนี้:
-
บทเรียนเรื่อง repetition codes สำหรับการแนะนำการแก้ไขข้อผิดพลาดควอนตัม
-
เอกสาร
qiskit-pauliceสำหรับ API การค้นหา check แบบเต็มรูปแบบ และ GitHub repository ของแพ็กเกจสำหรับซอร์สโค้ด -
บทความ Low-overhead error detection with spacetime codes สำหรับทฤษฎีเบื้องหลัง checks
เอกสารอ้างอิง
-
[1] Martiel, S., & Javadi-Abhari, A. (2025). Low-overhead error detection with spacetime codes. arXiv preprint arXiv:2504.15725.
-
[2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., & Temme, K. (2023). Single-shot error mitigation by coherent Pauli checks. Physical Review Research, 5(3), 033193.