การตรวจจับข้อผิดพลาดแบบ overhead ต่ำโดยใช้ spacetime code
ในบทเรียนนี้ เราจะ implement workflow เพื่อสุ่มตัวอย่างจาก Clifford circuit และใช้แพ็กเกจ qiskit-paulice เพื่อเพิ่ม fidelity ของการกระจายที่วัดได้ ด้วยการใส่ spacetime Pauli check ตลอดทั้ง circuit Spacetime Pauli check คล้ายกับ coherent Pauli check (CPC) ตรงที่ Clifford circuit "payload" ถูกพันกันกับ qubit เสริมบางตัวเพื่อ "ตรวจ" ค่าคงที่บางอย่างใน circuit การวัด ancilla ให้ syndrome ที่ใช้ตัดสินว่าตรวจพบข้อผิดพลาดเชิงตรรกะระหว่างการรัน circuit หรือไม่ การ postselect เฉพาะตัวอย่างที่ไม่พบข้อผิดพลาดช่วยเพิ่ม fidelity ของการกระจายที่สุ่มได้ โดยแลกกับอัตรา postselection ที่ลดลง ความแตกต่างสำคัญระหว่าง CPC กับ spacetime check คือการวัด syndrome ของ CPC ต้องวัด operator น้ำหนักสูงที่อยู่เฉพาะที่ในเวลา บน topology ของ qubit ที่การเชื่อมต่อจำกัด เช่น heavy hex การ implement check เหล่านี้ต้องใช้ SWAP gate จำนวนมาก ซึ่งมักทำให้ circuit ลึกเกินกว่าจะรันได้จริง การ implement Pauli check เป็น spacetime code ทำให้กระจาย check ไปทั่ว payload circuit ได้ ช่วยให้เข้ารหัส qubit ได้อย่างมีประสิทธิภาพต่อฮาร์ดแวร์ และยังตรวจจับข้อผิดพลาดเชิงตรรกะระหว่างการรัน circuit ได้ดี บทบาทหลักของแพ็กเกจ qiskit-paulice คือการหาและแทรก spacetime Pauli check ที่ใช้ได้ลงในตำแหน่งของ circuit ที่ทั้งเพิ่มประสิทธิผลการตรวจจับข้อผิดพลาดสูงสุดและลด overhead ของ qubit ให้น้อยที่สุด
บทเรียนนี้ implement workflow เพื่อเพิ่ม fidelity ของการกระจายที่สุ่มจาก quantum circuit โดยใช้ spacetime check เนื่องจากเราจะใช้ Clifford circuit เราจึงประมาณ fidelity ได้อย่างตรงไปตรงมาจากค่าเฉลี่ยของค่าคาดหมายของ stabilizer ของ circuit เราจะทำตามขั้นตอนเหล่านี้:
-
สร้าง circuit: Clifford circuit สุ่มแบบ brickwork 1 มิติที่ตื้น ใช้เพื่อแสดงภาพ check
-
ระบุ qubit เป้าหมายสำหรับ check: แมป circuit ไปยัง qubit จริงบน
ibm_bostonHeron QPU และระบุ qubit ที่มี ancilla ข้างเคียง -
Transpile circuit ไปยัง backend: เลือก layout ของ qubit และแปลง gate ของ circuit เป็นชุด gate เนทีฟของ
ibm_boston -
สร้างโมเดลสัญญาณรบกวนจากข้อมูล backend: สร้างโมเดลสัญญาณรบกวนคร่าว ๆ จากข้อมูล benchmark ของ backend ข้อมูลสัญญาณรบกวนนี้จะใช้ให้คะแนนประสิทธิผลของแต่ละ check ที่เพิ่ม
-
แสดงภาพ Pauli check: เพิ่ม Pauli check ลงใน circuit ตื้น เพื่อให้เราเห็นว่า check ถูกเพิ่มอย่างไร
-
กำหนด stabilizer ที่จะวัด: เพิ่มความลึกของ circuit เพื่อเพิ่มผลกระทบของสัญญาณรบกวนจาก gate สุ่มเลือกส่วนย่อยของ stabilizer ของ circuit ที่ลึกขึ้น
-
หา check ที่ดีสำหรับทั้งสถานะ: หาชุด check หนึ่งชุดสำหรับ stabilizer ของสถานะที่ circuit เตรียม check เหล่านี้ใช้ได้ไม่ว่าจะวัดอะไรหลังจากนั้น ดังนั้น circuit ที่มี check ตัวเดียวกันจึงใช้ได้กับทุก stabilizer ที่สุ่มมา
-
สุ่มตัวอย่างจาก circuit ที่มีสัญญาณรบกวน: วัด circuit เปล่าและ circuit ที่มี check ในฐานของแต่ละ stabilizer ที่สุ่มมาด้วย samplomatic และสุ่มตัวอย่างด้วย primitive
Executorบนตัวจำลองที่มีสัญญาณรบกวนซึ่งใช้ depolarizing noise ชนิดเดียวกับที่ใช้หา check -
Postselect ตัวอย่างของ circuit ที่มี check ซึ่งไม่พบข้อผิดพลาด: ตัดตัวอย่างที่ไม่ผ่าน check อย่างน้อยหนึ่งรายการออก คำนวณค่าคาดหมายสำหรับ circuit เปล่าที่มีสัญญาณรบกวนและ circuit ที่มี check
-
เปรียบเทียบ fidelity ของสถานะที่มีสัญญาณรบกวนก่อนและหลัง postselect: แสดงว่าการ postselect เฉพาะตัวอย่างที่ไม่พบข้อผิดพลาดช่วยปรับปรุง fidelity ที่ประมาณได้ของสถานะที่มีสัญญาณรบกวน
1. สร้าง circuit
ก่อนอื่น เราจะสร้าง Clifford circuit ตื้น ๆ ที่มี entangling gate จัดวางแบบการเชื่อมต่อ brickwork เราจะใช้ circuit ขนาดเล็กนี้เพื่อแสดงภาพว่า check ถูกเพิ่มลงใน circuit อย่างไร
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_paulice": "qiskit-paulice", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit
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
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.draw("mpl", fold=-1, scale=0.6)

