Mababang-overhead na pagdetekta ng error gamit ang spacetime codes
Tinatayang paggamit: 10 segundo sa isang Heron r3 processor (PAALALA: Tantiya lamang ito. Maaaring mag-iba ang iyong aktwal na oras ng pagpapatakbo.)
Panimula
Ang Low-overhead error detection with spacetime codes [1] nina Simon Martiel at Ali Javadi-Abhari ay nagmumungkahi ng pagbuo ng mababang-timbang, connectivity-aware na spacetime checks para sa Clifford-dominated na mga circuit, pagkatapos ay post-selecting sa mga check na ito upang mahuli ang mga pagkakamali nang may mas kaunting overhead kaysa sa buong error correction at mas kaunting shots kaysa sa karaniwang error mitigation.
Nagmumungkahi ang papel na ito ng bagong paraan para sa pagdetekta ng error sa mga quantum circuit (partikular na ang Clifford circuits) na nagtatagpo sa pagitan ng buong error correction at mga mas magaang na mitigation na pamamaraan. Ang pangunahing ideya ay ang paggamit ng spacetime codes upang makabuo ng mga "check" sa buong circuit na kayang mahuli ang mga error, nang may mas mababang qubit at gate overhead kaysa sa buong fault-tolerant na error correction. Nagdisenyo ang mga may-akda ng mahusay na mga algorithm upang pumili ng mga check na mababang-timbang (kinasasangkutan ng ilang qubits), katugma sa pisikal na connectivity ng device, at sumasaklaw sa malalawak na temporal at spatial na bahagi ng circuit. Ipinakita nila ang pamamaraan sa mga circuit na may hanggang 50 logical qubits at ~2450 na CZ gates, na nakamit ang physical-to-logical fidelity gains na hanggang 236x. Tandaan din na habang nagsasama ng mas maraming non-Clifford na operasyon ang mga circuit, ang bilang ng mga wastong check ay bumababa nang exponensyal, na nagpapahiwatig na pinakamabisa ang pamamaraan para sa Clifford-dominated na mga circuit. Sa kabuuan, sa malapit na hinaharap, ang pagdetekta ng error sa pamamagitan ng spacetime codes ay maaaring mag-alok ng praktikal at mas mababang-overhead na ruta tungo sa pagpapabuti ng pagiging maaasahan ng quantum hardware.
Ang teknikong ito ng pagdetekta ng error ay umaasa sa konsepto ng coherent Pauli checks at batay sa gawa na Single-shot error mitigation by coherent Pauli checks [2] nina van den Berg et al.
Kamakailan, ang papel na Big cats: entanglement in 120 qubits and beyond [3] nina Javadi-Abhari et al. ay nag-uulat ng paglikha ng isang 120-qubit na Greenberger-Horne-Zeilinger (GHZ) state, ang pinakamalaking multipartite entangled state na nagawa hanggang ngayon sa isang superconducting-qubit na platform. Gamit ang isang hardware-aware compiler, mababang-overhead na pagdetekta ng error, at isang "temporary uncomputation" na teknik upang mabawasan ang ingay, ang mga mananaliksik ay nakamit ang isang fidelity na 0.56 ± 0.03 na may halos 28% na kahusayan sa post-selection. Ipinakita ng gawaing ito ang tunay na entanglement sa lahat ng 120 qubits, na nagpapatunay ng maraming paraan ng fidelity-certification, at isang pangunahing benchmark para sa scalable na quantum hardware.
Ang tutorial na ito ay nagtatayo sa mga ideyang ito, ginagabayan ka sa pagpapatupad ng error detection algorithm una sa isang maliit na random Clifford circuit at pagkatapos sa gawaing paghahanda ng GHZ state, upang matulungan kang mag-eksperimento sa pagdetekta ng error sa iyong sariling mga quantum circuit.
Mga Kinakailangan
Bago simulan ang tutorial na ito, tiyaking naka-install ang mga sumusunod:
- Qiskit SDK v2.0 o mas bago, na may suporta sa visualization
- Qiskit Runtime v0.40 o mas bago (
pip install qiskit-ibm-runtime) - Qiskit Aer v0.17.2 (
pip install qiskit-aer) - Qiskit Device Benchmarking (
pip install "qiskit-device-benchmarking @ git+https://github.com/qiskit-community/qiskit-device-benchmarking.git") - NumPy v2.3.2 (
pip install numpy) - Matplotlib v3.10.7 (
pip install matplotlib)
Setup
# Added by doQumentation — installs packages not in the Binder environment
%pip install -q qiskit-device-benchmarking
# Standard library imports
from collections import defaultdict, deque
from functools import partial
# External libraries
import matplotlib.pyplot as plt
import numpy as np
# Qiskit
from qiskit import ClassicalRegister, QuantumCircuit
from qiskit.circuit import Delay
from qiskit.circuit.library import RZGate, XGate
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit.quantum_info import Pauli, random_clifford
from qiskit.transpiler import AnalysisPass, PassManager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
CollectAndCollapse,
PadDelay,
PadDynamicalDecoupling,
RemoveBarriers,
)
from qiskit.transpiler.passes.optimization.collect_and_collapse import (
collect_using_filter_function,
collapse_to_operation,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.visualization import plot_histogram
# Qiskit Aer
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, ReadoutError, depolarizing_error
# Qiskit IBM Runtime
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
# Qiskit Device Benchmarking
from qiskit_device_benchmarking.utilities.gate_map import plot_gate_map
Unang Halimbawa
Upang ipakita ang pamamaraang ito, magsisimula tayo sa pagbuo ng isang simpleng Clifford circuit. Ang aming layunin ay makadetekta ng ilang uri ng error na nagaganap sa circuit na ito, upang maitapon namin ang mga maling resulta ng pagsukat. Sa terminolohiya ng pagdetekta ng error, ito rin ay kilala bilang aming payload circuit.
circ = random_clifford(num_qubits=2, seed=11).to_circuit()
circ.draw("mpl")
Ang aming layunin ay maglagay ng coherent Pauli check sa payload circuit na ito. Ngunit bago gawin iyon, hinahati namin ang circuit na ito sa mga layer. Magiging kapaki-pakinabang ito sa paglaon kapag naglalagay ng mga Pauli gate sa pagitan.
# Separate circuit into layers
dag = circuit_to_dag(circ)
circ_layers = []
for layer in dag.layers():
layer_as_circuit = dag_to_circuit(layer["graph"])
circ_layers.append(layer_as_circuit)
# Create subplots
fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(1, 5, figsize=(10, 4))
# Draw circuits on respective axes
circ_layers[0].draw(output="mpl", ax=ax1)
circ_layers[1].draw(output="mpl", ax=ax2)
circ_layers[2].draw(output="mpl", ax=ax3)
circ_layers[3].draw(output="mpl", ax=ax4)
circ_layers[4].draw(output="mpl", ax=ax5)
# Adjust layout to prevent overlap
plt.tight_layout()
plt.show()
Handa na tayo ngayon na magdagdag ng mga coherent Pauli check sa payload circuit. Upang magawa ito, kailangan nating bumuo ng isang "wastong check" at ilagay ito sa circuit. Ang isang "check" sa kasong ito ay isang operator na kayang ipahiwatig kung nagkaroon ng error sa circuit sa pamamagitan ng pagsukat sa isang ancilla qubit. Ito ay itinuturing na wastong check kapag ang mga karagdagang operator na inilagay sa quantum circuit ay hindi lohikal na nagbabago ng orihinal na circuit.
Ang check na ito ay kayang matukoy ang mga uri ng error na anticommute dito, at ang check ay magdudulot ng pagsukat ng state sa ancilla qubit sa halip na sa pamamagitan ng phase kickback. Kaya naman, makakaya nating itapon ang mga sukat kung saan may sinyales ng error.
Sa pangkalahatan, ang mga coherent Pauli check ay controlled-Pauli operators na inilalagay sa mga "wire" - mga spacetime location sa pagitan ng mga gate. Ang ancilla qubit na responsable sa pagsenyales ng error ay ang control qubit.
Sa ibaba, bumubuo tayo ng wastong check para sa Clifford circuit na ginawa namin sa itaas. Maaari naming ipakita na ang check na ito ay hindi nagbabago ng operasyon ng circuit sa pamamagitan ng pagpapakita na kapag ang mga Pauli check na ito ay pinaalis sa harap ng circuit, magkakansela sila. Madaling ipakita ito dahil ang isang Pauli operator sa pamamagitan ng isang Clifford gate ay isa pang Pauli operator.
Sa pangkalahatan, maaaring gumamit ng decoding heuristic gaya ng nakabalangkas sa [1] upang matukoy ang mga wastong check. Para sa layunin ng aming unang halimbawa, maaari rin kaming bumuo ng mga wastong check gamit ang mga analytical na kondisyon ng multiplikasyon ng Pauli at Clifford gate.
# Define a valid check
pauli_1 = Pauli("ZI")
pauli_2 = Pauli("XZ")
circ_1 = circ_layers[0].compose(circ_layers[1])
circ_1.draw("mpl")
pauli_1_ev = pauli_1.evolve(circ_1, frame="h")
pauli_1_ev
Pauli('-ZI')
circ_2 = circ.copy()
circ_2.draw("mpl")
pauli_2_ev = pauli_2.evolve(circ_2, frame="h")
pauli_2_ev
Pauli('-ZI')
pauli_1_ev.dot(pauli_2_ev)
Pauli('II')
Gaya ng makikita natin, mayroon tayong wastong check, dahil ang mga inilagay na Pauli operator ay may parehong epekto lamang ng isang identity operator sa circuit. Maaari na tayong maglagay ng mga check na ito sa circuit gamit ang isang ancilla qubit. Ang ancilla qubit na ito, o ang check qubit, ay nagsisimula sa state. Kasama nito ang mga controlled na bersyon ng mga Pauli operasyon na nakabalangkas sa itaas at sa wakas ay sinusukat sa basis. Ang check qubit na ito ay kayang makuha ang mga error sa payload circuit nang hindi lohikal na binabago ito. Ito ay dahil ang ilang uri ng ingay sa payload circuit ay magbabago sa estado ng check qubit, at ito ay masusukat bilang "1" sa halip na "0" kung sakaling magkaroon ng ganitong error.
# New circuit with 3 qubits (2 payload + 1 ancilla for check)
circ_meas = QuantumCircuit(3)
circ_meas.h(0)
circ_meas.compose(circ_layers[0], [1, 2], inplace=True)
circ_meas.compose(circ_layers[1], [1, 2], inplace=True)
circ_meas.cz(0, 2)
circ_meas.compose(circ_layers[2], [1, 2], inplace=True)
circ_meas.compose(circ_layers[3], [1, 2], inplace=True)
circ_meas.compose(circ_layers[4], [1, 2], inplace=True)
circ_meas.cz(0, 1)
circ_meas.cx(0, 2)
circ_meas.h(0)
# Add measurement to payload qubits
c0 = ClassicalRegister(2, name="c0")
circ_meas.add_register(c0)
circ_meas.measure(1, c0[0])
circ_meas.measure(2, c0[1])
# Add measurement to check qubit
c1 = ClassicalRegister(1, name="c1")
circ_meas.add_register(c1)
circ_meas.measure(0, c1[0])
# Visualize the final circuit with the inserted checks
circ_meas.draw("mpl")
Kung ang check qubit ay nasukat bilang "0", pinapanatili namin ang sukat na iyon. Kung ito ay nasukat bilang "1", nangangahulugan ito na nagkaroon ng error sa payload circuit, at itatapon namin ang sukat na iyon.
# Noiseless simulation using stabilizer method
sim_stab = AerSimulator(method="stabilizer")
res = sim_stab.run(circ_meas, shots=1000).result()
counts_noiseless = res.get_counts()
print(f"Stabilizer simulation result: {counts_noiseless}")
Stabilizer simulation result: {'0 11': 523, '0 01': 477}
# Plot the noiseless results
# Note that the first bit in the key corresponds to the check qubit
plot_histogram(counts_noiseless)
Pansinin na sa isang ideal na simulator, hindi nakakakita ng anumang error ang check qubit. Nagpapakilala na tayo ngayon ng noise model sa simulation at titingnan kung paano nakukuha ng check qubit ang mga error.
# Qiskit Aer noise model
noise = NoiseModel()
p2 = 0.003 # two-qubit depolarizing per CZ
p1 = 0.001 # one-qubit depolarizing per 1q Clifford
pr = 0.01 # readout bit-flip probability
# 1q depolarizing on common 1q gates
e1 = depolarizing_error(p1, 1)
for g1 in ["id", "rz", "sx", "x", "h", "s"]:
noise.add_all_qubit_quantum_error(e1, g1)
# 2q depolarizing on CZ
e2 = depolarizing_error(p2, 2)
noise.add_all_qubit_quantum_error(e2, "cz")
# Readout error on measure
ro = ReadoutError([[1 - pr, pr], [pr, 1 - pr]])
noise.add_all_qubit_readout_error(ro)
# Qiskit Aer simulation with noise model
aer = AerSimulator(method="automatic", seed_simulator=43210)
job = aer.run(circ_meas, shots=1000, noise_model=noise)
result = job.result()
counts_noisy = result.get_counts()
print(f"Noise model simulation result: {counts_noisy}")
Noise model simulation result: {'1 01': 5, '0 11': 478, '1 11': 6, '1 00': 2, '1 10': 1, '0 01': 500, '0 00': 5, '0 10': 3}
plot_histogram(counts_noisy)
Gaya ng makikita natin, ilang mga sukat ang nakuha ang error sa pamamagitan ng pagmamarka ng check qubit bilang "1", na makikita sa huling apat na hanay. Ang mga shot na ito ay itatapon. Paalala: Ang ancilla qubit ay maaari ring magdulot ng mga bagong error sa circuit. Upang mabawasan ang epekto nito, maaari tayong maglagay ng mga nested check na may karagdagang ancilla qubits sa quantum circuit.
Halimbawa sa Totoong Mundo: Maghanda ng GHZ State sa Tunay na Hardware
Hakbang 1: Isalin ang mga Classical na Input sa isang Quantum na Problema
Ipinakita na namin ngayon ang isang mahalagang gawain para sa mga quantum computing algorithm, at ito ay ang paghahanda ng GHZ state. Ipapakita namin kung paano gawin ito sa isang tunay na backend gamit ang pagdetekta ng error.
# Set optional seed for reproducibility
SEED = 1
if SEED:
np.random.seed(SEED)
Ang error detection algorithm para sa paghahanda ng GHZ state ay iginagalang ang hardware topology. Nagsisimula tayo sa pagpili ng nais na hardware.
# This is used to run on real hardware
service = QiskitRuntimeService()
# Choose a backend to build GHZ on
backend_name = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
backend = service.backend(backend_name)
coupling_map = backend.target.build_coupling_map()
Ang isang GHZ state sa qubits ay tinukoy bilang
Ang isang napaka-simpleng paraan upang ihanda ang GHZ state ay ang pumili ng isang root qubit na may paunang Hadamard gate, na naglalagay ng qubit sa isang pantay na superposition state, at pagkatapos ay i-entangle ang qubit na ito sa bawat isa pang qubit. Hindi ito isang magandang pamamaraan, dahil nangangailangan ito ng mahabang-distansya at malalim na CNOT na interaksyon. Sa tutorial na ito, gagamitin namin ang maraming teknik kasabay ng pagdetekta ng error upang mapagkakatiwalaang ihanda ang GHZ state sa tunay na hardware.
Hakbang 2: I-optimize ang problema para sa pagpapatakbo sa quantum hardware
I-map ang GHZ state sa hardware
Una, naghahanap tayo ng isang root para i-map ang GHZ circuit sa hardware. Tinatanggal natin ang mga edge/node na ang kanilang mga CZ error, measurement error, at T2 na halaga ay mas malala kaysa sa mga threshold sa ibaba. Hindi sila isasama sa GHZ circuit.
def bad_cz(target, threshold=0.01):
"""Return list of edges whose CZ error is worse than threshold."""
undirected_edges = []
for edge in backend.target.build_coupling_map().get_edges():
if (edge[1], edge[0]) not in undirected_edges:
undirected_edges.append(edge)
edges = undirected_edges
cz_errors = {}
for edge in edges:
cz_errors[edge] = target["cz"][edge].error
worst_edges = sorted(cz_errors.items(), key=lambda x: x[1], reverse=True)
return [list(edge) for edge, error in worst_edges if error > threshold]
def bad_readout(target, threshold=0.01):
"""Return list of nodes whose measurement error is worse than threshold."""
meas_errors = {}
for node in range(backend.num_qubits):
meas_errors[node] = target["measure"][(node,)].error
worst_nodes = sorted(
meas_errors.items(), key=lambda x: x[1], reverse=True
)
return [node for node, error in worst_nodes if error > threshold]
def bad_coherence(target, threshold=60):
"""Return list of nodes whose T2 value is lower than threshold."""
t2s = {}
for node in range(backend.num_qubits):
t2 = target.qubit_properties[node].t2
t2s[node] = t2 * 1e6 if t2 else 0
worst_nodes = sorted(t2s.items(), key=lambda x: x[1])
return [node for node, val in worst_nodes if val < threshold]
THRESH_CZ = 0.025 # exclude from BFS those edges whose CZ error is worse than this threshold
THRESH_MEAS = 0.15 # exclude from BFS those nodes whose measurement error is worse than this threshold
THRESH_T2 = 10 # exclude from BFS those nodes whose T2 value is lower than this threshold
bad_edges = bad_cz(backend.target, threshold=THRESH_CZ)
bad_nodes_readout = bad_readout(backend.target, threshold=THRESH_MEAS)
dead_qubits = bad_readout(backend.target, threshold=0.4)
bad_nodes_coherence = bad_coherence(backend.target, threshold=THRESH_T2)
bad_nodes = list(set(bad_nodes_readout) | set(bad_nodes_coherence))
print(f"{len(bad_edges)} bad edges: \n{bad_edges}")
print(f"{len(bad_nodes)} bad nodes: \n{bad_nodes}")
17 bad edges:
[[30, 31], [112, 113], [113, 114], [113, 119], [120, 121], [130, 131], [145, 146], [146, 147], [111, 112], [55, 59], [64, 65], [131, 138], [131, 132], [119, 133], [129, 130], [47, 57], [29, 38]]
5 bad nodes:
[1, 113, 131, 146, 120]
Gamit ang function sa ibaba, binubuo natin ang GHZ circuit sa piniling hardware simula sa root at gumagamit ng breadth-first search (BFS).
def parallel_ghz(root, num_qubits, backend, bad_edges, skip):
"""
Build a GHZ state of size `num_qubits` on the given `backend`,
starting from `root`, expanding in BFS order.
At each BFS layer, every active qubit adds at most one new neighbor
(so that two-qubit operations can run in parallel with no qubit conflicts).
It grows the entanglement tree outward layer-by-layer.
"""
# -------------------------------------------------------------
# (1) Filter usable connections from the backend coupling map
# -------------------------------------------------------------
# The coupling map lists all directed hardware connections as (control, target).
# We remove edges that are:
# - listed in `bad_edges` (or their reversed form)
# - involve a qubit in the `skip` list
cmap = backend.configuration().coupling_map
edges = [
e
for e in cmap
if e not in bad_edges
and [e[1], e[0]] not in bad_edges
and e[0] not in skip
and e[1] not in skip
]
# -------------------------------------------------------------
# (2) Build an undirected adjacency list for traversal
# -------------------------------------------------------------
# Even though coupling_map edges are directed, BFS expansion just needs
# connectivity information (so we treat edges as undirected for search).
adj = defaultdict(list)
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
# -------------------------------------------------------------
# (3) Initialize the quantum circuit and BFS state
# -------------------------------------------------------------
n = backend.configuration().num_qubits
qc = QuantumCircuit(
n
) # create a circuit with same number of qubits as hardware
visited = [
root
] # record the order qubits are added to the GHZ chain/tree
queue = deque([root]) # BFS queue (start from root)
explored = defaultdict(
set
) # to track which neighbors each node has already explored
layers = [] # list of per-layer (control, target) gate tuples
qc.h(root) # GHZ states start with a Hadamard on the root qubit
# -------------------------------------------------------------
# (4) BFS expansion: build the GHZ tree one layer at a time
# -------------------------------------------------------------
# Loop until we've added the desired number of qubits to the GHZ
while queue and len(visited) < num_qubits:
layer = [] # collect new (control, target) pairs for this layer
current = list(
queue
) # snapshot current frontier (so queue mutations don't affect iteration)
busy = (
set()
) # track qubits already used in this layer (to avoid conflicts)
for node in current:
queue.popleft()
# find one unvisited neighbor of this node not already explored
unvisited_neighbors = [
nb
for nb in adj[node]
if nb not in visited and nb not in explored[node]
]
if unvisited_neighbors:
nb = unvisited_neighbors[
0
] # pick the first available neighbor
visited.append(nb) # mark it as part of the GHZ structure
queue.append(
node
) # re-enqueue current node (can keep growing)
queue.append(nb) # enqueue the newly added qubit
explored[node].add(nb) # mark that edge as explored
layer.append(
(node, nb)
) # schedule a CNOT between node and neighbor
busy.update([node, nb]) # reserve both qubits for this layer
# stop early if we've reached the desired number of qubits
if len(visited) == num_qubits:
break
# else: node has no unused unvisited neighbors left → skip
if layer:
# add all pairs (node, nb) scheduled this round to layers
layers.append(layer)
else:
# nothing new discovered this pass → done
break
# -------------------------------------------------------------
# (5) Emit all layers into the quantum circuit
# -------------------------------------------------------------
# For each layer:
# - apply a CX gate for every (control, target) pair
# - insert a barrier so transpiler keeps layer structure
for layer in layers:
for q1, q2 in layer:
qc.cx(q1, q2)
qc.barrier()
# -------------------------------------------------------------
# (6) Return outputs
# -------------------------------------------------------------
# qc: the built quantum circuit
# visited: order of qubits added
# layers: list of parallelizable two-qubit operations per step
return qc, visited, layers
Paulit-ulit na naghahanap tayo ng pinakamahusay na root, kung saan magsisimula ang GHZ circuit.
ROOT = None # root for BFS search
GHZ_SIZE = 100 # number of (data) qubits in the GHZ state
SKIP = [] # nodes to intentionally skip so that we have a better chance for finding checks
# Search for the best root (yielding the shallowest GHZ)
if ROOT is None:
best_root = -1
base_depth = 100
for root in range(backend.num_qubits):
qc, ghz_qubits, _ = parallel_ghz(
root, GHZ_SIZE, backend, bad_edges, SKIP
)
if len(ghz_qubits) != GHZ_SIZE:
continue
depth = qc.depth(lambda x: x.operation.num_qubits == 2)
if depth < base_depth:
best_root = root
base_depth = depth
ROOT = best_root
Binubuo na natin ngayon ang GHZ circuit simula sa isang tiyak na node — ang pinakamahusay na root — naghahanap ng pinakamaikling depth gamit ang breadth-first search.
# Build a GHZ starting at the best root
qc, ghz_qubits, _ = parallel_ghz(
ROOT, GHZ_SIZE, backend, bad_edges, SKIP + bad_nodes
)
base_depth = qc.depth(lambda x: x.operation.num_qubits == 2)
base_count = qc.size(lambda x: x.operation.num_qubits == 2)
print(f"base depth: {base_depth}, base count: {base_count}")
print(f"ROOT: {ROOT}")
if len(ghz_qubits) != GHZ_SIZE:
raise Exception("No GHZ found. Relax error thresholds.")
base depth: 17, base count: 99
ROOT: 50
Kailangan pa nating isaalang-alang ang isang huling bagay bago maglagay ng mga wastong check. Ito ay may kaugnayan sa konsepto ng "coverage", na isang sukatan ng kung gaano karaming wire sa isang quantum circuit ang maaaring saklawin ng isang check. Sa mas mataas na coverage, mas malawak na bahagi ng circuit ang ating matutukoy na may error. Sa pamamagitan ng sukatan na ito, maaari tayong pumili sa mga wastong check na may pinakamataas na circuit coverage. Sa ibang salita, gagamitin natin ang weighted_coverage na function para i-score ang iba't ibang check para sa GHZ circuit.
def weighted_coverage(layers, parities, w_idle=0.2, w_gate=0.8):
"""
Compute weighted fraction (idle + gate) of wires that are
covered by at least one parity to all active wires.
"""
wires = active_wires(layers) # defined below
covered_by_any = {n_layer: set() for n_layer in range(len(layers))}
for parity in parities:
trace = z_trace_backward(layers, parity) # defined below
for n_layer, qs in trace.items():
covered_by_any[n_layer] |= qs
covered_weight = 0
total_weight = 0
for n_layer in range(len(layers)):
idle = wires[n_layer]["idle"]
gate = wires[n_layer]["gate"]
total_weight += w_idle * len(idle) + w_gate * len(gate)
covered_idle = covered_by_any[n_layer] & idle
covered_gate = covered_by_any[n_layer] & gate
covered_weight += w_idle * len(covered_idle) + w_gate * len(
covered_gate
)
return covered_weight / total_weight if total_weight > 0 else 0
def active_wires(layers):
"""
Returns per-layer dict with two sets:
- 'idle': activated wires that are idle in this layer
- 'gate': activated wires that are control/target of a CNOT at this layer
"""
first_activation = {}
for n_layer, layer in enumerate(layers):
for c, t in layer:
first_activation.setdefault(c, n_layer)
first_activation.setdefault(t, n_layer)
result = {}
for n_layer in range(len(layers)):
active = {
q
for q, n_layer0 in first_activation.items()
if n_layer >= n_layer0
}
gate = {q for c, t in layers[n_layer] for q in (c, t)}
idle = active - gate
result[n_layer] = {"idle": idle, "gate": gate}
return result
def z_trace_backward(layers, initial_Zs):
"""
Backward propagate Zs with parity cancellation.
Returns {layer: set of qubits with odd parity Z at that layer}.
"""
wires = active_wires(layers)
support = set(initial_Zs)
trace = {}
for n_layer in range(len(layers) - 1, -1, -1):
active = wires[n_layer]["idle"] | wires[n_layer]["gate"]
trace[n_layer] = support & active
# propagate backwards
new_support = set()
for q in support:
hit = False
for c, t in layers[n_layer]:
if q == t: # Z on target: copy to control
new_support ^= {t, c} # toggle both
hit = True
break
elif q == c: # Z on control: passes through
new_support ^= {c}
hit = True
break
if not hit: # unaffected
new_support ^= {q}
support = new_support
return trace
Maaari na nating ipasok ang mga tseke sa GHZ circuit. Ang paghahanap ng mga wastong tseke ay napaka-maginhawa para sa GHZ state, dahil ang anumang two-qubit Pauli operator na na kumikilos sa anumang dalawang qubit na ng GHZ circuit ay isang suporta at samakatuwid ay isang wastong tseke.
Pansinin din na ang mga tseke sa kasong ito ay controlled- operators na kalapit ng mga Hadamard gates mula sa kaliwa at kanan ng ancilla qubit. Ito ay katumbas ng isang CNOT gate na inilapat sa ancilla qubit. Ang code sa ibaba ay nagpapasok ng mga tseke sa circuit.
# --- Tunables controlling the search space / scoring ---
MAX_SKIPS = 10 # at most how many qubits to skip (in addition to the bad ones and the ones forced to skip above)
SHUFFLES = 200 # how many times to try removing nodes for checks
MAX_DEPTH_INCREASE = 10 # how far from the base GHZ depth to go to include checks (increase this for more checks at expense of depth)
W_IDLE = 0.2 # weight of errors to consider during idle timesteps
W_GATE = 0.8 # weight of errors to consider during gates
# Remove random nodes from the GHZ and build from the root again to increase checks
degree_two_nodes = [
i
for i in ghz_qubits
if all(n in ghz_qubits for n in coupling_map.neighbors(i))
and len(coupling_map.neighbors(i)) >= 2
]
# --- Best-so-far tracking for the randomized search ---
num_checks = 0
best_covered_fraction = -1
best_qc = qc
best_checks = []
best_parities = []
best_layers = []
# Outer loop: vary how many GHZ nodes we try skipping (0..MAX_SKIPS-1)
for num_skips in range(MAX_SKIPS):
# Inner loop: try SHUFFLES random choices of 'num_skips' nodes to skip
for _ in range(SHUFFLES):
# Construct the skip set:
# - pre-existing forced SKIP
# - plus a random sample of 'degree_two_nodes' of size 'num_skips'
skip = SKIP + list(np.random.choice(degree_two_nodes, num_skips))
# Rebuild the GHZ using the current skip set and bad_nodes
qc, ghz_qubits, layers = parallel_ghz(
ROOT, GHZ_SIZE, backend, bad_edges, skip + bad_nodes
)
# Measure circuit cost as 2-qubit-gate depth only
depth = qc.depth(lambda x: x.operation.num_qubits == 2)
# If we failed to reach the target GHZ size, discard this attempt
if len(ghz_qubits) != GHZ_SIZE:
continue
# --- Build "checks" around the GHZ we just constructed ---
# A check qubit is a non-GHZ, non-dead qubit that has ≥2 neighbors inside the GHZ
# and all those incident edges are usable (i.e., not in bad_edges).
checks = []
parities = []
for i in range(backend.num_qubits):
neighbors = [
n for n in coupling_map.neighbors(i) if n in ghz_qubits
]
if (
i not in ghz_qubits
and i not in dead_qubits
and len(neighbors) >= 2
and not any(
[
[neighbor, i] in bad_edges
or [i, neighbor] in bad_edges
for neighbor in neighbors
]
)
):
# Record this qubit as a check qubit
checks.append(i)
parities.append((neighbors[0], neighbors[1]))
# Physically couple the check qubit 'i' to the two GHZ neighbors via CNOTs
# (This is the actual "check" attachment in the circuit.)
qc.cx(neighbors[0], i)
qc.cx(neighbors[1], i)
# Score this design using the weighted coverage metric over the GHZ build layers
covered_fraction = weighted_coverage(
layers=layers, parities=parities, w_idle=W_IDLE, w_gate=W_GATE
)
# Keep it only if:
# - coverage improves over the best so far, AND
# - the 2q depth budget isn't blown by more than MAX_DEPTH_INCREASE
if (
covered_fraction > best_covered_fraction
and depth <= base_depth + MAX_DEPTH_INCREASE
):
best_covered_fraction = covered_fraction
best_qc = qc
best_ghz_qubits = ghz_qubits
best_checks = checks
best_parities = parities
best_layers = layers
Maaari na nating i-print ang mga qubit na ginagamit sa GHZ circuit at ang mga check qubit.
# --- After search, report the best design found ---
qc = best_qc
checks = best_checks
parities = best_parities
layers = best_layers
ghz_qubits = best_ghz_qubits
if len(ghz_qubits) != GHZ_SIZE:
raise Exception("No GHZ found. Relax error thresholds.")
print(f"GHZ qubits: {ghz_qubits} {len(ghz_qubits)}")
print(f"Check qubits: {checks} {len(checks)}")
covered_fraction = weighted_coverage(
layers=layers, parities=parities, w_idle=W_IDLE, w_gate=W_GATE
)
print(
"Covered fraction (no idle): ",
weighted_coverage(
layers=layers, parities=parities, w_idle=0.0, w_gate=1.0
),
)
GHZ qubits: [50, 49, 51, 38, 52, 48, 58, 53, 47, 71, 39, 46, 70, 54, 33, 45, 72, 69, 55, 32, 37, 73, 68, 34, 31, 44, 25, 74, 78, 67, 18, 24, 79, 75, 89, 57, 11, 23, 93, 59, 88, 66, 10, 22, 92, 90, 87, 65, 12, 9, 21, 94, 91, 86, 77, 13, 8, 20, 95, 98, 97, 14, 7, 36, 99, 111, 107, 15, 6, 41, 115, 110, 106, 19, 17, 5, 40, 114, 109, 108, 105, 27, 4, 42, 118, 104, 28, 3, 129, 117, 103, 29, 2, 128, 125, 96, 30, 127, 124, 102] 100
Check qubits: [16, 26, 35, 43, 85, 126] 6
Covered fraction (no idle): 0.4595959595959596
Maaari din nating i-print ang ilang mga istatistika ng error.
def circuit_errors(target, circ, error_type="cz"):
"""
Pull per-resource error numbers from a Qiskit Target
for ONLY the qubits/edges actually used by `circ`.
Args:
target: qiskit.transpiler.Target (e.g., backend.target)
circ: qiskit.QuantumCircuit
error_type: one of {"cz", "meas", "t1", "t2"}:
- "cz" -> 2q CZ gate error on the circuit's used edges
- "meas" -> measurement error on the circuit's used qubits
- "t1" -> T1 (converted to microseconds) on used qubits
- "t2" -> T2 (converted to microseconds) on used qubits
Returns:
list[float] of the requested quantity for the active edges/qubits.
"""
# Get all 2-qubit edges that appear in the circuit (as undirected pairs).
active_edges = active_gates(circ) # e.g., {(0,1), (2,3), ...}
# Intersect those with the device coupling map (so we only query valid edges).
# Note: target.build_coupling_map().get_edges() yields directed pairs.
edges = [
edge
for edge in target.build_coupling_map().get_edges()
if tuple(sorted(edge)) in active_edges
]
# Deduplicate direction: keep only one orientation of each edge.
undirected_edges = []
for edge in edges:
if (edge[1], edge[0]) not in undirected_edges:
undirected_edges.append(edge)
edges = undirected_edges # (not used later—see note below)
# Accumulators for different error/physics quantities
cz_errors, meas_errors, t1_errors, t2_errors = [], [], [], []
# For every active (undirected) edge in the circuit, fetch its CZ error.
# NOTE: Uses active_gates(circ) again (undirected tuples). This assumes
# `target['cz']` accepts undirected indexing; many Targets store both directions.
for edge in active_gates(circ):
cz_errors.append(target["cz"][edge].error)
# For every active qubit, fetch measure error and T1/T2 (converted to µs).
for qubit in active_qubits(circ):
meas_errors.append(target["measure"][(qubit,)].error)
t1_errors.append(
target.qubit_properties[qubit].t1 * 1e6
) # seconds -> microseconds
t2_errors.append(
target.qubit_properties[qubit].t2 * 1e6
) # seconds -> microseconds
# Select which set to return.
if error_type == "cz":
return cz_errors
elif error_type == "meas":
return meas_errors
elif error_type == "t1":
return t1_errors
else:
return t2_errors
def active_qubits(circ):
"""
Return a list of qubit indices that participate in at least one
non-delay, non-barrier instruction in `circ`.
"""
active_qubits = set()
for inst in circ.data:
# Skip scheduling artifacts that don't act on state
if (
inst.operation.name != "delay"
and inst.operation.name != "barrier"
):
for qubit in inst.qubits:
q = circ.find_bit(
qubit
).index # map Qubit object -> integer index
active_qubits.add(q)
return list(active_qubits)
def active_gates(circ):
"""
Return a set of undirected 2-qubit edges (i, j) that appear in `circ`.
"""
used_2q_gates = set()
for inst in circ:
if inst.operation.num_qubits == 2:
qs = inst.qubits
# map Qubit objects -> indices, then sort to make the edge undirected
qs = sorted([circ.find_bit(q).index for q in qs])
used_2q_gates.add(tuple(sorted(qs)))
return used_2q_gates
# ---- Print summary statistics ----
cz_errors = circuit_errors(backend.target, qc, error_type="cz")
meas_errors = circuit_errors(backend.target, qc, error_type="meas")
t1_errors = circuit_errors(backend.target, qc, error_type="t1")
t2_errors = circuit_errors(backend.target, qc, error_type="t2")
np.set_printoptions(linewidth=np.inf)
print(
f"cz errors: \n mean: {np.round(np.mean(cz_errors), 3)}, max: {np.round(np.max(cz_errors), 3)}"
)
print(
f"meas errors: \n mean: {np.round(np.mean(meas_errors), 3)}, max: {np.round(np.max(meas_errors), 3)}"
)
print(
f"t1 errors: \n mean: {np.round(np.mean(t1_errors), 1)}, min: {np.round(np.min(t1_errors), 1)}"
)
print(
f"t2 errors: \n mean: {np.round(np.mean(t2_errors), 1)}, min: {np.round(np.min(t2_errors), 1)}"
)
cz errors:
mean: 0.002, max: 0.012
meas errors:
mean: 0.014, max: 0.121
t1 errors:
mean: 267.9, min: 23.6
t2 errors:
mean: 155.9, min: 13.9
Tulad ng dati, maaari nating i-simulate ang circuit nang una sa kawalan ng ingay upang matiyak ang kawastuhan ng GHZ state preparation circuit.
# --- Simulate to ensure correctness ---
qc_meas = qc.copy()
# Add measurements to the GHZ qubits
c1 = ClassicalRegister(len(ghz_qubits), "c1")
qc_meas.add_register(c1)
for q, c in zip(ghz_qubits, c1):
qc_meas.measure(q, c)
# Add measurements to the check qubits
if len(checks) > 0:
c2 = ClassicalRegister(len(checks), "c2")
qc_meas.add_register(c2)
for q, c in zip(checks, c2):
qc_meas.measure(q, c)
# Simulate the circuit with stabilizer method
sim_stab = AerSimulator(method="stabilizer")
res = sim_stab.run(qc_meas, shots=1000).result()
counts = res.get_counts()
print("Stabilizer simulation result:")
print(counts)
# Rename keys to "0 0" and "0 1" for easier plotting
# First len(checks) bits are check bits, rest are GHZ bits
keys = list(counts.keys())
for key in keys:
check_bits = key[: len(checks)]
ghz_bits = key[(len(checks) + 1) :]
if set(check_bits) == {"0"} and set(ghz_bits) == {"0"}:
counts["0 0"] = counts.pop(key)
elif set(check_bits) == {"0"} and set(ghz_bits) == {"1"}:
counts["0 1"] = counts.pop(key)
else:
continue
plot_histogram(counts)
Stabilizer simulation result:
{'000000 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111': 525, '000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000': 475}
Tulad ng inaasahan, ang mga check qubit ay nasusukat bilang lahat ng sero, at matagumpay nating nahanda ang GHZ state.
Hakbang 3: Isagawa gamit ang Qiskit primitives
Handa na tayo ngayon na patakbuhin ang circuit sa tunay na hardware at ipakita kung paano nakakakuha ng mga error ang error detection protocol sa paghahanda ng GHZ state.
BAD_QUBITS = [] # specify any additional bad qubits to avoid (this is specific to the chosen backend)
SHOTS = 10000 # number of shots
Nagtatakda tayo ng isang helper function upang idagdag ang mga sukat sa GHZ circuit.
def add_measurements(qc, ghz_qubits, checks):
# --- Measure each set of qubits into different classical registers to facilitate post-processing ---
# Add measurements to the GHZ qubits
c1 = ClassicalRegister(len(ghz_qubits), "c1")
qc.add_register(c1)
for q, c in zip(ghz_qubits, c1):
qc.measure(q, c)
# Add measurements to the check qubits
c2 = ClassicalRegister(len(checks), "c2")
qc.add_register(c2)
for q, c in zip(checks, c2):
qc.measure(q, c)
return qc
Bago ang pagpapatakbo, iginuhit natin ang layout ng GHZ qubits at ng check qubits sa napiling hardware.
# Plot the layout of GHZ and check qubits on the device
plot_gate_map(
backend,
label_qubits=True,
line_width=20,
line_color=[
"black"
if edge[0] in ghz_qubits + checks and edge[1] in ghz_qubits + checks
else "lightgrey"
for edge in backend.coupling_map.graph.edge_list()
],
qubit_color=[
"blue"
if i in ghz_qubits
else "salmon"
if i in checks
else "lightgrey"
for i in range(0, backend.num_qubits)
],
)
plt.show()

qc.draw("mpl", idle_wires=False, fold=-1)

Idinaragdag na natin ngayon ang mga sukat.
qc = add_measurements(qc, ghz_qubits, checks)
Ang scheduling pipeline sa ibaba ay nagtatakda ng timing, nag-aalis ng mga barrier, nagpapasimple ng mga delay, at naglalagay ng dynamical decoupling, habang pinapanatili ang orihinal na oras ng operasyon.
# The scheduling consists of first inserting delays while barriers are still there
# Then removing the barriers and consolidating the delays, so that the operations do not move in time
# Lastly we replace delays with dynamical decoupling
collect_function = partial(
collect_using_filter_function,
filter_function=(lambda node: node.op.name == "delay"),
split_blocks=True,
min_block_size=2,
split_layers=False,
collect_from_back=False,
max_block_width=None,
)
collapse_function = partial(
collapse_to_operation,
collapse_function=(
lambda circ: Delay(sum(inst.operation.duration for inst in circ))
),
)
class Unschedule(AnalysisPass):
"""Removes a property from the passmanager property set so that the circuit looks unscheduled, so we can schedule it again."""
def run(self, dag):
del self.property_set["node_start_time"]
def build_passmanager(backend, dd_qubits=None):
pm = generate_preset_pass_manager(
target=backend.target,
layout_method="trivial",
optimization_level=2,
routing_method="none",
)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=backend.target),
PadDelay(target=backend.target),
RemoveBarriers(),
Unschedule(),
CollectAndCollapse(
collect_function=collect_function,
collapse_function=collapse_function,
),
ALAPScheduleAnalysis(target=backend.target),
PadDynamicalDecoupling(
dd_sequence=[XGate(), RZGate(-np.pi), XGate(), RZGate(np.pi)],
spacing=[1 / 4, 1 / 2, 0, 0, 1 / 4],
target=backend.target,
qubits=dd_qubits,
),
]
)
return pm
Maaari na nating gamitin ang custom pass manager upang i-transpile ang circuit para sa napiling backend.
# Transpile the circuits for the backend
pm = build_passmanager(backend, ghz_qubits)
# Instruction set architecture (ISA) level circuit after scheduling and DD insertion
isa_circuit = pm.run(qc)
# Draw after scheduling and DD insertion
# timeline_drawer(isa_circuit, show_idle=False, time_range=(0, 1000), target=backend.target)
isa_circuit.draw("mpl", fold=-1, idle_wires=False)

