Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

Come regolare le opzioni di configurazione

Il calcolo dei limiti è esatto in linea di principio ma approssimato in pratica: far evolvere un termine di errore di Pauli attraverso un circuito produce un operatore con un numero esponenziale di termini, e trovare la norma spettrale del commutatore risultante è a sua volta costoso. Le opzioni seguenti controllano questo costo. Si dividono in tre gruppi:

  • Le opzioni di Accuratezza modificano il valore numerico dei limiti, bilanciando la precisione con il costo.

  • L'Ambito determina quali limiti vengono calcolati.

  • Le opzioni di Esecuzione modificano come viene eseguito il calcolo (non cosa calcola).

OpzionePredefinitoControllaEffetto sul risultato
Accuratezza
evolution_max_terms1_000_000Quanti termini dell'operatore sopravvivono all'evoluzionePiù basso: più economico, limiti più larghi
atol_simplify1e-8Quali termini del commutatore vengono scartati come trascurabiliPiù alto: più economico, limiti più larghi
eigval_max_qubits14Quanto può essere largo un commutatore perché ne venga calcolata la norma spettralePiù basso: più economico, limiti più larghi
atol_eigenvalue1e-10Quando l'autosolver iterativo si considera convergentePiù alto: più economico, limiti meno accurati
Ambito
max_num_boxesNonePer quanti box vengono calcolati i limitiI box oltre il limite mantengono il limite banale di 2.0
Esecuzione
timeoutNoneUn budget di tempo reale per l'intero calcoloOgni lavoro rimanente mantiene il limite banale di 2.0
num_processes1Quanti processi worker calcolano i limiti in paralleloNessuno di per sé

Un limite è utile solo se è valido: non deve mai sottostimare l'effetto di un termine di errore sull'osservabile. Allentare una qualsiasi di queste opzioni diversa da atol_eigenvalue può solo rendere i limiti più grandi (più conservativi), mai più piccoli. Quindi, il costo di un'impostazione lasca è un overhead di campionamento maggiore, non un valore di aspettazione sbagliato - atol_eigenvalue è l'eccezione.

Definizione del circuito​

I seguenti esperimenti usano lo stesso circuito di Ising con Trotter e la stessa osservabile della guida rapida. A questa dimensione, ogni opzione ha un effetto visibile mentre ogni calcolo richiede comunque meno di un secondo.

# 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

Una piccola funzione di supporto riassume ogni insieme di limiti in una riga, così l'effetto di un'opzione si vede in poche cifre invece che in un muro di array.

Ogni limite è compreso tra 0 e il limite banale di 2.0. Il riepilogo riporta il limite medio e la sua deviazione standard, seguiti da come i termini si distribuiscono in questi gruppi:

  • Termini con limite esattamente 0 perché commutano con l'osservabile evoluta
  • Termini con un limite calcolato tra 0 e 2.0
  • Termini lasciati al banale 2.0

Nota che una media più bassa è più stretta, e più stretta significa un overhead di campionamento minore in seguito.

Una scansione su un'opzione contrassegna il valore predefinito dell'opzione con *:

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

Accuratezza​

evolution_max_terms​

Far evolvere un termine di errore di Pauli attraverso gate non-Clifford lo trasforma in una somma di Pauli, e tale somma cresce con la profondità del circuito. evolution_max_terms limita quanti di questi termini vengono mantenuti. La norma 1 di ciò che viene scartato viene aggiunta al limite, quindi nulla va perso silenziosamente.

La scansione va da un valore più stretto del predefinito fino a un taglio estremo. Abbassare il tetto rende il calcolo più economico e i limiti più larghi.

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

Alzare il tetto oltre il valore predefinito qui non cambia nulla. Per un circuito di questa dimensione, il valore predefinito mantiene già ogni termine, quindi i limiti sono i più stretti che questa opzione possa ottenere. Questa è la situazione tipica. Restringere un'opzione di accuratezza conviene solo quando il suo valore predefinito è effettivamente limitante, e il modo per scoprirlo è l'esperimento qui sopra. Rieseguilo con un valore più stretto e verifica se i limiti si muovono.

Attenzione: La routine di propagazione sottostante tratta evolution_max_terms come una capacità da pre-allocare, non come un limite superiore da verificare. Passare qualcosa di nominalmente illimitato, come numpy.iinfo(numpy.uint).max, chiede all'allocatore terabyte di memoria e interrompe il processo. Mantienilo a un numero realistico. Il valore predefinito di un milione è già generoso.

atol_simplify​

Dopo che il commutatore è stato formato, i termini con un coefficiente inferiore a atol_simplify vengono scartati. Come per il tetto dell'evoluzione, la norma 1 persa in questo modo viene aggiunta al 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​

Il limite più stretto disponibile è la norma spettrale del commutatore, che richiede un calcolo degli autovalori. Questo diventa costoso man mano che il commutatore si allarga, quindi eigval_max_qubits imposta la larghezza oltre la quale il tentativo viene abbandonato a favore della disuguaglianza triangolare. Quel limite, 2∑i∣ci∣2 \sum_i |c_i| sui coefficienti cic_i del commutatore, è valido ma più largo.

