Jak zacieśniać granice za pomocą limitu prędkości
Informacja w obwodzie bramek dwukubitowych nie może rozchodzić się dowolnie szybko. Błąd występujący kilka warstw przed końcem, na kubicie odległym od nośnika obserwabli, nie ma drogi, by do niej dotrzeć. tighten_with_speed_limit zamienia ten argument geometryczny (tę samą intuicję, na której opierają się granice Lieba-Robinsona) w ciaśniejsze granice w przód.
Ten krok jest tani, nie wymaga niczego poza tym, czego już wymagały granice w przód, i może tylko zacieśnić granice. Nie ma kompromisu kosztem dokładności, który trzeba by rozważać.
Pierwsze pytanie, na które warto odpowiedzieć, brzmi „kiedy to pomaga?”, ponieważ rzadko pomaga w pełni zbieżnemu zestawowi granic w przód.
Konfiguracja
Użyj tego samego trotteryzowanego obwodu Isinga co w przewodniku szybkiego startu, z obserwablą na pojedynczym kubicie w środku łańcucha. Geometria 1D ułatwia rozumowanie o efekcie: wpływ musi przejść łańcuch o jedno wiązanie na warstwę.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "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
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.bounds import (
compute_forward_bounds,
tighten_with_speed_limit,
)
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions
def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit
num_qubits = 10
circuit = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# A single-qubit observable in the middle of the chain.
observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])
boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)
def compare(label, bounds):
"""Tightens a set of bounds and prints what changed."""
tightened = tighten_with_speed_limit(
bounds, boxed_circuit, noise_model_paulis, observable
)
before = np.concatenate([bound.rates for bound in bounds.values()])
after = np.concatenate([bound.rates for bound in tightened.values()])
print(
f"{label:30} mean {before.mean():.4f} -> {after.mean():.4f} "
f"tightened {np.count_nonzero(after < before - 1e-12):4} of {len(before)} "
f"loosened {np.count_nonzero(after > before + 1e-12)}"
)
print(
f"observable: Z on qubit {num_qubits // 2}, circuit depth {circuit.depth()}"
)
observable: Z on qubit 5, circuit depth 12
Kiedy pomaga, a kiedy nie
tighten_with_speed_limit przyjmuje granice, które już masz, i zwraca ich zacieśnioną kopię; twoje oryginalne granice nigdy nie są modyfikowane. To, ile zyskuje, zależy w całości od tego, jak luźne były dane wejściowe.
# 1. Fully computed forward bounds.
full_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
compare("fully computed", full_bounds)
# 2. Bounds where the eigenvalue step was skipped, leaving the looser triangle inequality.
loose_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, eigval_max_qubits=0
)
compare("triangle inequality only", loose_bounds)
# 3. Bounds computed only for the first few boxes, the rest left at the trivial bound of 2.0.
shallow_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=2
)
compare("max_num_boxes=2", shallow_bounds)
fully computed mean 0.3639 -> 0.3639 tightened 0 of 792 loosened 0
triangle inequality only mean 0.5081 -> 0.4842 tightened 105 of 792 loosened 0
max_num_boxes=2 mean 1.5707 -> 0.5284 tightened 450 of 792 loosened 0
Pierwszy wiersz jest tym, na który warto zwrócić uwagę. Dla w pełni obliczonych granic w przód limit prędkości niczego nie zmienia. To nie jest porażka. Oznacza to, że granice były już co najmniej tak ciasne, jak może udowodnić argument geometryczny, ponieważ ewolucja operatora sama w praktyce odkryła stożek świetlny.
Zyski pojawiają się tam, gdzie granice nie zostały w pełni obliczone:
-
eigval_max_qubits=0wymusza awaryjne użycie nierówności trójkąta, więc granice nie odzwierciedlają już rzeczywistej normy spektralnej komutatora. Limit prędkości odzyskuje część tej straty. -
max_num_boxes=2zostawia większość warstw przy trywialnej granicy 2.0 (granicy, która nie niesie żadnej informacji). Tutaj limit prędkości pomaga, zmniejszając średnią mniej więcej trzykrotnie.
W każdym przypadku kolumna „poluzowane” wynosi zero. Żaden składnik nigdy nie jest pogarszany, co jest gwarancją, dzięki której ten krok można bezpiecznie stosować bezwarunkowo.
Co daje w narzucie próbkowania
Ciaśniejsze granice oznaczają mniej składników błędu, które trzeba łagodzić przy danej tolerancji obciążenia, a to właśnie ta liczba ma znaczenie na sprzęcie.
from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales
# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}
tightened_shallow = tighten_with_speed_limit(
shallow_bounds, boxed_circuit, noise_model_paulis, observable
)
print(
f"{'bounds':30} {'overhead':>9} {'mitigated':>10} {'residual bias':>14}"
)
for label, bounds in (
("max_num_boxes=2", shallow_bounds),
("max_num_boxes=2 + speed limit", tightened_shallow),
("fully computed", full_bounds),
):
scales, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
mitigated = sum(int(np.count_nonzero(scale)) for scale in scales.values())
total = sum(len(scale) for scale in scales.values())
print(
f"{label:30} {cost:9.4f} {f'{mitigated}/{total}':>10} {bias:>14.4f}"
)
bounds overhead mitigated residual bias
max_num_boxes=2 420.2823 617/792 0.0490
max_num_boxes=2 + speed limit 7.8511 226/792 0.0485
fully computed 5.4031 188/792 0.0494
Kiedy tego używać
Stosuj to do granic w przód zawsze, gdy nie są w pełni zbieżne (zasadniczo zawsze, gdy zamieniłeś dokładność na szybkość, używając którejkolwiek z opcji z przewodnika konfiguracji). Jest tanie w porównaniu z obliczaniem granic, nie może niczego pogorszyć i odzyskuje sporo z tego, co oddał limit max_num_boxes lub eigval_max_qubits.
Dla w pełni obliczonych granic jest to operacja pusta, więc stosowanie go bezwarunkowo nie szkodzi.
Dwa ograniczenia:
-
Dotyczy tylko granic w przód. Granice wstecz dotyczą wpływu błędu na stan początkowy, a nie na obserwablę, więc argument się nie przenosi.
-
Obserwabla musi być pojedynczym składnikiem Pauliego. Dla obserwabli z kilkoma składnikami wywołaj to raz dla każdego operatora Pauliego i połącz wyniki samodzielnie. Jeśli obserwabla nie jest pojedynczym składnikiem Pauliego, zgłasza
NotImplementedError.
W standardowym przepływie pracy należy to umieścić między obliczeniem granic w przód a ich scaleniem z granicami wstecz, jak pokazuje przewodnik szybkiego startu.