Przejdź do głównej treści
Ten projekt jest w wersji beta. Znalazłeś błąd? Otwórz zgłoszenie na GitHub. Masz pytanie lub pomysł? Zapytaj w Discussions — chętnie poznamy Twoją opinię!
Jak oceniasz jakość tłumaczenia tej strony?

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

OpcjaDomyślnieKontrolujeWpływ na wynik
Dokładność
evolution_max_terms1_000_000Ile członów operatora przetrwa ewolucjęNiżej: taniej, luźniejsze ograniczenia
atol_simplify1e-8Które człony komutatora są odrzucane jako pomijalneWyżej: taniej, luźniejsze ograniczenia
eigval_max_qubits14Jak szeroki może być komutator, aby obliczyć jego normę spektralnąNiżej: taniej, luźniejsze ograniczenia
atol_eigenvalue1e-10Kiedy iteracyjny solver wartości własnych uznaje się za zbieżnyWyżej: taniej, mniej dokładne ograniczenia
Zakres
max_num_boxesNoneDla ilu bloków obliczane są ograniczeniaBloki poza limitem zachowują trywialne ograniczenie 2.0
Wykonanie
timeoutNoneBudżet czasu rzeczywistego dla całego obliczeniaPozostała praca zachowuje trywialne ograniczenie 2.0
num_processes1Ile procesów roboczych oblicza ograniczenia równolegleSamo 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, 2∑i∣ci∣2 \sum_i |c_i| po współczynnikach komutatora cic_i, 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:

  1. num_processes: Darmowe przyspieszenie, bez utraty dokładności.

  2. max_num_boxes: Jeśli z góry wiesz, jak głęboko w obwodzie ci zależy.

  3. eigval_max_qubits: Pomija najkosztowniejszy krok dla najszerszych komutatorów, które zwykle stanowią niewielką mniejszość członów.

  4. evolution_max_terms i atol_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.

  5. 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.