Wie man Schranken mit einem Speed Limit verschärft
Information in einem Circuit aus Zwei-Qubit-Gates kann sich nicht beliebig schnell ausbreiten. Ein Fehler, der mehrere Schichten vor dem Ende auf einem Qubit weit entfernt vom Träger der Observablen auftritt, hat keinen Weg, sie zu erreichen. tighten_with_speed_limit macht dieses geometrische Argument (dieselbe Intuition wie hinter den Lieb-Robinson-Schranken) zu engeren Vorwärtsschranken.
Der Schritt ist günstig, braucht nichts, was die Vorwärtsschranken nicht ohnehin schon verlangten, und kann Schranken nur enger machen. Es gibt keinen Genauigkeits-Kompromiss abzuwägen.
Die Frage, die man zuerst beantworten sollte, lautet "Wann hilft es?", denn bei einem vollständig konvergierten Satz von Vorwärtsschranken hilft es oft nicht.
Einrichtung
Verwende denselben Trotterisierten Ising-Circuit wie in der Quickstart-Anleitung, mit der Observablen auf einem einzelnen Qubit in der Mitte der Kette. Die 1D-Geometrie macht den Effekt leicht nachvollziehbar: Ein Einfluss muss die Kette pro Schicht eine Bindung weit durchlaufen.
# 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
Wann es hilft und wann nicht
tighten_with_speed_limit nimmt die Schranken, die du bereits hast, und gibt eine verschärfte Kopie zurück; deine ursprünglichen Schranken werden nie verändert. Wie viel es bringt, hängt ganz davon ab, wie locker die Eingabe war.
# 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
Die erste Zeile ist die, auf die du achten solltest. Bei vollständig berechneten Vorwärtsschranken ändert das Speed Limit nichts. Das ist kein Fehler. Es bedeutet, dass die Schranken bereits mindestens so eng waren, wie das geometrische Argument beweisen kann, weil die Operatorentwicklung den Lightcone effektiv selbst entdeckt hat.
Die Gewinne zeigen sich dort, wo die Schranken nicht vollständig berechnet wurden:
-
eigval_max_qubits=0erzwingt den Rückfall auf die Dreiecksungleichung, sodass die Schranken die tatsächliche Spektralnorm des Kommutators nicht mehr widerspiegeln. Das Speed Limit holt einen Teil dieses Verlusts zurück. -
max_num_boxes=2lässt die meisten Schichten bei der trivialen Schranke von 2.0 (eine Schranke, die keine Information enthält). Hier hilft das Speed Limit und senkt den Mittelwert um etwa den Faktor drei.
In jedem Fall ist die Spalte „gelockert“ null. Kein Term wird je schlechter, und diese Garantie macht den Schritt bedenkenlos unbedingt anwendbar.
Was es beim Sampling-Overhead bringt
Engere Schranken bedeuten, dass bei gegebener Bias-Toleranz weniger Fehlerterme mitigiert werden müssen, und genau diese Zahl zählt auf der Hardware.
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
Wann man es verwendet
Wende es auf Vorwärtsschranken an, sobald diese nicht vollständig konvergiert sind (im Grunde immer dann, wenn du mit einer der Optionen im Konfigurationsleitfaden Genauigkeit gegen Geschwindigkeit getauscht hast). Es ist im Vergleich zur Schrankenberechnung günstig, kann nichts verschlechtern und holt viel von dem zurück, was eine max_num_boxes- oder eigval_max_qubits-Grenze hergegeben hat.
Bei vollständig berechneten Schranken ist es ein No-op, sodass es nicht schadet, es bedingungslos anzuwenden.
Zwei Einschränkungen:
-
Es gilt nur für Vorwärtsschranken. Rückwärtsschranken betreffen die Wirkung des Fehlers auf den Anfangszustand statt auf die Observable, daher lässt sich das Argument nicht übertragen.
-
Die Observable muss ein einzelner Pauli-Term sein. Bei einer Observablen mit mehreren Termen rufst du es einmal pro Pauli auf und kombinierst die Ergebnisse selbst. Ist die Observable kein einzelner Pauli-Term, löst es einen
NotImplementedErroraus.
Im Standard-Workflow gehört es zwischen die Berechnung der Vorwärtsschranken und deren Zusammenführung mit den Rückwärtsschranken, wie die Quickstart-Anleitung zeigt.