Cum să reglezi opțiunile de configurare
Calculul limitelor este exact în principiu, dar aproximativ în practică: evoluția unui termen de eroare Pauli printr-un circuit produce un operator cu un număr exponențial de termeni, iar găsirea normei spectrale a comutatorului rezultat este ea însăși costisitoare. Opțiunile de mai jos controlează acest cost. Ele se împart în trei grupuri:
-
Opțiunile de acuratețe modifică valoarea numerică a limitelor, schimbând strânsoarea limitelor pe cost.
-
Domeniul stabilește care limite sunt calculate.
-
Opțiunile de execuție modifică modul în care rulează calculul (nu ceea ce calculează).
| Opțiune | Implicit | Controlează | Efectul asupra rezultatului |
|---|---|---|---|
| Acuratețe | |||
evolution_max_terms | 1_000_000 | Câți termeni ai operatorului supraviețuiesc evoluției | Mai mic: mai ieftin, limite mai laxe |
atol_simplify | 1e-8 | Care termeni ai comutatorului sunt eliminați ca neglijabili | Mai mare: mai ieftin, limite mai laxe |
eigval_max_qubits | 14 | Cât de lat poate fi un comutator pentru ca norma sa spectrală să fie calculată | Mai mic: mai ieftin, limite mai laxe |
atol_eigenvalue | 1e-10 | Când se consideră că solverul iterativ de valori proprii a convergit | Mai mare: mai ieftin, limite mai puțin precise |
| Domeniu | |||
max_num_boxes | None | Pentru câte box-uri sunt calculate limite | Box-urile peste limită păstrează limita trivială de 2.0 |
| Execuție | |||
timeout | None | Un buget de timp real pentru întregul calcul | Orice lucru rămas păstrează limita trivială de 2.0 |
num_processes | 1 | Câte procese worker calculează limitele în paralel | Niciunul în sine |
O limită este utilă doar dacă este validă: nu trebuie să subestimeze niciodată efectul unui termen de eroare asupra
observabilei. Relaxarea oricăreia dintre aceste opțiuni, în afară de atol_eigenvalue, poate face limitele doar
mai mari (mai conservatoare), niciodată mai mici. Astfel, costul unei setări laxe este un overhead de eșantionare
mai mare, nu o valoare de așteptare greșită - atol_eigenvalue este excepția.
Definiția circuitului
Următoarele experimente folosesc același circuit Ising Trotterizat și aceeași observabilă ca ghidul de pornire rapidă. La această dimensiune, fiecare opțiune are un efect vizibil, iar fiecare calcul durează totuși sub o secundă.
# 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
Un mic ajutor rezumă fiecare set de limite într-o singură linie, astfel încât efectul unei opțiuni apare ca câteva numere, nu ca un perete de tablouri.
Fiecare limită se află între 0 și limita trivială de 2.0. Rezumatul raportează limita medie și abaterea standard, urmate de modul în care termenii se împart în aceste grupuri:
- Termeni limitați exact la 0 deoarece comută cu observabila evoluată
- Termeni cu o limită calculată între 0 și 2.0
- Termeni rămași la valoarea trivială 2.0
Reține că o medie mai mică este mai strânsă, iar mai strâns înseamnă un overhead de eșantionare mai mic mai târziu.
O parcurgere a unei opțiuni marchează valoarea implicită a opțiunii cu *:
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
Acuratețe
evolution_max_terms
Evoluția unui termen de eroare Pauli prin porți non-Clifford îl transformă într-o sumă de operatori Pauli, iar acea sumă
crește odată cu adâncimea circuitului. evolution_max_terms plafonează câți dintre acești termeni sunt păstrați. Norma 1 a
tot ce este eliminat se adaugă la limită, deci nimic nu se pierde în tăcere.
Parcurgerea merge de la o valoare mai strânsă decât cea implicită până la o tăiere extremă. Reducerea plafonului face calculul mai ieftin și limitele mai laxe.
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
Ridicarea plafonului peste valoarea implicită nu schimbă nimic aici. Pentru un circuit de această dimensiune, valoarea implicită păstrează deja fiecare termen, deci limitele sunt la fel de strânse pe cât le poate face această opțiune. Aceasta este situația tipică. Strângerea unei opțiuni de acuratețe merită doar atunci când valoarea ei implicită chiar limitează, iar modul de a afla este experimentul de mai sus. Rulează-l din nou cu o valoare mai strânsă și vezi dacă limitele se mișcă.
Avertisment: Rutina de propagare de dedesubt tratează evolution_max_terms ca pe o capacitate de
pre-alocat, nu ca pe o limită superioară față de care se verifică. Transmiterea unei valori nominal nelimitate, precum
numpy.iinfo(numpy.uint).max, cere alocatorului terabyți de memorie și oprește procesul.
Păstreaz-o la un număr realist. Valoarea implicită de un milion este deja generoasă.
atol_simplify
După formarea comutatorului, termenii cu un coeficient sub atol_simplify sunt eliminați. Ca și în cazul
plafonului evoluției, norma 1 pierdută astfel se adaugă la limită.
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
Cea mai strânsă limită disponibilă este norma spectrală a comutatorului, care necesită un calcul de valori
proprii. Acesta devine costisitor pe măsură ce comutatorul se lărgește, așa că eigval_max_qubits stabilește lățimea
peste care încercarea este abandonată în favoarea inegalității triunghiului. Acea limită,
peste coeficienții comutatorului , este validă, dar mai laxă.
Setarea ei la zero forțează varianta de rezervă peste tot, ceea ce arată beneficiul solverului de valori proprii într-un singur număr.
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
Chiar și cu solverul de valori proprii oprit, media rămâne mult sub valoarea trivială 2.0. Ponderea termenilor limitați la 0 nu se schimbă: un termen care comută exact cu observabila evoluată are un comutator zero, lucru recunoscut înainte de a se lua în considerare orice calcul de valori proprii. Dintre ceilalți, limita din inegalitatea triunghiului ajunge la 2.0 pentru unii și este plafonată acolo, dar pentru mulți este totuși mai mică, deci păstrează o limită calculată sub 2.0.
atol_eigenvalue
Când norma spectrală este calculată, atol_eigenvalue este toleranța de convergență a
solverului iterativ de valori proprii. Majoritatea comutatorilor nu ajung însă niciodată la acel solver:
-
Un comutator care acționează cel mult pe patru qubiți este diagonalizat exact, direct.
-
Unul mai lat este mai întâi redus la cel mai mic număr de qubiți de care au nevoie termenii săi Pauli, iar dacă acea formă redusă acționează cel mult pe opt qubiți, este diagonalizat exact.
Doar un comutator care, după această reducere, are încă nevoie de mai mult de opt qubiți ajunge la solverul iterativ, singurul loc în care se aplică această toleranță. Circuitele de dimensiunea celor folosite în acest ghid nu ajung niciodată atât de departe, așa că o parcurgere a acestei opțiuni ar arăta limite identice pentru fiecare valoare. Contează pentru con-uri de lumină late pe dispozitive mari.
Spre deosebire de celelalte opțiuni de acuratețe, relaxarea atol_eigenvalue nu este sigură într-o direcție. Un
solver iterativ de valori proprii se apropie de valoarea proprie extremă dinăuntrul spectrului, deci o estimare
care nu a convergit complet subestimează norma spectrală și limita. Valoarea implicită este
intenționat strânsă din acest motiv. Dacă solverul nu reușește să conveargă, varianta de rezervă cu inegalitatea
triunghiului preia controlul, ceea ce este din nou valid.
Domeniu
max_num_boxes
Uneori limita utilă nu este acuratețea, ci adâncimea. max_num_boxes oprește lansarea calculelor de limite
peste un anumit număr de box-uri, iar fiecare strat de dincolo de el primește în schimb limita trivială
de 2.0.
Limita numără instrucțiunile BoxOp zgomotoase (cele care poartă o adnotare InjectNoise) în loc
să numere, de exemplu, o adâncime în porți. Astfel rămâne legată de structura circuitului vizibilă utilizatorului și
evită cuplarea de referințele de adnotare generate automat, care se pot schimba între
rulările de transpilare.
Aceasta este alegerea naturală pentru un circuit ale cărui două jumătăți sunt tratate respectiv prin limite înainte și înapoi: calculează fiecare limită doar atât de adânc cât va fi folosită.
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ție
timeout
timeout este un buget de timp real în secunde. Când expiră, limitele care au fost calculate sunt păstrate,
iar tot ce este în așteptare păstrează limita trivială de 2.0, deci rezultatul este întotdeauna utilizabil, doar
mai lax cu cât îl întrerupi mai devreme.
Nu este un termen limită strict: sarcinilor deja în rulare li se permite să se termine, deci apelul poate depăși bugetul cu durata unei singure sarcini.
num_processes
Limitele pentru diferiți termeni de eroare sunt independente, așa că num_processes le distribuie pe procese
worker. Singur, acesta afectează doar cât durează calculul, niciodată rezultatul.
Notă: Un timeout combinat cu num_processes face ca rezultatul să depindă de timp: care sarcini
se termină înainte de expirarea bugetului nu este determinist. Limitele rămân valide, dar nu mai sunt
reproductibile de la o rulare la alta.
De unde să începi
Valorile implicite sunt alese pentru a oferi limite strânse pe circuite de o dimensiune la care acest lucru este accesibil. Când un calcul este prea lent, încearcă să aplici opțiunile aproximativ în această ordine:
-
num_processes: O accelerare gratuită, fără cost de acuratețe. -
max_num_boxes: Dacă știi dinainte cât de adânc în circuit te interesează. -
eigval_max_qubits: Omite pasul cel mai costisitor pentru cei mai lați comutatori, care sunt de obicei o mică minoritate a termenilor. -
evolution_max_termsșiatol_simplify: Acestea afectează acuratețea peste tot, așa că reglează-le ultimele, verificând cu o parcurgere ca cele de mai sus cât se mișcă limitele. -
timeout: O oprire forțată, nu un ajutor de reglare, pentru cazul în care un calcul trebuie să se termine până la un termen limită, indiferent de calitate.
Cu cât limitele sunt mai laxe, cu atât overhead-ul de eșantionare raportat de compute_local_scales va fi mai mare pentru
o toleranță dată a deplasării. Acel overhead este mărimea care merită urmărită în timpul reglării: îți arată ce
costă de fapt aproximările.