Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Comment resserrer les bornes avec une limite de vitesse

L'information dans un circuit de portes à deux qubits ne peut pas se propager arbitrairement vite. Une erreur survenant plusieurs couches avant la fin, sur un qubit éloigné du support de l'observable, n'a aucun chemin pour l'atteindre. tighten_with_speed_limit transforme cet argument géométrique (la même intuition que celle des bornes de Lieb-Robinson) en bornes avant plus serrées.

Cette étape est peu coûteuse, ne nécessite rien de plus que ce que les bornes avant exigeaient déjà, et ne peut que resserrer les bornes. Il n'y a aucun compromis de précision à peser.

La question à se poser d'abord est « quand est-ce utile ? », car cela aide rarement sur un ensemble de bornes avant entièrement convergé.

Configuration​

Utilise le même circuit d'Ising trotterisé que le guide de démarrage rapide, avec l'observable sur un seul qubit au milieu de la chaîne. La géométrie 1D rend l'effet facile à analyser : une influence doit parcourir la chaîne d'une liaison par couche.

# 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

Quand c'est utile, et quand ça ne l'est pas​

tighten_with_speed_limit prend les bornes dont tu disposes déjà et renvoie une copie resserrée ; tes bornes d'origine ne sont jamais modifiées. Le gain dépend entièrement de la largeur des bornes en entrée.

# 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

La première ligne est celle à remarquer. Sur des bornes avant entièrement calculées, la limite de vitesse ne change rien. Ce n'est pas un échec. Cela signifie que les bornes étaient déjà au moins aussi serrées que ce que l'argument géométrique peut prouver, car l'évolution de l'opérateur a effectivement découvert le cône de lumière par elle-même.

Les gains apparaissent là où les bornes n'étaient pas entièrement calculées :

  • eigval_max_qubits=0 force le repli sur l'inégalité triangulaire, de sorte que les bornes ne reflètent plus la véritable norme spectrale du commutateur. La limite de vitesse récupère une partie de cette perte.

  • max_num_boxes=2 laisse la plupart des couches à la borne triviale de 2.0 (une borne qui n'encode aucune information). Ici, la limite de vitesse est utile, réduisant la moyenne d'environ un facteur trois.

Dans tous les cas, la colonne « relâchées » est nulle. Aucun terme n'est jamais dégradé, ce qui est la garantie qui rend l'étape sûre à appliquer sans condition.

Ce qu'elle apporte en surcoût d'échantillonnage​

Des bornes plus serrées signifient moins de termes d'erreur à atténuer pour une tolérance de biais donnée, ce qui est le nombre qui compte sur le matériel.

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

Quand l'utiliser​

Applique-la aux bornes avant chaque fois que ces bornes ne sont pas entièrement convergées (en gros, chaque fois que tu as sacrifié de la précision pour de la vitesse avec l'une des options du guide de configuration). Elle est peu coûteuse comparée au calcul des bornes, ne peut rien aggraver, et récupère une grande partie de ce qu'une limite max_num_boxes ou eigval_max_qubits a fait perdre.

Sur des bornes entièrement calculées, elle ne fait rien, il n'y a donc aucun inconvénient à l'appliquer sans condition.

Deux limitations :

  • Elle ne s'applique qu'aux bornes avant. Les bornes arrière concernent l'effet de l'erreur sur l'état initial plutôt que sur l'observable, donc l'argument ne se transpose pas.

  • L'observable doit être un unique terme de Pauli. Pour une observable à plusieurs termes, appelle-la une fois par Pauli et combine les résultats toi-même. Si l'observable n'est pas un unique terme de Pauli, elle lève une NotImplementedError.

Dans le workflow standard, elle se place entre le calcul des bornes avant et leur fusion avec les bornes arrière, comme le montre le guide de démarrage rapide.