Cum să calculezi limite personalizate
compute_forward_bounds și compute_backward_bounds sunt funcții wrapper de conveniență. Ambele sunt construite din
aceleași trei piese, pe care API-ul public le expune:
-
O funcție de normă, care limitează un singur termen de eroare Pauli evoluat
-
Un con de lumină, care stabilește spre ce sunt evoluați termenii de eroare
-
compute_bounds, care iterează circuitul și distribuie munca per termen
Asamblarea lor singur înseamnă că poți limita ceva ce wrapper-ele nu acoperă. Acest ghid parcurge un caz în care merită să faci asta: un circuit oglindă, unde limita înapoi obișnuită este inutil de laxă.
Ce fac de fapt wrapper-ele
Cele două wrapper-e diferă în cele trei locuri enumerate mai sus:
compute_forward_bounds | compute_backward_bounds | |
|---|---|---|
| circuit | ca atare | remove_measure(circuit).inverse() |
| funcție de normă | time_evolved_norm_forward, față de observabilă | time_evolved_norm_backward, față de starea inițială |
| con de lumină | LightCone.initialize_from_pauli, inițializat din observabilă | LightCone.initialize_from_measurements, inițializat din fiecare qubit activ |
backwards | False | True |
Limita înainte întreabă „Cât poate această eroare să schimbe observabila pe care urmează s-o măsor?" și răspunde cu norma spectrală a unui comutator. Limita înapoi întreabă „Cât poate această eroare să schimbe starea din care am pornit?" și răspunde cu o normă nucleară, considerând că acea stare este starea cu toți qubiții în zero pe fiecare qubit activ. Sunt întrebări diferite, iar fiecare este cea potrivită pentru erorile din propria jumătate a circuitului.
Cazul circuitului oglindă
Un circuit oglindă aplică o unitară și apoi o anulează: . Acestea apar oricând rezultatul ideal trebuie cunoscut exact (în general ecouri Loschmidt, benchmarking randomizat și experimente de verificare).
Circuitele oglindă au o proprietate pe care limita înapoi generică nu o poate exploata. Deoarece a doua jumătate este inversa primei, evoluția unei erori înapoi spre starea inițială parcurge aceleași porți ca evoluția ei înainte spre observabilă. Cele două întrebări coincid, deci limita mai fină de tip înainte este validă și pentru direcția înapoi.
# 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
Asamblează limita personalizată
Rețeta păstrează ce face compute_backward_bounds cu circuitul (elimină măsurătorile,
îl inversează și îl iterează cu backwards=True) și înlocuiește celelalte două piese: funcția de normă
înainte și un con de lumină inițializat din observabilă cu LightCone.initialize_from_pauli,
exact cum își inițializează compute_forward_bounds conul propriu. Fiecare înlocuire este o singură linie.
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,
)
Cât câștigă
Comparație cu limita înapoi standard pe același circuit, rezumată la fel ca în ghidul de configurare: limita medie și abaterea ei standard, urmate de ponderea termenilor limitați la 0, cu o limită calculată între, și lăsați la valoarea trivială 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%
Limita înapoi standard se află aproape de valoarea trivială 2.0, care nu aduce aproape nicio informație. Pe un circuit oglindă, fiecare eroare este „aproape de starea inițială" în sensul în care măsoară limita, deci nu poate discrimina între termeni. Limita personalizată este de câteva ori mai strânsă și limitează mulți dintre termeni la 0.
Pentru a verifica implementarea, cele două pot fi comparate box cu box. Pentru fiecare box, limita înapoi personalizată iese identică cu limita înainte a aceluiași box, până la eroarea de rotunjire. Ambele limitează aceeași eroare în același punct al circuitului, iar pe un circuit oglindă porțile din urma acelui punct se anulează până la inversa porților dinaintea lui, deci cele două calcule evaluează aceeași mărime de la capete opuse:
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
Notă: Această egalitate este o proprietate a structurii oglindă, nu a tehnicii. Pe un circuit care nu este auto-inversant, aceeași înlocuire rulează în continuare, dar nu mai calculează o limită validă pentru direcția înapoi: răspunde la întrebarea înainte despre o jumătate de circuit unde întrebarea nu se aplică. Folosește asta doar când a doua jumătate a circuitului tău o inversează pe prima.
Introdu-o în restul fluxului de lucru
Rezultatul este un dicționar Bounds obișnuit, deci tot ce urmează rămâne neschimbat.
Pe un circuit oglindă nu există nici un motiv să calculezi vreuna dintre limite pentru întregul circuit. Limitele
inainte sunt necesare doar pentru box-urile lui , numărate de la sfârșit, iar limitele înapoi
doar pentru box-urile lui , numărate de la început. max_num_boxes limitează deci fiecare
dintre ele la jumătate din box-urile zgomotoase, iar merge_bounds îmbină cele două jumătăți.
Așa sunt folosite limitele în practică, ceea ce înseamnă că comparația box cu box de mai sus nu mai este disponibilă: ea are nevoie de ambele limite pentru fiecare box. Este o verificare a implementării pentru acest ghid, nu un pas al fluxului de lucru.
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
Cu limitele înapoi standard, overhead-ul de eșantionare este mult mai mare. merge_bounds folosește limitele
inapoi până la un punct de comutare din circuit și limitele înainte de acolo încolo,
alegând punctul care minimizează deplasarea totală. Cu fiecare limită calculată pentru o jumătate, acel punct
se află la mijlocul circuitului, deci box-urile lui sunt acoperite doar de limite înapoi. Limita
inapoi standard este aproape de trivialul 2.0 acolo, ceea ce ridică overhead-ul. Cea personalizată
este la fel de strânsă ca ar fi fost limita înainte, deci overhead-ul se potrivește cu cel al limitelor
inainte complete.
Asta ridică întrebarea evidentă de ce să te mai complici, când limitele înainte complete singure dau același rezultat. Răspunsul este costul. O limită înainte pentru un box de la începutul circuitului își evoluează eroarea prin restul lui și apoi prin tot , ceea ce este cea mai lungă și mai largă evoluție din circuit. Operatorul evoluat poate crește exponențial în numărul de porți non-Clifford pe care le traversează, iar calculul normei spectrale a comutatorului devine exponențial mai costisitor pe măsură ce conul de lumină se lărgește. În abordarea jumătate-jumătate, nicio eroare nu este evoluată prin mai mult de jumătate din circuit.
Economia crește deci odată cu adâncimea circuitului. Mai important, limitele înainte complete
încetează să mai poată fi calculate cu mult înainte ca cele pe jumătate să o facă. Odată ce evoluțiile primelor
box-uri depășesc evolution_max_terms sau eigval_max_qubits, limitele lor sunt trunchiate sau revin
la inegalitatea triunghiului, iar în cele din urmă costă prea mult să fie calculate. Oriunde ar fi acel punct,
abordarea jumătate-jumătate are nevoie doar ca el să cadă undeva în prima jumătate a circuitului.
Limitele înainte nu trebuie să ajungă mai departe de mijloc, iar pe un circuit oglindă, limitele
inapoi pentru box-urile lui își evoluează erorile înapoi până la început prin tot atâta
circuit cât limitele înainte pentru box-urile corespunzătoare ale lui până la sfârșit, deci se epuizează
la aceeași adâncime.
Notă: De ce să calculezi limite înapoi pentru box-urile lui , când limitele înainte ale box-urilor lui par să dea aceleași numere? Pentru că box-urile nu sunt imagini în oglindă unele ale altora. Fiecare box injectează zgomotul pe o parte a porților sale, iar straturile inverse ale lui sunt box-uri separate cu modele de zgomot proprii, deci box-ul care oglindește un box al lui îi vede erorile într-un alt punct al circuitului. De aceea limitele lor concordă doar parțial, iar limitele pentru box-urile lui trebuie calculate pentru acele box-uri.
Scrierea altor limite personalizate
Aceleași trei înlocuiri acoperă și alte cazuri:
-
O normă diferită.
time_evolved_norm_forwardacceptăcomm_norm_order; transmiterea oricărei valori diferite de2omite solverul de valori proprii și calculează direct acea normă. -
Un con de lumină diferit.
LightCone.initialize_from_pauliinițializează un con de lumină din orice Pauli, iar constructorulLightConeprimește qubiții de urmărit și operațiile care îl inițializează, deci o limită poate fi calculată față de orice operator pe care îl poți exprima astfel. -
O funcție de normă complet diferită.
compute_boundscere doar un obiect apelabil: care primește unPaulișiRotationGatesși returnează unCommutatorBounds. Orice satisface această semnătură funcționează, inclusiv o limită în formă închisă specifică familiei tale de circuite.
Orice ai înlocui, trebuie să demonstrezi că rezultatul este o limită superioară autentică. Subestimarea efectului unui termen de eroare nu produce o eroare vizibilă. Produce o valoare de așteptare discret greșită, pentru că termenul va fi fost eliminat din modelul de zgomot atenuat.