Isinusumite natin ang trabaho gamit ang Qiskit Runtime Sampler primitive.
# Select the sampler options
sampler = Sampler(mode=backend)
sampler.options.default_shots = SHOTS
sampler.options.dynamical_decoupling.enable = False
sampler.options.execution.rep_delay = 0.00025
# Submit the job
print("Submitting sampler job")
ghz_job = sampler.run([isa_circuit])
print(ghz_job.job_id())
d493f17nmdfs73abf9qg
Hakbang 4: Post-process at ibalik ang resulta sa nais na klasikal na format
Maaari na nating kunin at suriin ang mga resulta mula sa Sampler job.
# Retrieve the job results
job_result = ghz_job.result()
# Get the counts from GHZ and check qubit measurements
ghz_counts = job_result[0].data.c1.get_counts()
checks_counts = job_result[0].data.c2.get_counts()
# Post-process to get unflagged GHZ counts (i.e., check bits are all '0')
joined_counts = job_result[0].join_data().get_counts()
unflagged_counts = {}
for key, count in joined_counts.items():
check_bits = key[: len(checks)]
ghz_bits = key[len(checks) :]
if set(check_bits) == {"0"}:
unflagged_counts[ghz_bits] = count
# Get top 20 outcomes by frequency from the unflagged counts
top_counts = dict(
sorted(unflagged_counts.items(), key=lambda x: x[1], reverse=True)[:20]
)
# Rename keys for better visualization
top_counts_renamed = {}
i = 0
for key, count in top_counts.items():
if set(key) == {"0"}:
top_counts_renamed["all 0s"] = count
elif set(key) == {"1"}:
top_counts_renamed["all 1s"] = count
else:
top_counts_renamed[f"other_{i}"] = count
i += 1
plot_histogram(top_counts_renamed, figsize=(12, 7))

Sa histogram sa itaas, nagpakita tayo ng 20 bitstring na sukat mula sa GHZ qubits na hindi na-flag ng mga check qubit. Gaya ng inaasahan, ang all-0 at all-1 na mga bitstring ang may pinakamataas na bilang. Dapat pansinin na ang ilang maling bitstring na may mababang error weight ay hindi nakuha ng error detection. Ang pinakamataas na bilang ay makikita pa rin sa mga inaasahang bitstring.
Talakayan
Sa tutorial na ito, ipinakita natin kung paano ipatupad ang isang low-overhead na pamamaraan ng error detection gamit ang mga spacetime code, at ipinakita ang tunay na aplikasyon nito sa paghahanda ng mga GHZ state sa hardware. Sumangguni sa [3] upang mas malalim na tuklasin ang mga teknikal na detalye ng paghahanda ng GHZ state. Bukod sa error detection, ginagamit ng mga may-akda ang readout error mitigation gamit ang M3 at TREX at nagsasagawa ng mga pamamaraan ng pansamantalang uncomputation upang maghanda ng mga high-fidelity na GHZ state.
Mga Sanggunian
- [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.