Přeskočit na hlavní obsah
Tento projekt je v betě. Našel jsi chybu? Otevřít issue na GitHubu. Máš otázku nebo nápad? Zeptej se v Discussions — rádi si poslechneme tvou zpětnou vazbu!
Jaká je kvalita překladu na této stránce?

Jak vypočítat vlastní meze

compute_forward_bounds a compute_backward_bounds jsou pohodlné obaly. Oba jsou postaveny ze stejných tří částí, které veřejné API zpřístupňuje:

  • Normová funkce, která omezuje jeden vyvinutý člen Pauliho chyby

  • Světelný kužel, který určuje, k čemu se členy chyb vyvíjejí

  • compute_bounds, který prochází obvod a rozděluje práci po členech

Jejich složení vlastními silami znamená, že můžeš omezit něco, co obaly nepokrývají. Tento průvodce projde jeden případ, kde se to vyplatí: zrcadlový obvod, kde je obvyklá zpětná mez zbytečně volná.

Co obaly vlastně dělají​

Oba obaly se liší ve třech výše uvedených místech:

compute_forward_boundscompute_backward_bounds
obvodtak, jak je zadánremove_measure(circuit).inverse()
normová funkcetime_evolved_norm_forward, vůči pozorovatelnétime_evolved_norm_backward, vůči počátečnímu stavu
světelný kuželLightCone.initialize_from_pauli, zaset z pozorovatelnéLightCone.initialize_from_measurements, zaset z každého aktivního qubitu
backwardsFalseTrue

Dopředná mez se ptá „Jak moc může tato chyba změnit pozorovatelnou, kterou se chystám měřit?“ a odpovídá spektrální normou komutátoru. Zpětná mez se ptá „Jak moc může tato chyba změnit stav, ze kterého jsem začal?“ a odpovídá nukleární normou, přičemž tento stav bere jako stav samých nul na každém aktivním qubitu. Jsou to různé otázky a každá je ta pravá pro chyby ve své vlastní polovině obvodu.

Případ zrcadlového obvodu​

Zrcadlový obvod aplikuje nějakou unitární operaci a pak ji vrátí zpět: U†UU^\dagger U. Objevují se všude, kde je třeba přesně znát ideální výsledek (obecně Loschmidtovy echo, randomizované benchmarkování a ověřovací experimenty).

Zrcadlové obvody mají vlastnost, kterou obecná zpětná mez nedokáže využít. Protože druhá polovina je inverzí první, vyvíjení chyby zpět k počátečnímu stavu prochází stejná hradla jako vyvíjení vpřed k pozorovatelné. Obě otázky splývají, takže ostřejší mez dopředného typu je platná i pro zpětný směr.

# 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

Sestavení vlastní meze​

Recept ponechává to, co compute_backward_bounds dělá s obvodem (odstraní měření, invertuje jej a prochází jej s backwards=True) a nahrazuje ostatní dvě části: dopřednou normovou funkci a světelný kužel zasetý z pozorovatelné pomocí LightCone.initialize_from_pauli, přesně jako si jej zasévá compute_forward_bounds. Každá náhrada je jeden řádek.

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

Kolik to přinese​

Porovnání se standardní zpětnou mezí na stejném obvodu, shrnuté stejně jako v průvodci konfigurací: průměrná mez a její směrodatná odchylka, potom podíl členů s mezí 0, s vypočítanou mezí mezi a ponechaných na triviální 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%

Standardní zpětná mez leží blízko triviální hodnoty 2.0, která nenese téměř žádnou informaci. Na zrcadlovém obvodu je každá chyba „blízko počátečního stavu“ ve smyslu, který tato mez měří, takže nedokáže rozlišit mezi členy. Vlastní mez je několikanásobně těsnější a mnoho členů omezuje hodnotou 0.

Pro ověření implementace lze obě porovnat box po boxu. U každého boxu vychází vlastní zpětná mez shodně s dopřednou mezí téhož boxu, až na chybu zaokrouhlení. Obě omezují stejnou chybu ve stejném místě obvodu a na zrcadlovém obvodu se hradla po tomto místě vyruší na inverzi hradel před ním, takže oba výpočty vyhodnocují tutéž veličinu z opačných konců:

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

Poznámka: Tato rovnost je vlastností zrcadlové struktury, ne techniky. Na obvodu, který není sám sobě inverzní, stejná náhrada stále běží, ale už nepočítá platnou mez pro zpětný směr: odpovídá na dopřednou otázku o polovině obvodu, kde tato otázka neplatí. Použij to jen tehdy, když druhá polovina tvého obvodu invertuje první.

