Sari la conținutul principal
Acest proiect este în versiune beta. Ai găsit o eroare? Deschide un issue pe GitHub. Ai o întrebare sau o idee? Întreabă în Discussions — ne-ar plăcea să aflăm părerea ta!
Cum este calitatea traducerii pe această pagină?

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_boundscompute_backward_bounds
circuitca atareremove_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
backwardsFalseTrue

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ă: U†UU^\dagger U. 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 U†U^\dagger, numărate de la sfârșit, iar limitele înapoi doar pentru box-urile lui UU, 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 UU 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 UU și apoi prin tot U†U^\dagger, 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 UU își evoluează erorile înapoi până la început prin tot atâta circuit cât limitele înainte pentru box-urile corespunzătoare ale lui U†U^\dagger 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 UU, când limitele înainte ale box-urilor lui U†U^\dagger 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 U†U^\dagger sunt box-uri separate cu modele de zgomot proprii, deci box-ul care oglindește un box al lui UU îi vede erorile într-un alt punct al circuitului. De aceea limitele lor concordă doar parțial, iar limitele pentru box-urile lui UU trebuie calculate pentru acele box-uri.

Scrierea altor limite personalizate​

Aceleași trei înlocuiri acoperă și alte cazuri:

  • O normă diferită. time_evolved_norm_forward acceptă comm_norm_order; transmiterea oricărei valori diferite de 2 omite solverul de valori proprii și calculează direct acea normă.

  • Un con de lumină diferit. LightCone.initialize_from_pauli inițializează un con de lumină din orice Pauli, iar constructorul LightCone primeș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_bounds cere doar un obiect apelabil: care primește un Pauli și RotationGates și returnează un CommutatorBounds. 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.