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 obliczać niestandardowe ograniczenia

compute_forward_bounds i compute_backward_bounds to wygodne opakowania. Oba są zbudowane z tych samych trzech elementów, które udostępnia publiczne API:

  • Funkcja normy, która ogranicza pojedynczy ewoluowany człon błędu Pauliego

  • Stożek światła, który ustala, w kierunku czego ewoluują człony błędu

  • compute_bounds, które iteruje po obwodzie i rozdziela pracę na poszczególne człony

Złożenie ich samodzielnie oznacza, że możesz ograniczyć coś, czego opakowania nie obejmują. Ten przewodnik przedstawia jeden przypadek, w którym warto to zrobić: obwód lustrzany, w którym zwykłe ograniczenie wsteczne jest niepotrzebnie luźne.

Co faktycznie robią opakowania​

Dwa opakowania różnią się w trzech miejscach wymienionych powyżej:

compute_forward_boundscompute_backward_bounds
obwódjak podanoremove_measure(circuit).inverse()
funkcja normytime_evolved_norm_forward, względem obserwablatime_evolved_norm_backward, względem stanu początkowego
stożek światłaLightCone.initialize_from_pauli, zasiany z obserwablaLightCone.initialize_from_measurements, zasiany ze wszystkich aktywnych kubitów
backwardsFalseTrue

Ograniczenie w przód pyta „Jak bardzo ten błąd może zmienić obserwabl, który zaraz zmierzę?” i odpowiada normą spektralną komutatora. Ograniczenie wsteczne pyta „Jak bardzo ten błąd może zmienić stan, od którego zacząłem?” i odpowiada normą jądrową, przyjmując, że tym stanem jest stan samych zer na każdym aktywnym kubicie. To różne pytania, a każde jest właściwe dla błędów w swojej połowie obwodu.

Przypadek obwodu lustrzanego​

Obwód lustrzany stosuje pewną unitarną operację, a następnie ją cofa: U†UU^\dagger U. Pojawiają się one zawsze wtedy, gdy idealny wynik musi być znany dokładnie (zwykle w echach Loschmidta, losowym benchmarkingu i eksperymentach weryfikacyjnych).

Obwody lustrzane mają własność, której ogólne ograniczenie wsteczne nie potrafi wykorzystać. Ponieważ druga połowa jest odwrotnością pierwszej, ewoluowanie błędu wstecz do stanu początkowego przechodzi przez te same bramki co ewoluowanie go w przód do obserwabla. Oba pytania się pokrywają, więc ostrzejsze ograniczenie w stylu „w przód” jest poprawne także dla kierunku wstecznego.

# 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
# The circuit of the quickstart guide...
forward_half = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# ...followed by its inverse. The ideal result is the initial state.
circuit = forward_half.compose(forward_half.inverse())

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"mirror circuit: {num_qubits} qubits, depth {circuit.depth()}")
print(f"noisy layers: {len(noise_model_paulis)}")
mirror circuit: 10 qubits, depth 24
noisy layers: 4

Złóż niestandardowe ograniczenie​

Przepis zachowuje to, co compute_backward_bounds robi z obwodem (usuwa pomiary, odwraca go i iteruje po nim z backwards=True), i podmienia pozostałe dwa elementy: funkcję normy w przód oraz stożek światła zasiany z obserwabla przez LightCone.initialize_from_pauli, dokładnie tak, jak compute_forward_bounds zasiewa własny. Każda podmiana to jedna linia.

from functools import partial

from qiskit_addon_slc.bounds import (
LightCone,
compute_bounds,
time_evolved_norm_forward,
)
from qiskit_addon_slc.utils import remove_measure

def compute_mirror_backward_bounds(
circuit,
noise_model_paulis,
/,
observable,
*,
evolution_max_terms=1_000_000,
**kwargs,
):
"""Bounds the backward direction of a mirror circuit with a forward-style bound.

Valid only for circuits whose second half inverts the first: the backward evolution then
traverses the same gates as the forward one, and reproduces the forward bounds.
"""
# As `compute_backward_bounds` does: strip measurements and invert, so that walking the circuit
# backwards from its end becomes walking the inverted circuit forwards.
circuit = remove_measure(circuit).inverse()

# The substitution: the forward norm function, bounding against the observable rather than
# against the initial state.
norm_fn = partial(
time_evolved_norm_forward,
observable=observable,
evolution_max_terms=evolution_max_terms,
)

# Seed the light cone from the observable, not from every active qubit.
light_cone = LightCone.initialize_from_pauli(circuit, observable)

return compute_bounds(
circuit,
noise_model_paulis,
light_cone,
norm_fn,
backwards=True,
**kwargs,
)

