Cómo ajustar las cotas con un límite de velocidad
La información en un circuito de compuertas de dos qubits no puede propagarse arbitrariamente rápido. Un error que ocurre
varias capas antes del final, en un qubit lejos del soporte del observable, no tiene camino para alcanzarlo.
tighten_with_speed_limit convierte ese argumento geométrico (la misma intuición que hay detrás de las
cotas de Lieb-Robinson) en cotas hacia delante más ajustadas.
El paso es barato, no necesita nada más allá de lo que ya requerían las cotas hacia delante, y solo puede hacer las cotas más ajustadas. No hay que sopesar ninguna compensación de precisión.
La pregunta que conviene responder primero es "¿cuándo ayuda?", porque no suele ayudar con un conjunto de cotas hacia delante totalmente convergido.
Configuración
Usa el mismo circuito de Ising con descomposición de Trotter que en la guía de inicio rápido, con el observable sobre un solo qubit en medio de la cadena. La geometría 1D facilita razonar sobre el efecto: una influencia tiene que recorrer la cadena un enlace por capa.
# 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
Cuándo ayuda y cuándo no
tighten_with_speed_limit toma las cotas que ya tienes y devuelve una copia ajustada; tus
cotas originales nunca se modifican. Cuánto gana depende por completo de lo holgada que fuera la entrada.
# 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 primera línea es la que hay que notar. Con cotas hacia delante totalmente calculadas, el límite de velocidad no cambia nada. Eso no es un fallo. Significa que las cotas ya eran al menos tan ajustadas como lo que el argumento geométrico puede demostrar, porque la evolución del operador ha descubierto en la práctica el cono de luz por sí misma.
Las ganancias aparecen donde las cotas no se calcularon por completo:
-
eigval_max_qubits=0fuerza el recurso a la desigualdad triangular, por lo que las cotas ya no reflejan la norma espectral real del conmutador. El límite de velocidad recupera parte de esa pérdida. -
max_num_boxes=2deja la mayoría de las capas en la cota trivial de 2.0 (una cota que no codifica ninguna información). Aquí, el límite de velocidad es útil y reduce la media en aproximadamente un factor de tres.
En todos los casos la columna "loosened" es cero. Ningún término empeora nunca, que es la garantía que hace seguro aplicar el paso de forma incondicional.
Lo que aporta en sobrecarga de muestreo
Cotas más ajustadas significan que hay menos términos de error que mitigar para una tolerancia de sesgo dada, que es el número que importa en el 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
Cuándo usarlo
Aplícalo a las cotas hacia delante siempre que no estén totalmente convergidas (básicamente,
siempre que hayas sacrificado precisión por velocidad con cualquiera de las opciones de la
guía de configuración). Es barato comparado con el cálculo de cotas,
no puede empeorar nada y recupera gran parte de lo que un límite de max_num_boxes o eigval_max_qubits
había cedido.
Con cotas totalmente calculadas no hace nada, así que no hay inconveniente en aplicarlo de forma incondicional.
Dos limitaciones:
-
Se aplica solo a cotas hacia delante. Las cotas hacia atrás se refieren al efecto del error sobre el estado inicial y no sobre el observable, por lo que el argumento no se extiende a ellas.
-
El observable debe ser un único término de Pauli. Para un observable con varios términos, llámalo una vez por cada Pauli y combina los resultados tú mismo. Si el observable no es un único término de Pauli, lanza un
NotImplementedError.
En el flujo de trabajo estándar, va entre el cálculo de las cotas hacia delante y su fusión con las cotas hacia atrás, como muestra la guía de inicio rápido.