Impostarlo a zero forza ovunque il ripiego, il che mostra il beneficio dell'autosolver in un solo numero.

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

Anche con l'autosolver disattivato, la media resta ben al di sotto del banale 2.0. La quota di termini con limite 0 non cambia: un termine che commuta esattamente con l'osservabile evoluta ha un commutatore nullo, che viene riconosciuto prima che si consideri qualsiasi calcolo degli autovalori. Tra gli altri, il limite della disuguaglianza triangolare raggiunge 2.0 per alcuni e lì viene limitato, ma per molti è comunque più piccolo, quindi mantengono un limite calcolato inferiore a 2.0.

atol_eigenvalue​

Quando la norma spettrale viene calcolata, atol_eigenvalue è la tolleranza di convergenza dell' autosolver iterativo. La maggior parte dei commutatori però non raggiunge mai quel solver:

  • Un commutatore che agisce al massimo su quattro qubit viene diagonalizzato direttamente in modo esatto.

  • Uno più largo viene prima ridotto al minor numero di qubit richiesto dai suoi termini di Pauli, e se quella forma ridotta agisce al massimo su otto qubit, viene diagonalizzato in modo esatto.

Solo un commutatore che dopo quella riduzione richiede ancora più di otto qubit passa al solver iterativo, che è l'unico posto in cui si applica questa tolleranza. I circuiti della dimensione usata in questa guida non arrivano mai a tanto, quindi una scansione su questa opzione mostrerebbe limiti identici per ogni valore. Conta per coni di luce ampi su dispositivi grandi.

A differenza delle altre opzioni di accuratezza, allentare atol_eigenvalue non è sicuro in una direzione. Un autosolver iterativo si avvicina all'autovalore estremo dall'interno dello spettro, quindi una stima non completamente convergente sottostima la norma spettrale, e il limite. Il valore predefinito è volutamente stretto per questo motivo. Se il solver non converge, subentra il ripiego sulla disuguaglianza triangolare, che è di nuovo valido.

Ambito​

max_num_boxes​

A volte il limite utile non è l'accuratezza ma la profondità. max_num_boxes smette di avviare i calcoli dei limiti oltre un dato numero di box, e a ogni livello successivo viene assegnato invece il limite banale di 2.0.

Il limite conta le istruzioni BoxOp rumorose (quelle che portano un'annotazione InjectNoise) piuttosto che, ad esempio, una profondità in gate. Questo lo lega alla struttura del circuito visibile all'utente e evita l'accoppiamento con i riferimenti di annotazione generati automaticamente, che possono cambiare tra esecuzioni di transpilazione.

Questa è la scelta naturale per un circuito le cui due metà sono gestite rispettivamente da limiti in avanti e all'indietro: calcola ciascuno solo fino alla profondità in cui verrà usato.

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

Esecuzione​

timeout​

timeout è un budget di tempo reale in secondi. Quando scade, i limiti già calcolati vengono mantenuti e tutto ciò che è in sospeso mantiene il limite banale di 2.0, quindi il risultato è sempre utilizzabile, solo più largo quanto prima lo interrompi.

Non è una scadenza rigida: i task già in esecuzione possono terminare, quindi la chiamata può superare il budget della durata di un task.

num_processes​

I limiti per diversi termini di errore sono indipendenti, quindi num_processes li distribuisce tra i processi worker. Di per sé, questo influisce solo sul tempo di calcolo, mai sul risultato.

Nota: Un timeout combinato con num_processes rende il risultato dipendente dalla tempistica: quali task terminano prima della scadenza del budget non è deterministico. I limiti restano validi, ma non sono più riproducibili da un'esecuzione all'altra.

Da dove iniziare​

I valori predefiniti sono scelti per dare limiti stretti su circuiti di una dimensione in cui ciò è sostenibile. Quando un calcolo è troppo lento, prova ad applicare le opzioni più o meno in quest'ordine:

  1. num_processes: Un'accelerazione gratuita, senza costo in accuratezza.

  2. max_num_boxes: Se sai in anticipo fino a che profondità del circuito ti interessa arrivare.

  3. eigval_max_qubits: Salta il passo più costoso per i commutatori più larghi, che di solito sono una piccola minoranza dei termini.

  4. evolution_max_terms e atol_simplify: Questi influiscono sull'accuratezza ovunque, quindi regolali per ultimi, verificando con una scansione come quelle qui sopra di quanto si muovono i limiti.

  5. timeout: Un arresto forzato più che uno strumento di regolazione, per quando un calcolo deve terminare entro una scadenza, qualunque sia la qualità.

Più larghi sono i limiti, maggiore è l'overhead di campionamento che compute_local_scales riporterà per una data tolleranza di bias. Quell'overhead è la quantità da tenere d'occhio durante la regolazione: ti dice quanto costano realmente le approssimazioni.