Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

Como ajustar as opções de configuração

O cálculo dos limites é exato em princípio, mas aproximado na prática: evoluir um termo de erro de Pauli por um circuito produz um operador com um número exponencial de termos, e encontrar a norma espectral do comutador resultante já é, por si só, caro. As opções abaixo controlam esse custo. Elas se dividem em três grupos:

  • As opções de Precisão mudam o valor numérico dos limites, trocando justeza por custo.

  • O Escopo determina quais limites são calculados.

  • As opções de Execução mudam como o cálculo é executado (não o que ele calcula).

OpçãoPadrãoControlaEfeito no resultado
Precisão
evolution_max_terms1_000_000Quantos termos do operador sobrevivem à evoluçãoMenor: mais barato, limites mais frouxos
atol_simplify1e-8Quais termos do comutador são descartados por serem desprezíveisMaior: mais barato, limites mais frouxos
eigval_max_qubits14Quão largo um comutador pode ser para que sua norma espectral seja calculadaMenor: mais barato, limites mais frouxos
atol_eigenvalue1e-10Quando o autossolucionador iterativo é considerado convergidoMaior: mais barato, limites menos precisos
Escopo
max_num_boxesNonePara quantas caixas os limites são calculadosAs caixas além do limite mantêm o limite trivial de 2.0
Execução
timeoutNoneUm orçamento de tempo real para todo o cálculoQualquer trabalho restante mantém o limite trivial de 2.0
num_processes1Quantos processos de trabalho calculam limites em paraleloNenhum por si só

Um limite só é útil se for válido: ele nunca deve subestimar o efeito de um termo de erro sobre o observável. Afrouxar qualquer uma dessas opções, exceto atol_eigenvalue, só pode tornar os limites maiores (mais conservadores), nunca menores. Assim, o custo de uma configuração frouxa é um overhead de amostragem maior, não um valor esperado errado - atol_eigenvalue é a exceção.

Definição do circuito​

Os experimentos a seguir usam o mesmo circuito de Ising com Trotter e o mesmo observável do guia de início rápido. Nesse tamanho, cada opção tem um efeito visível, enquanto cada cálculo ainda leva menos de um segundo.

# 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.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
)
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)
)

print(f"noisy layers: {len(noise_model_paulis)}")
print(
f"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}"
)
noisy layers: 2
Pauli error terms: 198

Uma pequena função auxiliar resume cada conjunto de limites em uma linha, de modo que o efeito de uma opção apareça como alguns números em vez de uma parede de arrays.

Todo limite fica entre 0 e o limite trivial de 2.0. O resumo informa o limite médio e seu desvio padrão, seguidos de como os termos se distribuem nestes grupos:

  • Termos limitados por exatamente 0 porque comutam com o observável evoluído
  • Termos com um limite calculado entre 0 e 2.0
  • Termos que ficaram no 2.0 trivial

Observe que uma média menor é mais justa, e mais justa significa um overhead de amostragem menor depois.

Uma varredura sobre uma opção marca o padrão dela com *:

import inspect
import time

from qiskit_addon_slc.bounds import compute_forward_bounds

def summarize(rates, label, elapsed):
"""Prints a one-line summary of a set of bounds."""
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:28} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%} │ "
f"{elapsed:4.2f}s"
)

def forward(label, **kwargs):
"""Computes the forward bounds, summarizes them and returns all their rates."""
start = time.perf_counter()
bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, **kwargs
)
elapsed = time.perf_counter() - start
rates = np.concatenate([bound.rates for bound in bounds.values()])
summarize(rates, label, elapsed)
return rates

def sweep(option, values):
"""Computes the forward bounds for each value of one option, marking the default with `*`."""
default = (
inspect.signature(compute_forward_bounds).parameters[option].default
)
for value in values:
marker = "*" if value == default else " "
forward(f"{marker} {option}={value:g}", **{option: value})

baseline = forward(" defaults")
defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s

Precisão​

evolution_max_terms​

Evoluir um termo de erro de Pauli por portas não Clifford o transforma em uma soma de Paulis, e essa soma cresce com a profundidade do circuito. evolution_max_terms limita quantos desses termos são mantidos. A norma 1 de tudo o que é descartado é somada ao limite, então nada se perde silenciosamente.

