Cara menghitung bound kustom
compute_forward_bounds dan compute_backward_bounds adalah wrapper praktis. Keduanya dibangun dari
tiga bagian yang sama, yang diekspos oleh API publik:
-
Sebuah fungsi norm, yang membatasi satu suku error Pauli yang sudah dievolusikan
-
Sebuah light cone, yang menentukan ke arah mana suku error dievolusikan
-
compute_bounds, yang mengiterasi circuit dan mendistribusikan pekerjaan per suku
Merakit ketiganya sendiri berarti kamu bisa membatasi sesuatu yang tidak dicakup wrapper. Panduan ini membahas satu kasus yang layak dikerjakan begitu: mirror circuit, di mana bound backward yang biasa terlalu longgar tanpa perlu.
Apa sebenarnya yang dilakukan wrapper
Kedua wrapper berbeda di tiga tempat yang tercantum di atas:
compute_forward_bounds | compute_backward_bounds | |
|---|---|---|
| circuit | sesuai input | remove_measure(circuit).inverse() |
| fungsi norm | time_evolved_norm_forward, terhadap observable | time_evolved_norm_backward, terhadap initial state |
| light cone | LightCone.initialize_from_pauli, diinisialisasi dari observable | LightCone.initialize_from_measurements, diinisialisasi dari setiap qubit aktif |
backwards | False | True |
Bound forward bertanya "Seberapa besar error ini bisa mengubah observable yang akan saya ukur?" dan menjawabnya dengan spectral norm dari sebuah commutator. Bound backward bertanya "Seberapa besar error ini bisa mengubah state tempat saya memulai?" dan menjawabnya dengan nuclear norm, dengan menganggap state itu adalah all-zero state pada setiap qubit aktif. Keduanya pertanyaan yang berbeda, dan masing-masing tepat untuk error di setengah circuit-nya sendiri.
Kasus mirror circuit
Mirror circuit menerapkan suatu unitary lalu membatalkannya: . Ini muncul setiap kali hasil idealnya harus diketahui dengan pasti (umumnya Loschmidt echo, randomized benchmarking, dan eksperimen verifikasi).
Mirror circuit punya sifat yang tidak bisa dimanfaatkan oleh bound backward generik. Karena setengah kedua adalah invers dari setengah pertama, mengevolusikan error mundur ke initial state melewati gate yang sama dengan mengevolusikannya maju ke observable. Kedua pertanyaan itu bertemu, jadi bound gaya forward yang lebih tajam valid juga untuk arah backward.
# 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
Merakit bound kustom
Resepnya mempertahankan apa yang dilakukan compute_backward_bounds pada circuit (membuang pengukuran,
membalikkannya, dan mengiterasinya dengan backwards=True) dan mengganti dua bagian lainnya: fungsi
norm forward, serta light cone yang diinisialisasi dari observable dengan LightCone.initialize_from_pauli,
persis seperti compute_forward_bounds menginisialisasi miliknya. Setiap penggantian cukup satu baris.
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,
)
Seberapa besar keuntungannya
Membandingkan dengan bound backward bawaan pada circuit yang sama, dirangkum dengan cara yang sama seperti di panduan konfigurasi: rata-rata bound dan standar deviasinya, diikuti porsi suku yang di-bound 0, yang bound-nya dihitung di antaranya, dan yang dibiarkan di nilai 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%
Bound backward bawaan berada dekat nilai trivial 2.0, yang nyaris tidak memberi informasi. Pada mirror circuit, setiap error "dekat dengan initial state" menurut ukuran bound itu, jadi ia tidak bisa membedakan antar suku. Bound kustom beberapa kali lebih ketat dan membatasi banyak suku dengan 0.
Untuk memeriksa implementasinya, keduanya bisa dibandingkan box demi box. Untuk setiap box, bound backward kustom menghasilkan nilai yang identik dengan bound forward box yang sama, sampai batas galat pembulatan. Keduanya membatasi error yang sama di titik yang sama dalam circuit, dan pada mirror circuit gate setelah titik itu saling meniadakan menjadi invers dari gate sebelumnya, jadi kedua komputasi menghitung besaran yang sama dari ujung yang berlawanan:
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
Catatan: Kesamaan ini adalah sifat dari struktur mirror, bukan dari tekniknya. Pada circuit yang tidak self-inverting, penggantian yang sama tetap berjalan tetapi tidak lagi menghitung bound yang valid untuk arah backward: ia menjawab pertanyaan forward tentang setengah circuit yang tidak cocok untuk pertanyaan itu. Pakai ini hanya kalau setengah kedua circuit-mu membalikkan setengah pertama.
Masukkan ke sisa alur kerja
Hasilnya adalah dictionary Bounds biasa, jadi semua yang menyusul tidak berubah.
Pada mirror circuit juga tidak ada alasan untuk menghitung salah satu bound untuk seluruh circuit. Bound
forward hanya dibutuhkan untuk box dari , dihitung dari akhir, dan bound backward
hanya untuk box dari , dihitung dari awal. Karena itu max_num_boxes membatasi masing-masing
ke setengah dari box yang noisy, dan merge_bounds menggabungkan kedua setengahnya.
Beginilah bound dipakai dalam praktik, dan itu berarti perbandingan box-demi-box di atas tidak lagi tersedia: perbandingan itu butuh kedua bound untuk setiap box. Itu hanya pemeriksaan implementasi untuk panduan ini, bukan langkah alur kerja.
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
Dengan bound backward bawaan, sampling overhead jauh lebih besar. merge_bounds memakai
bound backward sampai satu titik peralihan di circuit dan bound forward sesudahnya,
memilih titik yang meminimalkan total bias. Dengan tiap bound dihitung untuk satu setengah, titik itu
jatuh di tengah circuit, jadi box dari hanya dicakup oleh bound backward. Bound backward
bawaan di sana dekat dengan 2.0 yang trivial, yang mendorong overhead naik. Yang kustom
sama ketatnya dengan bound forward, jadi overhead-nya sama dengan overhead bound forward
lengkap.
Itu memunculkan pertanyaan yang jelas: kenapa repot-repot, kalau bound forward lengkap saja sudah memberi hasil yang sama. Jawabannya adalah biaya. Bound forward untuk box di awal circuit mengevolusikan error-nya melalui sisa lalu seluruh , yang merupakan evolusi terpanjang dan terlebar di circuit. Operator yang dievolusikan bisa tumbuh secara eksponensial terhadap jumlah gate non-Clifford yang dilaluinya, dan menghitung spectral norm commutator menjadi eksponensial lebih mahal seiring melebarnya light cone. Dengan pendekatan setengah-setengah, tidak ada error yang dievolusikan melewati lebih dari separuh circuit.
Jadi penghematannya bertambah seiring kedalaman circuit. Yang lebih penting, bound forward
lengkap berhenti bisa dihitung jauh sebelum yang setengahnya. Begitu evolusi box
awal melampaui evolution_max_terms atau eigval_max_qubits, bound-nya terpotong atau jatuh kembali
ke pertidaksamaan segitiga, dan akhirnya terlalu mahal untuk dihitung. Di mana pun titik itu
berada, pendekatan setengah-setengah hanya butuh titik itu jatuh di suatu tempat di separuh pertama circuit.
Bound forward tidak perlu menjangkau lebih jauh dari tengah, dan pada mirror circuit, bound
backward untuk box dari mengevolusikan error mundur ke awal melalui circuit sebanyak
yang dilalui bound forward untuk box padanan dari ke akhir, jadi keduanya kehabisan tenaga
pada kedalaman yang sama.
Catatan: Kenapa menghitung bound backward untuk box dari , padahal bound forward dari box tampaknya harus punya angka yang sama? Karena box-nya bukan bayangan cermin satu sama lain. Setiap box menyuntikkan noise-nya di satu sisi gate-nya, dan layer invers dari adalah box terpisah dengan model noise sendiri, jadi box yang mencerminkan box dari melihat error-nya di titik yang berbeda dalam circuit. Karena itu, bound keduanya hanya sebagian cocok, dan bound untuk box dari harus dihitung untuk box-box tersebut.
Menulis bound kustom lainnya
Tiga penggantian yang sama mencakup kasus lain:
-
Norm yang berbeda.
time_evolved_norm_forwardmenerimacomm_norm_order; memberi nilai selain2melewati eigensolver dan menghitung norm itu secara langsung. -
Light cone yang berbeda.
LightCone.initialize_from_paulimenginisialisasi light cone dari Pauli apa pun, dan constructorLightConemenerima qubit yang dilacak dan operasi yang menginisialisasinya, jadi bound bisa dihitung terhadap operator apa pun yang bisa kamu ekspresikan dengan cara itu. -
Fungsi norm yang sama sekali berbeda.
compute_boundshanya mensyaratkan sebuah callable: yang menerimaPaulidanRotationGatesdan mengembalikanCommutatorBounds. Apa pun yang memenuhi signature itu bisa dipakai, termasuk bound closed-form yang spesifik untuk keluarga circuit-mu.
Apa pun yang kamu gantikan, kamu harus membuktikan bahwa hasilnya benar-benar upper bound. Meremehkan pengaruh suku error tidak menimbulkan kegagalan yang terlihat. Hasilnya adalah expectation value yang diam-diam salah, karena suku itu akan terbuang dari model noise yang dimitigasi.