Ile zyskuje​

Porównanie ze standardowym ograniczeniem wstecznym na tym samym obwodzie, podsumowane tak samo jak w przewodniku konfiguracji: średnie ograniczenie i jego odchylenie standardowe, a następnie udział członów ograniczonych przez 0, z obliczonym ograniczeniem pomiędzy oraz pozostawionych przy trywialnym 2.0.

from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
)

forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
stock_backward = compute_backward_bounds(boxed_circuit, noise_model_paulis)
custom_backward = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable
)

def summarize(label, bounds):
"""Prints a one-line summary of a set of bounds."""
rates = np.concatenate([bound.rates for bound in bounds.values()])
# 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:18} 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%}"
)

summarize("forward", forward_bounds)
summarize("backward (stock)", stock_backward)
summarize("backward (custom)", custom_backward)
forward mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
backward (stock) mean 1.7218 ± 0.592 │ at 0: 0% computed: 95% at 2.0: 5%
backward (custom) mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%

Standardowe ograniczenie wsteczne znajduje się blisko trywialnej wartości 2.0, która niesie prawie zero informacji. W obwodzie lustrzanym każdy błąd jest „blisko stanu początkowego” w sensie, który mierzy to ograniczenie, więc nie potrafi ono rozróżniać członów. Niestandardowe ograniczenie jest kilkakrotnie ściślejsze i ogranicza wiele członów przez 0.

Aby sprawdzić implementację, można porównać oba ograniczenia blok po bloku. Dla każdego bloku niestandardowe ograniczenie wsteczne wychodzi identyczne z ograniczeniem w przód tego samego bloku, z dokładnością do błędu zaokrągleń. Oba ograniczają ten sam błąd w tym samym punkcie obwodu, a w obwodzie lustrzanym bramki po tym punkcie redukują się do odwrotności bramek przed nim, więc oba obliczenia wyznaczają tę samą wielkość z przeciwnych końców:

matches = sum(
np.allclose(forward_bounds[box_id].rates, custom_backward[box_id].rates)
for box_id in forward_bounds
)
print(f"{matches} of {len(forward_bounds)} boxes match the forward bounds")

worst = max(
np.abs(forward_bounds[box_id].rates - custom_backward[box_id].rates).max()
for box_id in forward_bounds
)
print(f"largest disagreement across all terms: {worst:.2e}")
16 of 16 boxes match the forward bounds
largest disagreement across all terms: 6.00e-15

Uwaga: Ta równość jest własnością struktury lustrzanej, a nie samej techniki. W obwodzie, który nie jest samoodwrotny, ta sama podmiana nadal działa, ale nie oblicza już poprawnego ograniczenia dla kierunku wstecznego: odpowiada na pytanie „w przód” o połowę obwodu, do której to pytanie nie ma zastosowania. Używaj tego tylko wtedy, gdy druga połowa obwodu odwraca pierwszą.

Przekaż to do reszty przepływu pracy​

Wynikiem jest zwykły słownik Bounds, więc wszystko, co następuje dalej, pozostaje bez zmian.

W obwodzie lustrzanym nie ma też powodu, by obliczać któreś z ograniczeń dla całego obwodu. Ograniczenia w przód są potrzebne tylko dla bloków U†U^\dagger, licząc od końca, a ograniczenia wsteczne tylko dla bloków UU, licząc od początku. max_num_boxes ogranicza więc każde z nich do połowy zaszumionych bloków, a merge_bounds łączy obie połowy.

Tak właśnie używa się ograniczeń w praktyce, co oznacza, że porównanie blok po bloku powyżej nie jest już dostępne: wymaga ono obu ograniczeń dla każdego bloku. To sprawdzenie implementacji na potrzeby tego przewodnika, a nie krok przepływu pracy.

from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds

# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}

# Compute each bound for its own half of the mirror circuit only. `forward_bounds` has an entry
# for every noisy box, which are the boxes `max_num_boxes` counts.
half = len(forward_bounds) // 2
half_forward = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
half_stock = compute_backward_bounds(
boxed_circuit, noise_model_paulis, max_num_boxes=half
)
half_custom = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)

print(
f"each bound computed for {half} of {len(forward_bounds)} noisy boxes\n"
)
print(f"{'bounds':34} {'overhead':>12} {'residual bias':>14}")
for label, bounds in (
(
"forward + stock backward",
merge_bounds(boxed_circuit, half_forward, half_stock, noise_rates),
),
(
"forward + custom backward",
merge_bounds(boxed_circuit, half_forward, half_custom, noise_rates),
),
("forward only, whole circuit", forward_bounds),
):
_, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
print(f"{label:34} {cost:12.4f} {bias:14.4f}")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
each bound computed for 8 of 16 noisy boxes