2. ระบุ qubit เป้าหมายสำหรับ check
ถัดไป เราเลือก layout ของ qubit 1 มิติบน Heron r3 QPU ibm_boston เราเลือก layout ที่ซิกแซกในแนวทแยงผ่าน coupling graph และทำให้จำนวน ancilla qubit ที่ใช้ได้มากที่สุด เพื่อหา ancilla ที่ใช้ได้ เราใช้ฟังก์ชัน get_check_qubits ซึ่งคืนลิสต์ของคู่ target/ancilla qubit Pauli check บน target_qubits[i] จะ implement โดยใช้ ancilla_qubits[i]
ใน coupling graph ด้านล่าง qubit สีเขียวคือ payload qubit และ qubit สีส้มคือ ancilla ที่เราจะใช้ implement check Qubit 68, 69, 89, 91, 111, 113 และ 133 มี ancilla ที่อยู่ติดกัน ดังนั้นเราจะใช้ qubit เหล่านั้น (ตรงกับ virtual qubit 0, 1, 3, 5, 7, 9 และ 11) เป็น target qubit สำหรับ implement check โปรดสังเกตว่า qubit 134 อยู่ติดกับ qubit 133 ใน payload แต่ไม่ได้ไฮไลต์เป็น ancilla เพราะ 133 ถูกเลือก (โดยพลการ) ไปแล้วเพื่อ implement check กับ ancilla 132
from qiskit.visualization import plot_coupling_map
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_paulice.layout import get_check_qubits
service = QiskitRuntimeService()
backend = service.backend("ibm_boston")
# Choose a layout and pair each available target qubit with a neighboring ancilla
layout = [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133]
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)
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,
)
Target qubits: [68, 69, 89, 91, 111, 113, 133]
Ancilla qubits: [67, 70, 88, 92, 110, 114, 132]

3. Transpile circuit ไปยัง backend
เนื่องจากเลือก backend ไว้แล้ว เราจึง transpile virtual circuit เป็น ISA circuit ได้เลย ในที่นี้เราเพียงตั้ง layout ของ qubit และแปลง gate เป็นชุด gate เนทีฟของ ibm_boston
from qiskit.transpiler import generate_preset_pass_manager
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)

