Jak dostroić opcje konfiguracji
Obliczanie ograniczeń jest w zasadzie dokładne, ale w praktyce przybliżone: ewolucja członu błędu Pauliego przez obwód daje operator o wykładniczo wielu członach, a wyznaczenie normy spektralnej wynikowego komutatora jest samo w sobie kosztowne. Poniższe opcje kontrolują ten koszt. Dzielą się na trzy grupy:
-
Opcje dokładności zmieniają wartość liczbową ograniczeń, wymieniając ich ścisłość na koszt.
-
Zakres określa, które ograniczenia są obliczane.
-
Opcje wykonania zmieniają, jak przebiega obliczenie (a nie co jest obliczane).
| Opcja | Domyślnie | Kontroluje | Wpływ na wynik |
|---|---|---|---|
| Dokładność | |||
evolution_max_terms | 1_000_000 | Ile członów operatora przetrwa ewolucję | Niżej: taniej, luźniejsze ograniczenia |
atol_simplify | 1e-8 | Które człony komutatora są odrzucane jako pomijalne | Wyżej: taniej, luźniejsze ograniczenia |
eigval_max_qubits | 14 | Jak szeroki może być komutator, aby obliczyć jego normę spektralną | Niżej: taniej, luźniejsze ograniczenia |
atol_eigenvalue | 1e-10 | Kiedy iteracyjny solver wartości własnych uznaje się za zbieżny | Wyżej: taniej, mniej dokładne ograniczenia |
| Zakres | |||
max_num_boxes | None | Dla ilu bloków obliczane są ograniczenia | Bloki poza limitem zachowują trywialne ograniczenie 2.0 |
| Wykonanie | |||
timeout | None | Budżet czasu rzeczywistego dla całego obliczenia | Pozostała praca zachowuje trywialne ograniczenie 2.0 |
num_processes | 1 | Ile procesów roboczych oblicza ograniczenia równolegle | Samo w sobie żaden |
Ograniczenie jest użyteczne tylko wtedy, gdy jest poprawne: nigdy nie może zaniżać wpływu członu błędu na
obserwabl. Poluzowanie którejkolwiek z tych opcji poza atol_eigenvalue może jedynie zwiększyć
ograniczenia (uczynić je bardziej zachowawczymi), nigdy zmniejszyć. Zatem kosztem luźnego ustawienia jest większy narzut
próbkowania, a nie błędna wartość oczekiwana - wyjątkiem jest atol_eigenvalue.
Definicja obwodu
Poniższe eksperymenty używają tego samego trotteryzowanego obwodu Isinga i obserwabla co przewodnik szybkiego startu. Przy tym rozmiarze każda opcja ma widoczny wpływ, a każde obliczenie nadal trwa krócej niż sekundę.
# 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
Mała funkcja pomocnicza podsumowuje każdy zestaw ograniczeń w jednym wierszu, dzięki czemu wpływ opcji widać jako kilka liczb, a nie ścianę tablic.
Każde ograniczenie leży między 0 a trywialnym ograniczeniem 2.0. Podsumowanie podaje średnie ograniczenie i jego odchylenie standardowe, a następnie to, jak człony rozkładają się na te grupy:
- Człony ograniczone dokładnie przez 0, ponieważ komutują z ewoluowanym obserwablem
- Człony z obliczonym ograniczeniem między 0 a 2.0
- Człony pozostawione przy trywialnym 2.0
Zwróć uwagę, że niższa średnia oznacza ściślejsze ograniczenie, a ściślejsze oznacza mniejszy narzut próbkowania później.
Przegląd po jednej opcji oznacza wartość domyślną opcji symbolem *:
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
Dokładność
evolution_max_terms
Ewolucja członu błędu Pauliego przez bramki nie-Cliffordowskie zamienia go w sumę operatorów Pauliego, a ta suma
rośnie wraz z głębokością obwodu. evolution_max_terms ogranicza liczbę zachowywanych członów. 1-norma
tego, co zostanie odrzucone, jest dodawana do ograniczenia, więc nic nie ginie po cichu.
Przegląd obejmuje wartości od ściślejszych niż domyślna aż po skrajne cięcie. Obniżanie limitu czyni obliczenie tańszym, a ograniczenia luźniejszymi.
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
Podniesienie limitu ponad wartość domyślną niczego tu nie zmienia. Dla obwodu tego rozmiaru wartość domyślna już zachowuje każdy człon, więc ograniczenia są tak ścisłe, jak ta opcja pozwala. To typowa sytuacja. Zaostrzanie opcji dokładności opłaca się dopiero wtedy, gdy jej wartość domyślna faktycznie ogranicza, a sposobem, by się o tym przekonać, jest powyższy eksperyment. Uruchom go ponownie ze ściślejszą wartością i sprawdź, czy ograniczenia się zmieniają.
Ostrzeżenie: Wewnętrzna procedura propagacji traktuje evolution_max_terms jako pojemność do
wstępnej alokacji, a nie górny limit do sprawdzenia. Przekazanie wartości nominalnie nieograniczonej, takiej
jak numpy.iinfo(numpy.uint).max, powoduje żądanie terabajtów pamięci od alokatora i przerwanie procesu.
Używaj realistycznej liczby. Wartość domyślna, milion, jest już hojna.
atol_simplify
Po utworzeniu komutatora człony ze współczynnikiem poniżej atol_simplify są odrzucane. Podobnie jak
w przypadku limitu ewolucji, utracona w ten sposób 1-norma jest dodawana do ograniczenia.
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
Najściślejszym dostępnym ograniczeniem jest norma spektralna komutatora, która wymaga obliczenia wartości
własnych. Staje się to kosztowne, gdy komutator się poszerza, więc eigval_max_qubits ustala szerokość,
powyżej której próba jest porzucana na rzecz nierówności trójkąta. To ograniczenie,
po współczynnikach komutatora , jest poprawne, ale luźniejsze.
Ustawienie zera wymusza użycie rozwiązania zastępczego wszędzie, co pokazuje korzyść z solvera wartości własnych w jednej liczbie.
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
Nawet po wyłączeniu solvera wartości własnych średnia pozostaje daleko poniżej trywialnego 2.0. Udział członów ograniczonych przez 0 się nie zmienia: człon, który dokładnie komutuje z ewoluowanym obserwablem, ma zerowy komutator, co jest rozpoznawane, zanim w ogóle rozważy się obliczanie wartości własnych. Wśród pozostałych ograniczenie z nierówności trójkąta dla niektórych sięga 2.0 i jest tam przycinane, ale dla wielu jest nadal mniejsze, więc zachowują obliczone ograniczenie poniżej 2.0.
atol_eigenvalue
Gdy norma spektralna jest obliczana, atol_eigenvalue jest tolerancją zbieżności
iteracyjnego solvera wartości własnych. Większość komutatorów jednak nigdy do tego solvera nie trafia:
-
Komutator działający na co najwyżej czterech kubitach jest diagonalizowany dokładnie, bezpośrednio.
-
Szerszy jest najpierw redukowany do najmniejszej liczby kubitów, jakiej potrzebują jego człony Pauliego, i jeśli ta zredukowana postać działa na co najwyżej ośmiu kubitach, jest diagonalizowana dokładnie.
Tylko komutator, który po tej redukcji nadal wymaga więcej niż ośmiu kubitów, trafia do iteracyjnego solvera, czyli jedynego miejsca, w którym ta tolerancja ma zastosowanie. Obwody o rozmiarze użytym w tym przewodniku nigdy tam nie docierają, więc przegląd po tej opcji pokazałby identyczne ograniczenia dla każdej wartości. Ma ona znaczenie dla szerokich stożków światła na dużych urządzeniach.
W przeciwieństwie do pozostałych opcji dokładności, poluzowanie atol_eigenvalue nie jest bezpieczne w jednym kierunku. Iteracyjny
solver wartości własnych zbliża się do skrajnej wartości własnej od wnętrza widma, więc oszacowanie,
które nie zbiegło się w pełni, zaniża normę spektralną, a tym samym ograniczenie. Z tego powodu wartość domyślna jest
celowo ścisła. Jeśli solver nie zbiegnie się, przejmuje rozwiązanie zastępcze z nierówności trójkąta,
które jest znów poprawne.
Zakres
max_num_boxes
Czasem użytecznym limitem jest nie dokładność, lecz głębokość. max_num_boxes przestaje uruchamiać obliczenia ograniczeń
po danej liczbie bloków, a każdej warstwie poza nią przypisywane jest zamiast tego trywialne ograniczenie
2.0.
Limit zlicza zaszumione instrukcje BoxOp (te z adnotacją InjectNoise), a nie
na przykład głębokość mierzoną w bramkach. Dzięki temu jest związany z widoczną dla użytkownika strukturą obwodu
i unika sprzężenia z automatycznie generowanymi odwołaniami do adnotacji, które mogą zmieniać się między
uruchomieniami transpilacji.
To naturalny wybór dla obwodu, którego dwie połowy obsługują odpowiednio ograniczenia w przód i wstecz: oblicz każde tylko tak głęboko, jak będzie używane.
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
Wykonanie
timeout
timeout to budżet czasu rzeczywistego w sekundach. Po jego upływie obliczone ograniczenia są zachowywane,
a wszystko, co zostało, zachowuje trywialne ograniczenie 2.0, więc wynik jest zawsze użyteczny, tylko
tym luźniejszy, im wcześniej przerwiesz.
To nie jest twardy termin: już działające zadania mogą się zakończyć, więc wywołanie może przekroczyć budżet o czas trwania jednego zadania.
num_processes
Ograniczenia dla różnych członów błędu są niezależne, więc num_processes rozdziela je między procesy
robocze. Samo w sobie wpływa to tylko na czas obliczenia, nigdy na jego wynik.
Uwaga: timeout w połączeniu z num_processes sprawia, że wynik zależy od czasu: to, które zadania
zakończą się przed upływem budżetu, nie jest deterministyczne. Ograniczenia pozostają poprawne, ale nie są już
powtarzalne między uruchomieniami.
Od czego zacząć
Wartości domyślne dobrano tak, by dawały ścisłe ograniczenia dla obwodów o rozmiarze, przy którym jest to opłacalne. Gdy obliczenie jest zbyt wolne, spróbuj stosować opcje mniej więcej w tej kolejności:
-
num_processes: Darmowe przyspieszenie, bez utraty dokładności. -
max_num_boxes: Jeśli z góry wiesz, jak głęboko w obwodzie ci zależy. -
eigval_max_qubits: Pomija najkosztowniejszy krok dla najszerszych komutatorów, które zwykle stanowią niewielką mniejszość członów. -
evolution_max_termsiatol_simplify: Wpływają one na dokładność w całości, więc dostrajaj je na końcu, sprawdzając przeglądem podobnym do powyższych, jak bardzo zmieniają się ograniczenia. -
timeout: Twarde zatrzymanie, a nie pomoc w dostrajaniu, na wypadek gdy obliczenie musi zakończyć się w terminie, niezależnie od jakości.
Im luźniejsze ograniczenia, tym większy narzut próbkowania, o którym poinformuje compute_local_scales dla
danej tolerancji błędu systematycznego. Ten narzut jest wielkością wartą obserwowania podczas dostrajania: mówi, ile
faktycznie kosztują przybliżenia.