PEC s SLC
Probabilistic error cancellation (PEC) je technika pro zmírnění kvantového šumu při výpočtech středních hodnot pozorovatelných. Umožňuje vypočítat nezkreslený odhad středních hodnot. Technika funguje tak, že spouští soubor zašuměných obvodů, do nichž je vložena inverze naučeného modelu šumu. Model šumu je poté aproximován Pauliho kanálem pomocí Pauli twirlingu (známého také jako randomized compiling). Takové modely šumu lze naučit efektivně a lze je mitigovat pomocí PEC (více podrobností viz Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors).
PEC vyžaduje pro výpočet nezkresleného odhadu exponenciální režii shotů. Shaded Lightcones (SLC) je technika, která využívá klasické zdroje k ohraničení citlivosti na chyby v celém obvodu. Pomocí těchto mezí může uživatel řízeně nastavovat zkreslení při odhadu pozorovatelné.
Tento návod ukazuje, jak použít PEC v kombinaci s SLC ke zmírnění šumu hradel se sníženou režií shotů ve srovnání s běžným PEC.
Postup je následující:
- Zadej obvod a pozorovatelnou.
- Vypočítej zašuměné, nemitigované střední hodnoty.
- Vypočítej dopředné a zpětné meze zkreslení.
- Nauč se šum provázávajících vrstev pomocí qiskit-noise-learning.
- Pomocí naučeného šumu sluč vypočtené meze do matice lokálních škál.
- Zvol požadovanou mez zkreslení na základě kompromisu mezi režií shotů a zkreslením.
- Připrav program PEC + SLC, spusť jej a proveď post-processing.
- Porovnej mitigované střední hodnoty a režii shotů.
Zadej obvod a pozorovatelnou
Připrav zrcadlový obvod Trotterizovaného časového vývoje hamiltoniánu 1D Isingova řetězce na osmi qubitech. Obvod zrcadli, aby se jeho unitární operace vyrušila a vznikl identický obvod, takže ideální střední hodnota každé pozorovatelné je známa jako 1 bez nutnosti ji počítat. Zrcadlení navíc zdvojnásobí hloubku obvodu, což zase zdvojnásobí vliv šumu.
Použij úhel , aby byla dynamika ne-Cliffordovská.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "qiskit_noise_learning": "qiskit-noise-learning", "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 warnings
import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_mitigation.utils import get_measurement_bases
warnings.filterwarnings("ignore")
def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)
for _ in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
return circuit
def prepare_basis(
circuit: QuantumCircuit, basis: list[int]
) -> QuantumCircuit:
# basis is a list of integer values from 0 to 3. These map to the basis measurement as:
# 0 = I; 1 = Z; 2 = X; 3 = Y
assert len(basis) == circuit.num_qubits
out_circ = circuit.copy_empty_like()
for qb, bas in enumerate(basis):
if bas == 2:
out_circ.h(qb)
elif bas == 3:
out_circ.rx(-np.pi / 2, qb)
out_circ.barrier()
out_circ.compose(circuit, inplace=True)
return out_circ
def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.measure_active()
return mirror_circ
# define observables
num_qubits = 8 # Width of the Ising chain
# The observable to measure: one X component (on qubit 2) and one Z component (on qubit 5)
target_obs_sparse = [("XZ", [2, 5], 1.0)]
observable = SparsePauliOp.from_sparse_list(
target_obs_sparse, num_qubits=num_qubits
)
# Determine the measurement basis each qubit requires for this observable.
bases, _ = get_measurement_bases(observable)
# Instantiate the mirrored circuit and prepend the basis-preparation gates
num_trotter_steps = 6
rx_angle = np.pi / 4
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)
mirrored_circuit = mirror_circuit(circuit)
mirrored_circuit = prepare_basis(mirrored_circuit, bases[0])
print("Visualization of 1 mirrored Trotter step")
prepare_basis(
mirror_circuit(construct_ising_circuit(num_qubits, 1, rx_angle)), bases[0]
).draw("mpl", scale=0.6, fold=-1)
Visualization of 1 mirrored Trotter step
Vypočítej zašuměné, nemitigované střední hodnoty
QuantumProgram obsahuje kvantový obvod a samplex ve formátu vhodném pro primitivu Executor. MitigationTask.prepare sestaví QuantumProgram z obvodu, pozorovatelných a hodnot parametrů a obvod zabalí do boxů podle zadaných voleb boxování. Nastav volbu boxování measure_annotations na "change_basis", aby se měření otočila do požadovaných bází, a přidej add_tags: "unique_box", aby simulátor mohl namapovat model šumu na boxy obvodu. Program je ve výchozím stavu Pauli twirlován, což na hardwaru mění koherentní šum na Pauliho šum, který mitigační metody předpokládají. num_randomizations určuje, kolik twirlovaných instancí každého obvodu se vzorkuje, a shots_per_randomization určuje, kolik shotů každá instance dostane, takže jejich součin je limit shotů na obvod.
Pokud máš přístup k QPU, můžeš program spustit pomocí Executor vytvořeného z backendu QiskitRuntimeService. Tento příklad používá AerSimulator ke spuštění programu lokálně. Jeho simulátorová volba layer_noise_model vkládá šum z daného Pauli-Lindbladova modelu šumu do každé vrstvy provázávajících hradel. Zde generujeme náhodný Pauli-Lindbladův model šumu s chybou na vrstvené hradlo (EPLG) 0.01, téměř o řád vyšší než u typických současných QPU Heron, abychom zvýraznili účinek použití SLC i na problému malého rozsahu.
from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
from samplomatic import Tag
from samplomatic.utils import get_annotation
def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
seed: int | None = None,
) -> list[tuple[CircuitInstruction, PauliLindbladMap]]:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
layer_noise_model = []
for layer in layers:
layer_instructions = layer.operation.params[0]
contain_measurement = any(
inst.name.startswith("measure") for inst in layer_instructions
)
if contain_measurement: # do not add measurement noise
layer_noise = PauliLindbladMap.identity(num_qubits)
else:
edges = [
(inst.qubits[0]._index, inst.qubits[1]._index)
for inst in layer_instructions
] # extract the edges of the layer
basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
rng = np.random.default_rng(seed=seed)
num_generators = len(basis_paulis)
rates = rng.random(len(basis_paulis)) * eplg / num_generators
layer_noise = PauliLindbladMap.from_sparse_list(
[
(pauli.to_label(), edge, rate)
for pauli, rate in zip(basis_paulis, rates, strict=True)
for edge in edges
],
num_qubits=num_qubits,
)
layer_noise_model.append((layer, layer_noise))
return layer_noise_model
num_randomizations = 400
shots_per_randomization = 16
boxing_options = {"add_tags": "unique_box"}
# find the unique layers to generate the layered noise for
box_layers = find_combined_unique_layers(
[mirrored_circuit], custom_boxing_options=boxing_options
)
layer_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, eplg=1e-2, seed=42
)
# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)
task = MitigationTask()
program = task.prepare(
circuit=mirrored_circuit,
observables=observable,
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
program._semantic_role = "simulation" # flag the program for the simulator
job_unmit_res = noisy_executor.run(program).result()
unmitigated_res = task.postprocess(job_unmit_res)
print(f"unmitigated result: {unmitigated_res.data.evs[0]}")
unmitigated result: 0.63125
Vypočítej dopředné a zpětné meze zkreslení
Dříve než spotřebuješ jakýkoli čas QPU na učení šumu, SLC pomocí klasické simulace ohraničí příspěvek každé potenciální chyby ke zkreslení pozorovatelné. Děje se to ve dvou doplňkových průchodech:
-
Dopředná propagace: každá Pauliho chyba se propaguje dopředu zbytkem obvodu a vyhodnotí se její komutátor s pozorovatelnou. Pouze chyba, která s pozorovatelnou antikomutuje, může výsledek zkreslit, takže tento průchod dává horní mez dopředného dopadu každé chyby.
-
Zpětná propagace: každá chyba se propaguje zpět na začátek obvodu a vyhodnotí se její komutátor s počátečním stavem. Protože obvod začíná ve stavu samých nul, chyby, které s ním komutují, nemohou výsledek ovlivnit, což zpřesňuje mez z druhé strany.
Obě meze dohromady tvoří těsné rozmezí kolem příspěvku nejhoršího případu každé chyby ke zkreslení. V dalším kroku se tyto meze sloučí s naučenými sazbami šumu, aby se určilo, které chyby stojí za opravu a které lze bezpečně ponechat. Volba boxování inject_noise_strategy="individual_modification" je zde nutná, aby každá anotace InjectNoise v obvodu s boxy měla jedinečnou identitu, ke které lze meze SLC připojit.
U problémů většího rozsahu mohou tyto výpočty vyžadovat značné klasické výpočetní zdroje.
from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
tighten_with_speed_limit,
)
from qiskit_mitigation import PEC
slc_boxing_options = {
"add_tags": "unique_box",
"inject_noise_strategy": "individual_modification",
}
slc_pec_task = PEC()
boxed_circuit, noise_model_paulis = (
slc_pec_task.generate_slc_bounds_computation_input(
mirrored_circuit, custom_boxing_options=slc_boxing_options
)
)
forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
tighten_forward_bounds = tighten_with_speed_limit(
forward_bounds, boxed_circuit, noise_model_paulis, observable
)
backward_bounds = compute_backward_bounds(boxed_circuit, noise_model_paulis)
Nauč se šum provázávajících vrstev
Abychom vypočítali konečný odhad toho, jak moc každá chyba ovlivňuje zkreslení střední hodnoty a jak drahé by bylo ji opravit, naučíme se sazby šumu a použijeme je na předpovězené meze šumu, které jsme vypočítali dříve.
PEC (stejně jako PEC+SLC) předpokládá twirlovaný model šumu každé vrstvy provázávajících hradel v obvodu. Pokud předpokládáš markovovský model šumu, předpokládá se, že hloubka obvodu neovlivňuje šumový kanál dané provázávající vrstvy. Proto je třeba naučit pouze model šumu každé jedinečné vrstvy provázávajících hradel. Obvod použitý v tomto návodu lze rozdělit na dvě jedinečné vrstvy. Navrhovat obvody s minimální sadou jedinečných provázávajících vrstev je důležité, pokud se chceš učit model šumu, protože jedinečné vrstvy se musí učit nezávisle. Pokud má tvůj obvod příliš mnoho jedinečných vrstev, může být učení šumu neproveditelné kvůli nákladům na vzorkování při učení mnoha vrstev.
K učení šumu se používá qiskit-noise-learning. Learner opakuje každou vrstvu fragment_depths krát ve svých učicích obvodech, každý obvod twirluje num_randomizations krát a rozpad naměřených fidelit proloží Pauli-Lindbladovým modelem. Tyto parametry mohou ovlivnit přesnost naučeného modelu. Čím přesnější je naučený model šumu, tím přesnější bude potlačení šumu.
Následující kód vykreslí naučený model šumu, přičemž obě vrstvy jsou sloučeny do jedné mapy. Každý qubit je koláč z jeho vah-1 sazeb X, Y a Z sečtených přes obě vrstvy a každá hrana ukazuje naučené sazby váhy 2. Například sazba 3e-4 znamená, že k této chybě dochází zhruba třikrát z každých 10 000 použití vrstvy.
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.results import LayerError, PauliLindbladError
from qiskit_ibm_runtime.visualization.embeddings import Embedding
from qiskit_noise_learning.aer_executor import AerExecutor
from qiskit_noise_learning.noise_learner import LearningOptions, NoiseLearner
from samplomatic import InjectNoise
line_coupling = [(i, i + 1) for i in range(num_qubits - 1)]
backend = GenericBackendV2(
num_qubits=num_qubits,
basis_gates=["id", "rz", "sx", "x", "cz"],
coupling_map=line_coupling,
)
# first learn only the layers PEC will inject noise into, i.e. not the measurement layer
pec_layers = [
layer
for layer in slc_pec_task.find_unique_layers(
mirrored_circuit, slc_boxing_options
)
if get_annotation(layer.operation, InjectNoise)
]
layers = slc_pec_task.find_unique_layers(mirrored_circuit, slc_boxing_options)
learning_options = LearningOptions(
fragment_depths=[2, 4, 8, 16],
num_randomizations=250,
shots_per_randomizations=16,
)
# qiskit-noise-learning is not compatible with the Executor in local mode,
# and requires the simulator present in the package for running simulation
noise_dict = {
get_annotation(layer.operation, Tag).ref: noise_layer
for layer, noise_layer in layer_noise_model
}
noise_learner_simulator = AerExecutor(
qasm_simulator=AerSimulator(), noise_dict=noise_dict
)
learner = NoiseLearner(
backend, options=learning_options, executor=noise_learner_simulator
)
learned_noise = (
learner.run(pec_layers).result().to_dict()
) # keyed by InjectNoise reference
# Draw the learned model on the line, laid out as a line with the qubits in order.
# Composing the two layers' maps sums their rates, giving one map over all the edges.
embedding = Embedding(
[(0, 0), (0, 2), (0, 4), (2, 4), (4, 4), (4, 2), (4, 0), (2, 0)],
line_coupling,
)
first, second = learned_noise.values()
learned_model = first.compose(second).simplify()
error = PauliLindbladError(
PauliList(
learned_model.get_qubit_sparse_pauli_list_copy().to_pauli_list()
),
learned_model.rates,
)
fig = LayerError(circuit, list(range(num_qubits)), error).draw_map(
embedding, radius=0.4, height=460, width=620
)
fig.update_traces(
marker={
"cmin": 0,
"cmax": max(learned_model.rates),
"colorbar": {"tickformat": ".1e", "title": "rate"},
},
selector={"mode": "markers"},
)
fig.show()
Pomocí naučeného šumu sluč vypočtené meze do matice lokálních škál
S klasickými mezemi (z předchozího kroku) i naučenými sazbami šumu v ruce nyní můžeme vypočítat matici lokálních škál — váhu pro každou chybu, která určuje, jak agresivně má PEC každý jednotlivý šumový člen potlačovat.
Klíčová myšlenka SLC je, že není třeba potlačit všechny chyby úplně. Chyba, jejíž dopředná i zpětná mez jsou malé, má na pozorovatelnou zanedbatelný vliv bez ohledu na svou sazbu, takže ji lze nechat částečně nebo úplně nemitigovanou. merge_bounds sloučí klasické meze s naučenými Pauli-Lindbladovými sazbami, poté compute_local_scales přiřadí každé chybě škálu mezi 0 (zcela ignorována) a 1 (zcela potlačena), podle uživatelem zadané bias_tolerance. Chyby s velkým odhadovaným příspěvkem ke zkreslení dostanou škálu blízkou 1 (plné zacházení PEC), zatímco chyby s malým příspěvkem dostanou nižší škálu, čímž se sníží režie vzorkování, kterou nesou.
SLC nabízí spojitý ovladač mezi dvěma extrémy — běžným PEC (nulové zkreslení, nejvyšší režie) a žádnou mitigací (nulová režie, plné zkreslení šumem). Tento ovladač řídí parametr bias_tolerance: jeho nastavení na 0 obnoví plné PEC, zatímco větší hodnoty připouštějí větší zbytkové zkreslení výměnou za nižší režii vzorkování γ².
Tento krok prochází bias_tolerance v řadě hodnot a pro každou vypočítá výslednou režii vzorkování. Vynesení těchto bodů proti režii plného PEC ti pomůže zvolit pracovní bod, který odpovídá tvému rozpočtu shotů. Například tolerování 5 % zbytkového zkreslení může snížit režii vzorkování o řád oproti plnému PEC, takže je výpočet proveditelný na dnešních QPU, kde by jinak nebyl.
import matplotlib.pyplot as plt
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds
merged_bounds = merge_bounds(
boxed_circuit,
tighten_forward_bounds,
backward_bounds,
noise_rates=learned_noise,
)
biases = []
costs = []
for bias in [0.0, *np.arange(0.01, 0.11, 0.01).tolist()]:
_, cost_, bias_ = compute_local_scales(
boxed_circuit,
merged_bounds,
noise_rates=learned_noise,
bias_tolerance=bias,
)
biases.append(bias_)
costs.append(cost_)
# Compute tha full PEC overhead and compare it with the overhead and bias of different SLC configurations
vanilla_gamma = slc_pec_task.calculate_gamma(
slc_pec_task.boxed_circuit, learned_noise, 1
)
xticks = np.arange(0, 11, 2)
fig, ax = plt.subplots()
ax.scatter(
[0], [vanilla_gamma**2], marker="D", c="tab:orange", label="full PEC"
)
ax.plot(
100 * np.array(biases),
np.array(costs),
"o-",
c="tab:blue",
label="PEC+SLC",
)
ax.set_xticks(xticks, [f"{x:.2f}" for x in xticks])
ax.set_xlabel("Remaining bias [%]")
ax.set_ylabel(r"Sampling overhead, $\gamma^2$")
ax.grid()
ax.legend()
fig.suptitle("PEC sampling overhead reduction due to SLC")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')
Zvol požadovanou mez zkreslení na základě kompromisu mezi režií shotů a zkreslením
V tomto návodu volíme toleranci zkreslení 5 %. Všimni si, že tento výpočet zkreslení vychází z naučeného modelu šumu, který se nemusí dokonale shodovat se skutečným šumem zařízení.
chosen_bias_threshold = 0.05
local_scales, sampling_cost, bias = compute_local_scales(
boxed_circuit, merged_bounds, learned_noise, bias_tolerance=bias
)
slc_pec_task.set_slc_local_scales(
local_scales, float(np.sqrt(sampling_cost)), bias
)
Připrav program PEC + SLC, spusť jej a proveď post-processing
S vybranými lokálními škálami můžeme sestavit úplný program PEC+SLC. Volání PEC.prepare vytvoří QuantumProgram, který zakóduje strategii vkládání anti-šumu do anotací boxů obvodu. Předání noise_gain="slc_local_scales" instruuje executor, aby použil váhy jednotlivých chyb nastavené pomocí set_slc_local_scales, místo aby na každou chybu aplikoval stejné plné inverzní zesílení šumu, jako by to udělalo běžné PEC.
Pro férové srovnání se připraví druhý úkol PEC s noise_gain=0, který odpovídá plnému potlačení PEC bez tolerance zkreslení SLC. Oba programy jsou přidány do stejného QuantumProgram, takže sdílejí jedinou úlohu — jejich obvody jsou prokládané a spouštějí se společně, takže jakýkoli drift hardwaru ovlivní oba stejně.
Po dokončení úlohy postprocess převede surové naměřené bitové řetězce na střední hodnoty a pro každý úkol použije příslušný přeškálovací faktor PEC γ.
program_pec = slc_pec_task.prepare(
circuit=mirrored_circuit,
observables=[observable],
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=slc_boxing_options,
noise_maps=learned_noise, # PEC option
noise_gain="slc_local_scales", # PEC option. Defines the usage of SLC
max_sampling_overhead=100, # PEC option
)
full_pec_task = PEC()
program_pec = full_pec_task.prepare(
circuit=mirrored_circuit,
observables=[observable],
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
quantum_program=program_pec,
noise_maps=learned_noise, # PEC option
noise_gain=0, # PEC option
max_sampling_overhead=100, # PEC option
)
# Execute
program_pec._semantic_role = (
"simulation" # flag the program for the simulator
)
job_pec_results = noisy_executor.run(program_pec).result()
# post-processing
pec_slc_res = slc_pec_task.postprocess(job_pec_results)
full_pec_res = full_pec_task.postprocess(job_pec_results)
Porovnej mitigované střední hodnoty a režii shotů
Ideální střední hodnota zrcadleného obvodu je z konstrukce 1, takže zkreslení libovolného odhadu je prostě |1 - result|. Tato závěrečná buňka vypíše zkreslení a celkový počet shotů pro tři scénáře vedle sebe:
-
Nemitigováno: surová zašuměná střední hodnota bez jakéhokoli potlačení chyb.
-
Plné PEC: potlačení chyb s nulovým zkreslením pomocí úplného inverzního modelu šumu na každé vrstvě, při plné režii vzorkování γ².
-
PEC+SLC: potlačení chyb řízené lokálními škálami shaded-lightcone, které toleruje zvolenou
bias_tolerancevýměnou za podstatně nižší počet shotů.
Porovnání tří řádků ukazuje konkrétní přínos SLC: mitigované zkreslení PEC+SLC zůstává hluboko pod nemitigovaným zkreslením, zatímco celkový počet shotů je výrazně menší, než kolik vyžadovalo plné PEC.
print(
f"Unmitigated result bias: {np.abs(1 - unmitigated_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_unmit_res[0]['_meas'].shape[0] * job_unmit_res[0]['_meas'].shape[2]}"
)
print(
f"Full PEC final result bias: {np.abs(1 - full_pec_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_pec_results[1]['_meas'].shape[0] * job_pec_results[1]['_meas'].shape[2]}"
)
print(
f"PEC+SLC final result bias: {np.abs(1 - pec_slc_res.data.evs[0]) * 100:.2f}%, \
total shots ran: {job_pec_results[0]['_meas'].shape[0] * job_pec_results[0]['_meas'].shape[2]}"
)
Unmitigated result bias: 36.88%, total shots ran: 6400
Full PEC final result bias: 0.97%, total shots ran: 48528
PEC+SLC final result bias: 1.46%, total shots ran: 24480