Como apertar limites com um limite de velocidade
A informação em um circuito de portas de dois qubits não pode se espalhar arbitrariamente rápido. Um erro que ocorre
várias camadas antes do fim, em um qubit distante do suporte do observável, não tem caminho para alcançá-lo.
tighten_with_speed_limit transforma esse argumento geométrico (a mesma intuição por trás dos
limites de Lieb-Robinson) em limites diretos mais apertados.
A etapa é barata, não precisa de nada além do que os limites diretos já exigiam e só pode tornar os limites mais apertados. Não há compromisso de precisão a ponderar.
A pergunta que vale responder primeiro é "quando isso ajuda?", porque nem sempre ajuda em um conjunto de limites diretos totalmente convergido.
Configuração
Use o mesmo circuito de Ising com Trotter do guia de início rápido, com o observável em um único qubit no meio da cadeia. A geometria 1D facilita raciocinar sobre o efeito: uma influência precisa percorrer a cadeia uma ligação por camada.
# 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
Quando ajuda e quando não ajuda
tighten_with_speed_limit pega os limites que você já tem e retorna uma cópia apertada; seus
limites originais nunca são modificados. O quanto ele ganha depende inteiramente de quão frouxa era a 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
A primeira linha é a que merece atenção. Em limites diretos totalmente calculados, o limite de velocidade não muda nada. Isso não é uma falha. Significa que os limites já eram pelo menos tão apertados quanto o argumento geométrico consegue provar, porque a evolução do operador efetivamente descobriu o cone de luz por conta própria.
Os ganhos aparecem onde os limites não foram totalmente calculados:
-
eigval_max_qubits=0força o recurso da desigualdade triangular, então os limites deixam de refletir a norma espectral real do comutador. O limite de velocidade recupera parte dessa perda. -
max_num_boxes=2deixa a maioria das camadas no limite trivial de 2.0 (um limite que não codifica nenhuma informação). Aqui, o limite de velocidade ajuda, reduzindo a média em aproximadamente um fator de três.
Em todos os casos, a coluna "afrouxados" é zero. Nenhum termo jamais piora, o que é a garantia que torna a etapa segura de aplicar incondicionalmente.
O que ele rende em overhead de amostragem
Limites mais apertados significam menos termos de erro a mitigar para uma dada tolerância de viés, que é o número que importa no 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
Quando usar
Aplique-o aos limites diretos sempre que esses limites não estiverem totalmente convergidos (essencialmente,
sempre que você tiver trocado precisão por velocidade usando qualquer uma das opções do
guia de configuração). Ele é barato comparado ao cálculo dos limites,
não pode piorar nada e recupera boa parte do que um limite de max_num_boxes ou eigval_max_qubits
tinha cedido.
Em limites totalmente calculados ele não faz nada, então não há problema em aplicá-lo incondicionalmente.
Duas limitações:
-
Ele se aplica somente a limites diretos. Os limites inversos tratam do efeito do erro sobre o estado inicial, e não sobre o observável, então o argumento não se transfere.
-
O observável deve ser um único termo de Pauli. Para um observável com vários termos, chame isto uma vez por Pauli e combine os resultados você mesmo. Se o observável não for um único termo de Pauli, ele gera um
NotImplementedError.
No fluxo de trabalho padrão, ele pertence entre o cálculo dos limites diretos e sua mesclagem com os limites inversos, como mostra o guia de início rápido.