Szybki start
Ten przewodnik pokazuje minimalny działający przykład użycia pakietu qiskit-paulice. Używamy czasoprzestrzennych kontroli Pauliego, aby wykrywać błędy podczas wykonywania obwodu Clifforda, a następnie poddajemy postselekcji tylko te próbki, dla których nie wykryto błędu. Bardziej szczegółowy, kompletny przepływ pracy, który ilościowo określa poprawę wierności, znajdziesz w szczegółowym samouczku.
Kroki przepływu pracy
- Przygotuj dane wejściowe: obwód Clifforda (ładunek), kubity docelowe i pomocnicze (ancilla) przeznaczone na kontrole oraz model szumu.
- Znajdź dobre czasoprzestrzenne kontrole Pauliego i dodaj je do obwodu.
- Wykonaj próbkowanie obwodu z kontrolami.
- Poddaj postselekcji tylko te próbki, dla których nie wykryto błędu.
1. Przygotuj dane wejściowe
add_pauli_checks przyjmuje obwód Clifforda circuit zakończony pomiarami (lub, z argumentem stabilizers, obwód bez pomiarów, jak w przewodniku o kodach czasoprzestrzennych), kubity docelowe, które posłużą do realizacji kontroli, oraz model szumu służący do oceny kandydatów na kontrole. Tutaj budujemy płytki losowy obwód Clifforda, układamy go na jednowymiarowym łańcuchu kubitów fizycznych na ibm_boston, łączymy każdy kubit ładunku z sąsiednim kubitem pomocniczym (aby kontrola nie wymagała bramek SWAP) i wnioskujemy przybliżony model szumu depolaryzacyjnego z danych testowych backendu.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_paulice": "qiskit-paulice"}
_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
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_paulice.layout import get_check_qubits
from qiskit_paulice.noise_models import NoiseModel
# Backend and a 1D chain of physical qubits to run on
backend = QiskitRuntimeService().backend("ibm_boston")
layout = [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133]
# A shallow brickwork random Clifford payload circuit
rng = np.random.default_rng(1764)
circuit = QuantumCircuit(len(layout))
circuit.h(range(circuit.num_qubits))
for d in range(4):
for i in range(d % 2, circuit.num_qubits - 1, 2):
circuit.cz(i, i + 1)
for q in range(circuit.num_qubits):
if rng.integers(0, 2):
circuit.sx(q)
if rng.integers(0, 2):
circuit.s(q)
circuit.measure_all()
# Pair each payload qubit with a neighboring ancilla to host a check (no SWAPs needed)
target_qubits, ancilla_qubits = get_check_qubits(backend.coupling_map, layout)
# A rough depolarizing noise model from backend benchmark data, used to score checks
noise_model = NoiseModel.from_backend(
backend, layout, uniform_gate_noise=True
)
circuit.draw("mpl", fold=-1)

2. Znajdź i dodaj czasoprzestrzenne kontrole Pauliego
add_pauli_checks wyszukuje skuteczne kontrole o niskiej wadze dla każdego kubitu docelowego i dodaje je do obwodu, zwracając ciąg instancji CheckedCircuit. Instancje CheckedCircuit zawierają rosnącą liczbę kontroli, od 0 kontroli aż do jednej kontroli na każdy kubit docelowy. Każda kontrola poprawia zdolność wykrywania błędów kosztem nieco większej głębokości obwodu. W tym przykładzie użyjemy obwodu z kontrolą na każdym kubicie docelowym.
from qiskit_paulice import add_pauli_checks
# The circuit has virtual qubits, so we specify our target qubits with virtual indices
target_qubits_v = [layout.index(q) for q in target_qubits]
# Add spacetime Pauli checks
checked_circuit = add_pauli_checks(circuit, target_qubits_v, noise_model)[-1]
checked_circuit.circuit.draw("mpl", fold=-1, scale=0.4, idle_wires=False)

3. Próbkowanie obwodu z kontrolami
Zanim zaczniemy próbkowanie, transpilujemy obwód z kontrolami na kubity fizyczne (initial_layout = layout + ancilla_qubits) i do natywnego zestawu bramek bazowych ibm_boston. Aby emulować zaszumiony QPU, próbkujemy za pomocą zaszumionego backendu stabilizatorowego Aer, którego poziomy błędów odpowiadają noise_model użytemu do wyboru kontroli.
from qiskit import transpile
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import ReadoutError, depolarizing_error
# Transpile once: lay the checked circuit out on our qubits and into the native basis
isa_circuit = transpile(
checked_circuit.circuit,
backend,
initial_layout=layout + ancilla_qubits,
optimization_level=0,
)
# Build an Aer noise model matching the depolarizing model used to pick checks
aer_noise = AerNoiseModel()
aer_noise.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
p = noise_model.readout_noise
aer_noise.add_all_qubit_readout_error(ReadoutError([[1 - p, p], [p, 1 - p]]))
simulator = AerSimulator(method="stabilizer", noise_model=aer_noise)
counts = (
simulator.run(isa_circuit, shots=1000, seed_simulator=1764)
.result()
.get_counts()
)
print(f"sampled {sum(counts.values())} shots")
sampled 1000 shots
4. Postselekcja próbek bez wykrytego błędu
get_postselection_method odwzorowuje każde wykonanie na jego wektor syndromu; zachowujemy tylko te z syndromem złożonym z samych zer — czyli takie, w których żadna kontrola nie wykryła błędu. Odrzucenie oznaczonych wykonań usuwa wykryte błędy z rozkładu, który pozostaje, poprawiając jego wierność kosztem niższej szybkości próbkowania. Ilościowe porównanie wierności znajdziesz w samouczku.
# Keep only the shots in which no check reported an error
ps_fn = checked_circuit.get_postselection_method()
counts_postselected = {
bs: n for bs, n in counts.items() if not ps_fn(bs).any()
}
kept, total = sum(counts_postselected.values()), sum(counts.values())
print(f"kept {kept} of {total} shots ({kept / total:.0%})")
kept 927 of 1000 shots (93%)