A varredura vai de mais justa que o padrão até um corte extremo. Reduzir o limite torna o cálculo mais barato e os limites mais frouxos.

sweep("evolution_max_terms", (10_000_000, 1_000_000, 1000, 100, 10, 2))
evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s
* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s
evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s

Aumentar o limite além do padrão não muda nada aqui. Para um circuito desse tamanho, o padrão já mantém todos os termos, então os limites são tão justos quanto essa opção consegue torná-los. Essa é a situação típica. Apertar uma opção de precisão só compensa quando o padrão realmente está limitando, e a forma de descobrir isso é o experimento acima. Execute-o de novo com um valor mais justo e veja se os limites se movem.

Aviso: A rotina de propagação subjacente trata evolution_max_terms como uma capacidade a pré-alocar, não como um limite superior a verificar. Passar algo nominalmente ilimitado, como numpy.iinfo(numpy.uint).max, pede ao alocador terabytes de memória e aborta o processo. Mantenha um número realista. O padrão de um milhão já é generoso.

atol_simplify​

Depois que o comutador é formado, os termos com coeficiente abaixo de atol_simplify são descartados. Assim como com o limite de evolução, a norma 1 perdida dessa forma é somada ao limite.

sweep("atol_simplify", (0.0, 1e-8, 1e-4, 1e-2, 1e-1))
atol_simplify=0 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
* atol_simplify=1e-08 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
atol_simplify=0.0001 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
atol_simplify=0.01 mean 0.3656 ± 0.685 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.31s
atol_simplify=0.1 mean 0.3837 ± 0.697 │ at 0: 59% computed: 31% at 2.0: 10% │ 0.31s

eigval_max_qubits​

O limite mais justo disponível é a norma espectral do comutador, que exige um cálculo de autovalores. Isso fica caro à medida que o comutador se alarga, então eigval_max_qubits define a largura acima da qual a tentativa é abandonada em favor da desigualdade triangular. Esse limite, 2∑i∣ci∣2 \sum_i |c_i| sobre os coeficientes cic_i do comutador, é válido, mas mais frouxo.

Defini-lo como zero força o recurso alternativo em todos os casos, o que mostra o benefício do autossolucionador em um único número.

sweep("eigval_max_qubits", (20, 14, 8, 4, 2, 0))
eigval_max_qubits=20 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
* eigval_max_qubits=14 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
eigval_max_qubits=8 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
eigval_max_qubits=4 mean 0.3742 ± 0.694 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.30s
eigval_max_qubits=2 mean 0.4983 ± 0.809 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.27s
eigval_max_qubits=0 mean 0.5081 ± 0.810 │ at 0: 59% computed: 21% at 2.0: 19% │ 0.27s

Mesmo com o autossolucionador desligado, a média permanece bem abaixo do 2.0 trivial. A fração de termos limitados por 0 não muda: um termo que comuta exatamente com o observável evoluído tem comutador zero, o que é reconhecido antes de qualquer cálculo de autovalores ser considerado. Dos demais, o limite da desigualdade triangular chega a 2.0 para alguns e é truncado ali, mas para muitos ainda é menor, então eles mantêm um limite calculado abaixo de 2.0.

atol_eigenvalue​

Quando a norma espectral é calculada, atol_eigenvalue é a tolerância de convergência do autossolucionador iterativo. A maioria dos comutadores, porém, nunca chega a esse solucionador:

  • Um comutador que atua em no máximo quatro qubits é diagonalizado diretamente, de forma exata.

  • Um comutador mais largo é primeiro reduzido ao menor número de qubits de que seus termos de Pauli precisam, e se essa forma reduzida atua em no máximo oito qubits, ele é diagonalizado de forma exata.

Somente um comutador que ainda precisa de mais de oito qubits após essa redução vai para o solucionador iterativo, que é o único lugar onde essa tolerância se aplica. Circuitos do tamanho usado neste guia nunca chegam até lá, então uma varredura sobre essa opção mostraria limites idênticos para todos os valores. Ela importa para cones de luz largos em dispositivos grandes.

Ao contrário das outras opções de precisão, afrouxar atol_eigenvalue não é seguro em uma direção. Um autossolucionador iterativo se aproxima do autovalor extremo vindo de dentro do espectro, então uma estimativa que não convergiu totalmente subestima a norma espectral e, com ela, o limite. O padrão é deliberadamente justo por esse motivo. Se o solucionador não converge, o recurso alternativo da desigualdade triangular assume, o que volta a ser válido.