bounds overhead residual bias
forward + stock backward 29848.0583 0.0498
forward + custom backward 465.4028 0.0498
forward only, whole circuit 465.4028 0.0498

Przy standardowych ograniczeniach wstecznych narzut próbkowania jest znacznie większy. merge_bounds używa ograniczeń wstecznych do jednego punktu przełączenia w obwodzie, a od niego ograniczeń w przód, wybierając punkt minimalizujący całkowity błąd systematyczny. Gdy każde ograniczenie jest obliczane dla jednej połowy, ten punkt wypada w środku obwodu, więc bloki UU są pokryte wyłącznie ograniczeniami wstecznymi. Standardowe ograniczenie wsteczne jest tam bliskie trywialnego 2.0, co zwiększa narzut. Niestandardowe jest tak ścisłe, jak byłoby ograniczenie w przód, więc narzut odpowiada narzutowi pełnych ograniczeń w przód.

Nasuwa się oczywiste pytanie, po co się trudzić, skoro same pełne ograniczenia w przód dają ten sam wynik. Odpowiedzią jest koszt. Ograniczenie w przód dla bloku na początku obwodu ewoluuje jego błąd przez resztę UU, a potem przez całe U†U^\dagger, co jest najdłuższą i najszerszą ewolucją w obwodzie. Ewoluowany operator może rosnąć wykładniczo wraz z liczbą bramek nie-Cliffordowskich, przez które przechodzi, a obliczenie normy spektralnej komutatora staje się wykładniczo droższe, gdy stożek światła się poszerza. W podejściu połowa na połowę żaden błąd nie jest ewoluowany przez więcej niż połowę obwodu.

Oszczędność rośnie więc wraz z głębokością obwodu. Co ważniejsze, pełne ograniczenia w przód przestają być możliwe do obliczenia na długo przed tym, zanim stanie się to z połowicznymi. Gdy ewolucje wczesnych bloków przekroczą evolution_max_terms lub eigval_max_qubits, ich ograniczenia są obcinane lub wracają do nierówności trójkąta, a w końcu ich obliczenie kosztuje zbyt wiele. Gdziekolwiek ten punkt wypada, podejście połowa na połowę potrzebuje tylko, by wypadał gdzieś w pierwszej połowie obwodu. Ograniczenia w przód muszą sięgać nie dalej niż do środka, a w obwodzie lustrzanym ograniczenia wsteczne dla bloków UU ewoluują ich błędy wstecz do początku przez tyle samo obwodu, ile ograniczenia w przód dla odpowiadających bloków U†U^\dagger do końca, więc wyczerpują się na tej samej głębokości.

Uwaga: Dlaczego obliczać ograniczenia wsteczne dla bloków UU, skoro ograniczenia w przód dla bloków U†U^\dagger wyglądają, jakby miały mieć te same wartości? Ponieważ bloki nie są swoimi lustrzanymi odbiciami. Każdy blok wstrzykuje swój szum po jednej stronie swoich bramek, a odwrócone warstwy U†U^\dagger są osobnymi blokami z własnymi modelami szumu, więc blok lustrzany względem bloku UU widzi jego błędy w innym punkcie obwodu. Dlatego ich ograniczenia zgadzają się tylko częściowo, a ograniczenia dla bloków UU trzeba obliczyć dla tych bloków.

Pisanie innych niestandardowych ograniczeń​

Te same trzy podmiany obejmują inne przypadki:

  • Inna norma. time_evolved_norm_forward przyjmuje comm_norm_order; przekazanie czegokolwiek innego niż 2 pomija solver wartości własnych i oblicza tę normę bezpośrednio.

  • Inny stożek światła. LightCone.initialize_from_pauli zasiewa stożek światła z dowolnego operatora Pauliego, a konstruktor LightCone przyjmuje kubity do śledzenia i operacje go zasiewające, więc ograniczenie można obliczyć względem dowolnego operatora, który da się w ten sposób wyrazić.

  • Zupełnie inna funkcja normy. compute_bounds wymaga tylko obiektu wywoływalnego: przyjmującego Pauli i RotationGates oraz zwracającego CommutatorBounds. Działa cokolwiek, co spełnia tę sygnaturę, włącznie z ograniczeniem w postaci zamkniętej, specyficznym dla twojej rodziny obwodów.

Cokolwiek podstawisz, musisz udowodnić, że wynik jest rzeczywistym ograniczeniem górnym. Zaniżenie wpływu członu błędu nie powoduje widocznej awarii. Daje wartość oczekiwaną, która jest po cichu błędna, ponieważ człon zostanie pominięty w złagodzonym modelu szumu.