4. สร้างโมเดลสัญญาณรบกวนจากข้อมูล backend
เมื่อเรารู้แล้วว่า circuit จะถูกจัดวางบน QPU อย่างไร เราก็สร้างแบบจำลองได้ว่าสัญญาณรบกวนของ gate และการอ่านค่าบน backend จะส่งผลต่อการรัน circuit อย่างไร โมเดลสัญญาณรบกวนใช้กำหนดตำแหน่งที่เหมาะสมที่สุดใน circuit สำหรับวาง Pauli check เราต้องการวาง check ในตำแหน่งที่จับข้อผิดพลาดได้มากที่สุด แม้โมเดลสัญญาณรบกวนที่แม่นยำกว่าจะช่วยให้ check ตรวจจับได้ดีขึ้น แต่โดยปกติไม่จำเป็นต้องเรียนรู้โมเดลสัญญาณรบกวนด้วยการสุ่มตัวอย่าง QPU ในที่นี้เราอนุมานโมเดลสัญญาณรบกวน depolarizing ของ gate แบบสม่ำเสมอโดยใช้เพียงข้อมูล benchmark ของ backend จาก qiskit-ibm-runtime และทิ้งข้อผิดพลาดการอ่านค่าที่รายงานมาด้วย เพราะ check ที่เราหาในขั้นตอนที่ 7 อ่านเฉพาะ ancilla ของมัน ข้อผิดพลาดการอ่านค่าของ payload จึงมองไม่เห็นสำหรับ check และจะมีแต่ทำให้ fidelity หลัง postselect ในขั้นตอนที่ 10 ต่ำลง ดูข้อมูลเพิ่มเติมเกี่ยวกับการระบุสัญญาณรบกวนของ gate และการอ่านค่าในเอกสาร qiskit_paulice.NoiseModel สำหรับผู้ใช้ที่ต้องการใช้โมเดลสัญญาณรบกวน Pauli-Lindblad ที่เรียนรู้มา ดู NoiseModel.from_pauli_lindblad_maps
from dataclasses import replace
from qiskit_paulice.noise_models import NoiseModel
# Model the gate noise as a uniform depolarizing channel based on backend benchmark data
noise_model = replace(
NoiseModel.from_backend(backend, layout, uniform_gate_noise=True),
readout_noise=None,
)
print(noise_model)
NoiseModel(gate_noise=0.0015808382794757693, readout_noise=None, idling_noise=None)
5. แสดงภาพ Pauli check
ตอนนี้ลองเพิ่ม check ลงใน circuit แล้วดูว่าหน้าตาเป็นอย่างไร เราใช้ฟังก์ชัน qiskit_paulice.add_pauli_checks เพื่อหาชุด check ที่ใช้ได้และดีได้ เราส่ง Clifford payload circuit เป็นอินพุต พร้อมลิสต์ payload qubit ที่จะเพิ่ม check (target qubit) และโมเดลสัญญาณรบกวนที่เราสร้างในขั้นตอนก่อนหน้า ในตัวอย่างนี้ เราจะเพิ่ม check ให้ target qubit ทั้ง 7 ตัวที่มี ancilla ใช้ได้ Check ถูกเพิ่มให้ qubit ตามลำดับที่ปรากฏใน target_qubits ซึ่งทำให้ง่ายต่อการอนุมาน layout สุดท้ายของ circuit ที่มี check: final_layout = layout + ancilla_qubits หากผู้ใช้ตัดสินใจว่าต้องการรัน circuit เอาต์พุตตัวใดตัวหนึ่งที่มี check น้อยลง (i) layout สุดท้ายจะเป็น final_layout = layout + ancilla_qubits[:i+1]
การแสดงภาพ circuit แสดงว่า check ถูก implement อย่างถูกต้องโดยใช้คู่ target/ancilla ที่ระบุ
รายละเอียดของ qiskit_paulice.add_pauli_checks:
- อินพุตแรกต้องเป็น Clifford circuit ในที่นี้ไม่มีการวัด และเราส่ง
stabilizers="all"ดังนั้นจะหา check สำหรับ stabilizer ของสถานะที่ circuit เตรียม: check ใช้ได้ถ้า spacetime Pauli product ของมันวัด stabilizer ดังกล่าว syndrome ของมันคือบิต ancilla ของมันเพียงอย่างเดียว และยังใช้ได้ไม่ว่าจะวัด payload ในฐานใดหลังจากนั้น นี่คือสิ่งที่การประมาณ fidelity ต้องการ เพราะ stabilizer ที่สุ่มมาแต่ละตัวถูกวัดในฐานที่ต่างกัน (ถ้าไม่ระบุstabilizersจะหา check สำหรับการวัดปลายทางของ circuit แทน) - อาร์กิวเมนต์ตำแหน่งที่สองคือลิสต์ดัชนี target qubit แต่ละ target qubit จะใช้ entangling gate ร่วมกับ ancilla qubit เพื่อสร้าง check Check จะถูกหาและเพิ่มลงใน circuit ตามลำดับที่ให้ไว้ในอาร์กิวเมนต์นี้
- อาร์กิวเมนต์ตำแหน่งสุดท้ายคือโมเดลสัญญาณรบกวนที่แทนสัญญาณรบกวนที่คาดว่าจะส่งผลต่อ circuit ระหว่างการรัน โมเดลนี้ใช้ประเมินประสิทธิผลของ check ที่กำหนด
ancilla_qubits: หาก circuit อินพุตมี layout kwarg นี้บอกadd_pauli_checksว่าจะกำหนด qubit จริงให้ ancilla ที่เพิ่มใหม่อย่างไรancilla_qubitsควรยาวเท่ากับtarget_qubits(อาร์กิวเมนต์ตำแหน่งที่ 2) และควรใช้ancilla_qubits[i]implement check บนtarget_qubits[i]cost: ฟังก์ชันต้นทุนที่ใช้ประเมิน check เป็นได้ทั้งgammaหรือLER(อัตราข้อผิดพลาดเชิงตรรกะ) ซึ่งล้วนเป็นวิธีอธิบายว่า noise channel ส่วนใดที่ check ตรวจไม่พบ เมื่อใช้stabilizers="all"ข้อผิดพลาดนับเป็นเชิงตรรกะหากทำให้สถานะที่เตรียมเปลี่ยนไป ซึ่งตรงกับสิ่งที่ fidelity วัดพอดีmethod: วิธีที่ใช้หา check ที่ใช้ได้ เราใช้windowedซึ่งสุ่มตัวอย่างส่วนย่อยของหน้าต่างเชิงพื้นที่จำนวนมากจาก support (wire) ของ target qubit ค่าอื่นที่ใช้ได้คือgeneticและwindowed_genetic- เอาต์พุตของ
add_pauli_checksมี circuit ที่มีจำนวน check เพิ่มขึ้นเรื่อย ๆ ตั้งแต่ circuit ที่ไม่มี check ไปจนถึง circuit ที่มี check บนทุก target qubit (โดยสมมติว่าหา check ที่ใช้ได้เจอ)
ดู ส่วนที่ II-IV ของข้อมูลเสริม สำหรับรายละเอียดการหา spacetime Pauli check ที่ดี
import matplotlib.pyplot as plt
from qiskit_paulice import add_pauli_checks
# Find checks on circuit
checked = add_pauli_checks(
circuit_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(f"Physical layout of payload+ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)
Physical layout of payload+ancillas: [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133, 67, 70, 88, 92, 110, 114, 132]
Checked circuit:

6. กำหนด stabilizer ที่จะวัด
เพื่อทดสอบประสิทธิภาพของการตรวจจับข้อผิดพลาด เราจะประมาณ fidelity ของสถานะ stabilizer ที่ circuit เตรียมในอุดมคติ เทียบกับสถานะที่มีสัญญาณรบกวน ที่ฮาร์ดแวร์ส่งออกจริง โปรเจกเตอร์ไปยังสถานะ stabilizer บริสุทธิ์ นิยามเป็นค่าเฉลี่ยสม่ำเสมอของสมาชิกทั้ง ตัวของ stabilizer group ของมัน:
แทนค่านี้ลงในสมการ fidelity เราเห็นว่า fidelity ของสถานะที่มีสัญญาณรบกวน เท่ากับค่าคาดหมายเฉลี่ยของ stabilizer ทั้งหมด เทียบกับ :
สำหรับปัญหาที่ใหญ่ขึ้น การวัด (หรือแม้แต่การไล่นับ) stabilizer ทั้ง ตัวเป็นไปไม่ได้ ดังนั้นมักประมาณ fidelity โดยวัดตัวอย่างสุ่มของ stabilizer การสุ่ม stabilizer จาก อย่างสม่ำเสมอให้ตัวประมาณค่า fidelity ที่ไม่เอนเอียง
สำหรับการทดลองนี้ เราจะทำให้ circuit ลึกขึ้นมากเพื่อเสริมผลของสัญญาณรบกวนจาก gate และเราจะเพิ่ม check ให้ทุก qubit ที่มี ancilla qubit ข้างเคียง แม้จะมีวิธีที่ซับซ้อนกว่าในการสุ่ม stabilizer เพื่อประมาณ fidelity แต่ในตัวอย่างนี้เราจะสุ่ม stabilizer ตัวด้วยความน่าจะเป็นสม่ำเสมอ
from qiskit.quantum_info import Clifford, Pauli, PauliList
num_stabilizers = 100
num_shots = 1_000
num_checks = len(target_qubits)
depth = 72
circuit = random_clifford_circuit(num_qubits, depth, rng)
# Create all 2^N stabilizers
stabilizers = PauliList([Pauli("I" * num_qubits)])
for generator in (
Pauli(label) for label in Clifford(circuit).to_labels(mode="S")
):
stabilizers = stabilizers + stabilizers.compose(generator)
# Choose some random stabilizers from the set
keep = np.where(stabilizers.x.any(axis=1) | stabilizers.z.any(axis=1))[0]
chosen = np.random.default_rng(seed).choice(
keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = stabilizers[chosen]
depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
f"Sampled {len(stabilizers)} stabilizers of {circuit.num_qubits}-qubit circuit with two-qubit depth of {depth}: {{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)
Sampled 100 stabilizers of 12-qubit circuit with two-qubit depth of 72: {-YIIYZZZIZIYY, -XIXYIIZXYIZX, ...}
7. หา check ที่ดีสำหรับทั้งสถานะ
เราหาชุด check หนึ่งชุดสำหรับ circuit ลึกเหมือนในขั้นตอนที่ 5 ทุกประการ เนื่องจาก check อ่านเฉพาะ ancilla ของมัน circuit ที่มี check ตัวเดียวกันจึงใช้ได้กับทุก stabilizer ที่สุ่มมา circuit ที่คืนมาวัดเฉพาะ ancilla ของ check ดังนั้นเราจะเพิ่มการวัด payload ในขั้นตอนถัดไป
import time
# Transpile the deep payload without measurements and find one set of checks for the whole state
payload_isa = pm.run(circuit)
t0 = time.time()
checked = add_pauli_checks(
payload_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(
f"Added {len(checked[-1].check_qubits)} checks in {time.time() - t0:.0f}s."
)
print(
f"2-qubit depth increased from {payload_isa.depth(lambda x: len(x.qubits) == 2)} to "
f"{checked[-1].circuit.depth(lambda x: len(x.qubits) == 2)} when adding {num_checks} checks."
)
Added 7 checks in 3s.
2-qubit depth increased from 72 to 85 when adding 7 checks.
8. สุ่มตัวอย่างจาก circuit ที่มีสัญญาณรบกวน
เราสุ่มตัวอย่างทุกตัวแปร ตั้งแต่ payload เปล่าไปจนถึง circuit ที่มี check ครบทั้ง 7 ตัว สำหรับแต่ละ stabilizer ที่สุ่มมา ตัวแปรที่ไม่มี check เลยคือ payload เปล่าที่มีการวัดเหมือนกัน จึงใช้เป็นข้อมูลอ้างอิงที่มีสัญญาณรบกวน แทนที่จะต่อการหมุนฐานของทุก stabilizer เข้ากับสำเนาของทุกตัวแปร เราให้ samplomatic เลือกฐานการวัดตอนสุ่มตัวอย่าง สำหรับแต่ละตัวแปร เราทำดังนี้:
-
วัด payload และจัดกลุ่มการวัดปลายทางเข้าในกล่องด้วย boxing pass manager ของ samplomatic กล่องนี้มี annotation
ChangeBasisดังนั้นbuildจึงคืน template circuit ที่ gate เปลี่ยนฐานถูกทำให้เป็นพารามิเตอร์ พร้อม samplex ที่คำนวณพารามิเตอร์สำหรับฐานที่ร้องขอ -
เพิ่ม template และ samplex ลงใน
QuantumProgramsamplex รับฐานการวัดหนึ่งฐานต่อ qubit ในกล่อง เรียงตามลำดับ qubit จริงจากน้อยไปมาก เข้ารหัสเป็น , , , การส่งอาร์เรย์ของฐานที่มีแกนนำยาว100ทำให้ item เดียววัด stabilizer ทั้ง100ตัว: payload ในฐานของแต่ละ stabilizer และ ancilla ใน Qubit ที่อยู่นอก support ของ stabilizer ก็ถูกวัดใน เช่นกัน และบิตของมันจะถูกละเว้นเมื่อคำนวณค่าคาดหมายของ stabilizer
primitive Executor จาก qiskit-ibm-runtime รันทั้งโปรแกรมในงานเดียว เมื่อให้ AerSimulator แทน QPU มันจะรันในเครื่องบนตัวจำลองนั้น เราจึงส่งตัวจำลองที่มี depolarizing noise ของ gate แบบเดียวกับที่ใช้หา check ให้มัน และใช้วิธี stabilizer เนื่องจากทุก circuit เป็น Clifford ต้องใช้ Qiskit Aer เพียงเพื่อสร้างตัวจำลองนี้ โหมดในเครื่องรับ seed จากตัวเลือกของ Executor แต่ละเว้น noise_model ของมัน ดังนั้นต้องตั้งสัญญาณรบกวนบนตัวจำลองเอง หากส่ง QPU backend แทนก็จะรันโปรแกรมเดียวกันบนฮาร์ดแวร์ เมื่อการจำลองเสร็จ เราจะคำนวณอัตรา postselection และค่าคาดหมายเพื่อดูว่าการตรวจจับข้อผิดพลาดทำงานได้ดีแค่ไหนก่อนแสดงภาพข้อมูล
from qiskit import ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import depolarizing_error
from qiskit_ibm_runtime import Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from samplomatic.builders import build
from samplomatic.transpiler import generate_boxing_pass_manager
# Group the terminal measurements into a box whose measurement basis is chosen at sampling time
boxing_pm = generate_boxing_pass_manager(
enable_gates=False, measure_annotations="change_basis"
)
# bases[i, q]: basis in which physical qubit q is measured for stabilizer i (I=0, Z=1, X=2, Y=3)
bases = np.ones((len(stabilizers), backend.num_qubits), dtype=np.uint8)
bases[:, layout] = stabilizers.z + 2 * stabilizers.x
program = QuantumProgram(shots=num_shots)
for variant in checked:
circ = variant.circuit.copy()
circ.add_register(meas := ClassicalRegister(num_qubits, "meas"))
circ.measure(layout, meas)
template, samplex = build(boxing_pm.run(circ))
(basis_input,) = samplex.inputs().get_specs()
measured_qubits = sorted(layout + list(variant.check_qubits))
program.append_samplex_item(
template,
samplex=samplex,
samplex_arguments={basis_input.name: bases[:, measured_qubits]},
shape=(len(stabilizers),),
)
# Simulate the program with the same gate noise we used to find the checks
aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)
# outs[k][register]: bits of the variant with k checks, shape (stabilizers, shots, bits)
t0 = time.time()
outs = (
Executor(noisy_sim, options={"simulator": {"seed_simulator": seed}})
.run(program)
.result()
)
print(
f"Sampled {len(checked)} circuits for {len(stabilizers)} stabilizers in {time.time() - t0:.0f}s."
)
Sampled 8 circuits for 100 stabilizers in 77s.
9. Postselect ตัวอย่างของ circuit ที่มี check
ในแพ็กเกจนี้ check ถูก implement โดยใช้ entangling gate ระหว่าง ancilla qubit หนึ่งตัวกับ target qubit หนึ่งตัว Ancilla แต่ละตัวเริ่มใน ดังนั้น จึง stabilize สถานะอินพุตของมัน การ propagate ไปข้างหน้าจากจุดเริ่มต้นของ ancilla ผ่านทั้ง circuit ที่มี check ให้ คูณกับ stabilizer ของสถานะที่เตรียม ดังนั้น check ผ่านเมื่อบิต ancilla เป็น ตัวอย่างจะถูกเก็บไว้หากทุก check ผ่าน
circuit ที่มี check วัด ancilla ลงใน register checks_c ดังนั้นตัวอย่างจะถูกเก็บไว้หากทุกบิตของ register นั้นเป็น 0 โดยทั่วไป เมธอด get_postselection_method ของอินสแตนซ์ CheckedCircuit ที่ได้จาก add_pauli_checks จะแมป bitstring ของแต่ละ shot ไปเป็น syndrome ของทุก check พร้อมกัน สำหรับ check ที่หามาเพื่อ stabilizer ของสถานะที่เตรียม แต่ละ syndrome ก็คือบิต ancilla ของมัน เราจึงอ่าน register โดยตรง
ในแผนภูมิ เราเห็นว่าการเพิ่ม check มากขึ้นทำให้อัตรา postselection ลดลง แม้เป้าหมายของการตรวจจับข้อผิดพลาดคือจับข้อผิดพลาดให้ได้มากที่สุด แต่เมื่ออัตรา postselection ลดลง อาจต้องใช้ shot มากขึ้นเพื่อให้ได้ความแม่นยำเป้าหมาย หากอัตรา postselection ต่ำเกินไป จำนวน shot ที่ต้องใช้เพื่อเก็บตัวอย่างเชิงตรรกะหนึ่งตัวอาจสูงจนทำไม่ได้ ในตัวอย่างนี้ อัตรา postselection ดูเหมือนจะลู่เข้า ซึ่งบ่งว่า check เพิ่มเติมช่วยเพิ่มความสามารถในการตรวจจับน้อยลง แม้การเพิ่ม Pauli check มากขึ้นโดยทั่วไปจะให้ความสามารถตรวจจับข้อผิดพลาดมากขึ้น แต่ก็มาพร้อมต้นทุนคือความลึกของ circuit ที่เพิ่มขึ้นและต้นทุนการสุ่มตัวอย่างที่เพิ่มขึ้น
def keep_mask(out):
"""Per-shot True/False: every ancilla read 0, so every Pauli check passed."""
if "checks_c" not in out: # the bare payload has no checks
return np.ones(out["meas"].shape[:-1], dtype=bool)
return ~out["checks_c"].any(axis=-1)
# masks[k][i]: kept shots of stabilizer i measured on the variant with k checks
masks = [keep_mask(out) for out in outs]
psrs_arr = np.array([mask.mean(axis=-1) for mask in masks]).T
ks = np.arange(psrs_arr.shape[1])
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, psrs_arr.T, color="darkorange", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.median(psrs_arr, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set(
xlabel="Checks committed",
ylabel="Post-selection rate",
ylim=(0, 1.05),
title=f"Per-stabilizer post-selection rate ({len(psrs_arr)} stabilizers)",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

10. เปรียบเทียบ fidelity ของสถานะที่มีสัญญาณรบกวนก่อนและหลัง postselect
เราเห็นว่าการ postselect เฉพาะตัวอย่างที่ไม่พบข้อผิดพลาดช่วยปรับปรุงค่าคาดหมายของทุก stabilizer ได้มาก และจึงช่วยปรับปรุง fidelity ที่ประมาณได้ของสถานะ stabilizer แม้ค่าหลัง postselect จะคำนวณจากเฉพาะตัวอย่างที่ผ่านทุก check แต่ค่าคาดหมายแม่นยำกว่ามากและความแปรปรวนของตัวอย่างต่ำกว่า ตอนนี้เรายังเห็นด้วยว่าอัตรา postselection ข้างต้นแท้จริงแล้วลู่เข้าสู่ค่าที่ใกล้เคียงกับ fidelity ของ circuit ที่มีสัญญาณรบกวน อัตรา postselection ลู่เข้าเพราะ check จับข้อผิดพลาดส่วนใหญ่ที่เกิดขึ้นจริงระหว่างการรัน circuit ได้
def expectation_values(out, mask):
"""EV of each stabilizer from the shots of ``out`` kept by ``mask``.
Bit q of ``meas`` is virtual qubit q, so a stabilizer's eigenvalue in a shot is the parity of
the bits in its support, times its sign.
"""
support = stabilizers.x | stabilizers.z
parity = (out["meas"] & support[:, None, :]).sum(axis=-1) % 2
signs = np.where(stabilizers.phase == 2, -1.0, 1.0)
eigenvalues = np.where(parity, -1.0, 1.0) * signs[:, None]
kept = mask.sum(axis=-1)
return np.where(
kept > 0,
(eigenvalues * mask).sum(axis=-1) / np.maximum(kept, 1),
np.nan,
)
evs_noisy = expectation_values(outs[0], masks[0])
evs_post = expectation_values(outs[-1], masks[-1])
print(
f"ideal fidelity: 1.0\n"
f"noisy fidelity: {np.nanmean(evs_noisy):.4f}\n"
f"postselected fidelity: {np.nanmean(evs_post):.4f}\n"
f"mean post-sel rate: {masks[-1].mean():.3f}\n"
)
evs_ideal = np.ones(len(stabilizers))
idx = np.arange(len(stabilizers))
def cum_mean_sem(ys):
valid = ~np.isnan(ys)
s = np.cumsum(np.where(valid, ys, 0.0))
s2 = np.cumsum(np.where(valid, ys**2, 0.0))
n = np.maximum(np.cumsum(valid).astype(float), 1)
mean = s / n
sem = np.sqrt(np.maximum(s2 / n - mean**2, 0) / n)
return np.where(np.cumsum(valid) > 0, mean, np.nan), sem
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 ± std"
)
ax.axhline(m, color=color, linewidth=1, linestyle="--")
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", "post-sel")
ax.scatter(
idx, evs_noisy, color="red", s=22, alpha=0.7, zorder=3, label="noisy <G>"
)
ax.scatter(
idx,
evs_post,
color="green",
s=22,
alpha=0.7,
zorder=3,
label="post-sel <G>",
)
ax.set(
xlabel="stabilizer index",
ylabel=r"$\langle G \rangle$",
ylim=(-0.1, 1.1),
title="Per-stabilizer EVs",
)
ax.legend(loc="lower left", ncol=2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
M = np.arange(1, len(stabilizers) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
(evs_ideal, "black", "ideal"),
(evs_noisy, "red", "noisy"),
(evs_post, "green", "post-sel"),
]:
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",
ylabel="running fidelity estimate",
title="Fidelity convergence vs. number of stabilizers",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ideal fidelity: 1.0
noisy fidelity: 0.5582
postselected fidelity: 0.9747
mean post-sel rate: 0.535

เนื่องจากชุด check เดียวใช้ได้กับทุก stabilizer จึงมีเส้นโค้ง gamma เพียงเส้นเดียวให้ดู: ค่าของฟังก์ชันต้นทุนหลังจากแต่ละ check ถูกเพิ่ม สำหรับปัญหาอื่นบนชุด qubit จริงที่ต่างกัน และเมื่อโมเดลสัญญาณรบกวนไม่สมบูรณ์ (โปรดจำว่าโมเดลสัญญาณรบกวนในบทเรียนนี้จำลองสัญญาณรบกวนที่กระทบตัวอย่างได้ตรงพอดี) ลำดับที่เพิ่ม check ลงใน target qubit อาจมีผลต่อการลู่เข้าของเส้นโค้งชัดเจนกว่านี้
เส้นโค้ง gamma ที่ลู่เข้าสู่ 1.0 บ่งว่า check เพิ่มเติมจะตรวจจับข้อผิดพลาดเพิ่มได้ไม่มากนัก เพราะมีข้อผิดพลาดที่จำลองไว้เหลือที่ check ไม่ครอบคลุมน้อยมาก สิ่งนี้ตอกย้ำข้อสังเกตก่อนหน้าของเราที่ว่าการลู่เข้าของอัตรา postselection เกิดจากการที่ใน circuit ไม่มีข้อผิดพลาดที่ยังไม่ถูกครอบคลุมเหลืออยู่
costs = [variant.cost for variant in checked]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(costs)), costs, color="steelblue", marker="o")
ax.set(
xlabel="Checks committed",
ylabel="Gamma",
yscale="log",
title="Gamma of the noise left uncovered by the checks",
)
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()