Zapojení do zbytku pracovního postupu​

Výsledkem je běžný slovník Bounds, takže vše, co následuje, zůstává beze změny.

Na zrcadlovém obvodu také není důvod počítat kteroukoli mez pro celý obvod. Dopředné meze jsou potřeba jen pro boxy U†U^\dagger, počítané od konce, a zpětné meze jen pro boxy UU, počítané od začátku. max_num_boxes proto omezuje každou z nich na polovinu zašuměných boxů a merge_bounds obě poloviny spojí.

Takto se meze používají v praxi, a znamená to, že porovnání box po boxu výše už není k dispozici: potřebuje obě meze pro každý box. Je to kontrola implementace pro tento průvodce, ne krok pracovního postupu.

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

Se standardními zpětnými mezemi je režie vzorkování mnohem větší. merge_bounds používá zpětné meze do jednoho bodu přepnutí v obvodu a odtud dále dopředné meze, přičemž volí bod, který minimalizuje celkové zkreslení. Když je každá mez počítána pro jednu polovinu, tento bod padne doprostřed obvodu, takže boxy UU jsou pokryty samotnými zpětnými mezemi. Standardní zpětná mez je tam blízko triviální 2.0, což režii zvyšuje. Vlastní mez je stejně těsná, jako by byla dopředná mez, takže režie odpovídá režii úplných dopředných mezí.

Nabízí se zřejmá otázka, proč se obtěžovat, když úplné dopředné meze samy dávají stejný výsledek. Odpověď jsou náklady. Dopředná mez pro box na začátku obvodu vyvíjí jeho chybu skrz zbytek UU a poté celé U†U^\dagger, což je nejdelší a nejširší vyvíjení v obvodu. Vyvinutý operátor může růst exponenciálně v počtu non-Cliffordových hradel, kterými prochází, a výpočet spektrální normy komutátoru je exponenciálně dražší, jak se světelný kužel rozšiřuje. U přístupu půl na půl se žádná chyba nevyvíjí skrz více než polovinu obvodu.

Úspora proto roste s hloubkou obvodu. Co je důležitější, úplné dopředné meze přestanou být vypočitatelné dlouho předtím, než polovičné. Jakmile vyvíjení raných boxů překročí evolution_max_terms nebo eigval_max_qubits, jejich meze se zkrátí nebo přejdou na trojúhelníkovou nerovnost a nakonec je jejich výpočet příliš drahý. Ať ten bod leží kdekoli, přístup půl na půl potřebuje jen to, aby padl někam do první poloviny obvodu. Dopředné meze nemusí sahat dál než do poloviny a na zrcadlovém obvodu zpětné meze pro boxy UU vyvíjejí své chyby zpět na začátek skrz stejně velkou část obvodu jako dopředné meze pro odpovídající boxy U†U^\dagger až na konec, takže jim dojdou síly ve stejné hloubce.

Poznámka: Proč počítat zpětné meze pro boxy UU, když dopředné meze boxů U†U^\dagger vypadají, že by měly mít stejná čísla? Protože boxy nejsou svými zrcadlovými obrazy. Každý box vkládá svůj šum na jedné straně svých hradel a inverzní vrstvy U†U^\dagger jsou samostatné boxy s vlastními modely šumu, takže box, který zrcadlí box z UU, vidí jeho chyby v jiném místě obvodu. Proto se jejich meze shodují jen částečně a meze pro boxy UU je nutné počítat pro tyto boxy.

Psaní dalších vlastních mezí​

Stejné tři náhrady pokrývají další případy:

  • Jiná norma. time_evolved_norm_forward přijímá comm_norm_order; zadání čehokoli jiného než 2 přeskočí řešič vlastních hodnot a tuto normu spočítá přímo.

  • Jiný světelný kužel. LightCone.initialize_from_pauli zaseje světelný kužel z libovolného Pauliho členu a konstruktor LightCone bere qubity ke sledování a operace, které jej zasévají, takže mez lze spočítat vůči libovolnému operátoru, který tak umíš vyjádřit.

  • Úplně jiná normová funkce. compute_bounds vyžaduje jen volatelný objekt: který bere Pauli a RotationGates a vrací CommutatorBounds. Cokoli splňující tento podpis funguje, včetně meze v uzavřeném tvaru specifické pro tvou rodinu obvodů.

Ať nahradíš cokoli, musíš dokázat, že výsledek je skutečná horní mez. Podhodnocení vlivu členu chyby nevede k viditelnému selhání. Vede ke střední hodnotě, která je tiše špatná, protože člen bude vypuštěn z modelu šumu použitého pro mitigaci.