Escopo​

max_num_boxes​

Às vezes o limite útil não é a precisão, mas a profundidade. max_num_boxes deixa de iniciar cálculos de limites após um determinado número de caixas, e cada camada além dele recebe o limite trivial de 2.0.

O limite conta as instruções BoxOp ruidosas (aquelas com uma anotação InjectNoise) em vez de, por exemplo, uma profundidade em portas. Isso o mantém ligado à estrutura do circuito visível ao usuário e evita o acoplamento às referências de anotação geradas automaticamente, que podem mudar entre execuções de transpilação.

Essa é a escolha natural para um circuito cujas duas metades são tratadas por limites direto e inverso, respectivamente: calcule cada um apenas até a profundidade em que será usado.

from qiskit.circuit import BoxOp
from samplomatic import InjectNoise

# The limit counts the noisy boxes encountered while iterating the circuit, not the unique ones:
# a repeated layer is one noise model but several boxes. Boxes without an `InjectNoise`
# annotation carry no error terms and do not count.
num_boxes = sum(
1
for inst in boxed_circuit.data
if isinstance(inst.operation, BoxOp)
and any(
isinstance(annotation, InjectNoise)
for annotation in inst.operation.annotations
)
)
print(
f"the circuit has {num_boxes} noisy boxes drawn from "
f"{len(noise_model_paulis)} distinct noise models\n"
)

for limit in (None, 6, 4, 2, 1):
marker = "*" if limit is None else " "
forward(f"{marker} max_num_boxes={limit}", max_num_boxes=limit)
the circuit has 9 noisy boxes drawn from 2 distinct noise models

* max_num_boxes=None mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
max_num_boxes=6 mean 0.7486 ± 0.935 │ at 0: 53% computed: 18% at 2.0: 30% │ 0.18s
max_num_boxes=4 mean 1.1506 ± 0.980 │ at 0: 40% computed: 5% at 2.0: 55% │ 0.11s
max_num_boxes=2 mean 1.5707 ± 0.821 │ at 0: 21% computed: 0% at 2.0: 79% │ 0.05s
max_num_boxes=1 mean 1.8157 ± 0.579 │ at 0: 9% computed: 0% at 2.0: 91% │ 0.03s

Execução​

timeout​

timeout é um orçamento de tempo real em segundos. Quando ele expira, os limites já calculados são mantidos e tudo o que estiver pendente mantém o limite trivial de 2.0, então o resultado é sempre utilizável, só que mais frouxo quanto mais cedo você o interrompe.

Não é um prazo rígido: as tarefas já em execução podem terminar, então a chamada pode ultrapassar o orçamento pela duração de uma tarefa.

num_processes​

Os limites de diferentes termos de erro são independentes, então num_processes os distribui entre processos de trabalho. Por si só, isso afeta apenas quanto tempo o cálculo leva, nunca o resultado.

Nota: Combinar timeout com num_processes faz o resultado depender do tempo: quais tarefas terminam antes de o orçamento expirar não é determinístico. Os limites continuam válidos, mas deixam de ser reprodutíveis de uma execução para outra.

Por onde começar​

Os padrões são escolhidos para dar limites justos em circuitos de um tamanho em que isso é viável. Quando um cálculo está lento demais, tente aplicar as opções mais ou menos nesta ordem:

  1. num_processes: Um ganho de velocidade gratuito, sem custo de precisão.

  2. max_num_boxes: Se você sabe de antemão até que profundidade do circuito lhe interessa.

  3. eigval_max_qubits: Pula a etapa mais cara para os comutadores mais largos, que costumam ser uma pequena minoria dos termos.

  4. evolution_max_terms e atol_simplify: Elas afetam a precisão como um todo, então ajuste-as por último, conferindo com uma varredura como as acima o quanto os limites se movem.

  5. timeout: Uma parada forçada, mais do que um auxílio de ajuste, para quando um cálculo precisa terminar até um prazo, seja qual for a qualidade.

Quanto mais frouxos os limites, maior o overhead de amostragem que compute_local_scales informará para uma dada tolerância de viés. Esse overhead é a grandeza que vale acompanhar durante o ajuste: ele mostra o que as aproximações realmente custam.