Zpětná propagace operátoru (OBP) pro odhad střední hodnoty
Odhadovaná doba použití: 4 minuty na procesoru Heron r3 (POZNÁMKA: Jedná se pouze o odhad. Skutečná doba běhu se může lišit.)
Výsledky učení
Po absolvování tohoto tutoriálu by uživatelé měli rozumět:
- Jak používat
qiskit-addon-obpke snížení hloubky kvantového obvodu za cenu zvýšeného počtu spuštění obvodu - Jak používat
qiskit-addon-utilsk vytváření XYZ hamiltonianů a jejich obvodů časového vývoje
Předpoklady
Doporučujeme, aby uživatelé před tímto tutoriálem ovládali tato témata:
- Použití primitivy Estimator pro výpočet středních hodnot pozorovatelné veličiny
Pozadí
Zpětná propagace operátoru je technika, která spočívá v absorpci operací z konce kvantového obvodu do měřené pozorovatelné veličiny. Obecně tak snižuje hloubku obvodu za cenu většího počtu členů v pozorovatelné veličině. Cílem je zpětně propagovat co největší část obvodu bez toho, aby pozorovatelná veličina příliš narostla. Implementace v Qiskitu je k dispozici v doplňku OBP Qiskit. Podrobnosti najdeš v příslušné dokumentaci.
Uvažujme příkladový obvod, pro nějž má být změřena pozorovatelná veličina , kde jsou Pauliho operátory a jsou koeficienty. Obvod označíme jako jedinou unitární transformaci , kterou lze logicky rozdělit na , jak je znázorněno na obrázku níže.

Zpětná propagace operátoru absorbuje unitární transformaci do pozorovatelné veličiny jejím vyvinutím jako . Jinými slovy, část výpočtu se provede klasicky pomocí vývoje pozorovatelné veličiny z na . Původní problém lze nyní přeformulovat jako měření pozorovatelné veličiny pro nový obvod s menší hloubkou, jehož unitární transformace je .
Unitární transformace je reprezentována jako počet řezů . Existuje více způsobů, jak definovat řez. Například ve výše uvedeném příkladovém obvodu lze každou vrstvu a každou vrstvu brány považovat za samostatný řez. Zpětná propagace zahrnuje klasický výpočet . Každý řez lze vyjádřit jako , kde je -qubitový Pauliho operátor a je skalár. Snadno ověříme, že
Ve výše uvedeném příkladu, pokud , potřebujeme ke výpočtu střední hodnoty spustit dva kvantové obvody místo jednoho. Proto zpětná propagace může zvýšit počet členů v pozorovatelné veličině, což vede k vyššímu počtu spuštění obvodů. Jedním ze způsobů, jak umožnit hlubší zpětnou propagaci do obvodu a zároveň zabránit příliš velkému nárůstu operátoru, je ořezat členy s malými koeficienty namísto jejich přidání do operátoru. Například v uvedeném příkladu lze zvolit ořezání členu obsahujícího za předpokladu, že je dostatečně malý. Ořezání členů může vést k menšímu počtu kvantových obvodů, ale způsobuje určitou chybu ve výsledném výpočtu střední hodnoty úměrnou velikosti koeficientů ořezaných členů.
Požadavky
Před zahájením tohoto tutoriálu se ujisti, že máš nainstalováno následující:
- Qiskit SDK v2.0 nebo novější, s podporou vizualizace
- Qiskit Runtime v0.22 nebo novější (
pip install qiskit-ibm-runtime) - Doplněk OBP Qiskit 0.3 nebo novější (
pip install qiskit-addon-obp) - Pomocné nástroje Qiskit addon 0.3 nebo novější (
pip install qiskit-addon-utils)
Nastavení
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime rustworkx
import numpy as np
import matplotlib.pyplot as plt
from qiskit.primitives import StatevectorEstimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget
from rustworkx.visualization import graphviz_draw
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions
Příklad na simulátoru v malém měřítku
Tento tutoriál implementuje vzor Qiskit pro simulaci kvantové dynamiky Heisenbergova spinového řetězce pomocí doplňku OBP Qiskit. Všimni si, že na bezešumém simulátoru bude střední hodnota získaná s i bez zpětné propagace stejná.
Krok 1: Mapování klasických vstupů na kvantový problém
Mapování časového vývoje kvantového Heisenbergova modelu na kvantový experiment
Nejprve použijeme funkci generate_xyz_hamiltonian z balíčku qiskit-addon-utils pro generování Heisenbergova hamiltoniánu na daném grafu konektivity. Tento graf může být buď rustworkx.PyGraph nebo CouplingMap. V následující části použijeme lineární řetězec CouplingMap 10 Qubitů.
num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")
Dále vygenerujeme Pauliho operátor modelující Heisenbergův hamiltonián XYZ:
kde je graf mapy propojení. Pro tento tutoriál jsme použili rovné a rovné .
# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
print(hamiltonian)
SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j])
Z qubitového operátoru můžeme vygenerovat kvantový obvod modelující jeho časový vývoj. Použili jsme funkci generate_time_evolution_circuit s Lie-Trotterovou dekompozicí pro sestavení obvodu časového vývoje.
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)

Krok 2: Optimalizace problému pro spuštění na kvantovém hardwaru
Vytvoření řezů obvodu pro zpětnou propagaci
Funkce backpropagate zpracovává vždy celé řezy obvodu najednou, takže způsob dělení může ovlivnit výkon zpětné propagace pro daný problém. Zde seskupíme brány stejného typu do řezů pomocí funkce slice_by_depth.
Podrobnější diskusi o dělení obvodu najdeš v tomto návodu balíčku qiskit-addon-utils.
slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.
Omezení maximálního nárůstu operátoru během zpětné propagace
Během zpětné propagace počet členů v operátoru obecně rychle narůstá směrem k , kde je počet řezů. Pokud dva členy operátoru vzájemně nekomutují po qubitech, potřebujeme k získání odpovídajících středních hodnot samostatné obvody. Například pro dvouqubitovou pozorovatelnou veličinu platí, že , takže k výpočtu středních hodnot těchto dvou členů stačí měření v jediné bázi. Avšak antikomutuje s oběma zbývajícími členy, takže potřebujeme samostatné měření v jiné bázi pro výpočet střední hodnoty . Jinými slovy, k výpočtu potřebujeme dva obvody místo jednoho. S narůstajícím počtem členů v operátoru může narůstat i požadovaný počet spuštění obvodů.
Velikost operátoru lze omezit zadáním argumentu operator_budget funkce backpropagate, který přijímá instanci OperatorBudget.
Abychom omezili množství přidělených zdrojů (počet spuštění obvodů, a tedy požadovaný čas QPU), omezíme maximální počet qubitově komutujících Pauliho skupin, které může mít zpětně propagovaná pozorovatelná veličina. Zde určíme, že zpětná propagace se zastaví, jakmile počet qubitově komutujících Pauliho skupin v operátoru překročí osm.
op_budget = OperatorBudget(max_qwc_groups=8)
Zpětná propagace řezů z obvodu
Nejprve definujeme pozorovatelnou veličinu jako , kde je počet Qubitů. Budeme zpětně propagovat řezy z obvodu časového vývoje, dokud členy v pozorovatelné veličině nelze sloučit do osmi nebo méně qubitově komutujících Pauliho skupin.
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits=num_qubits,
)
observable
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
Níže uvidíš, že bylo zpětně propagováno šest řezů a členy byly sloučeny do šesti, nikoli osmi skupin. To znamená, že zpětná propagace dalšího řezu by způsobila překročení osmi Pauliho skupin. Tuto skutečnost můžeme ověřit prohlédnutím vrácených metadat. Všimni si také, že v této části je transformace obvodu přesná — žádné členy nové pozorovatelné veličiny nebyly ořezány. Zpětně propagovaný obvod a zpětně propagovaný operátor dávají přesně stejný výsledek jako původní obvod a operátor.
# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(bp_obs.paulis)} terms, which can be combined into "
f"{len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in "
f"{metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)
Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
Pro příklad malého měřítku na simulátoru nebudeme používat ořezání. Důvodem je, že v nepřítomnosti šumu vede obvod s i bez zpětné propagace ke stejnému výsledku a ořezání výsledek zhorší kvůli zavedené aproximaci.
Transpilace obvodu do sady bazových bran
Nyní transpilujeme původní i zpětně propagovaný obvod do bazových bran backendu. Nepotřebujeme transpilovat na skutečném backendu, protože pro malý příklad poběžíme na simulátoru.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<IBMBackend('ibm_kingston')>
pm_basis = generate_preset_pass_manager(
optimization_level=3, basis_gates=backend.configuration().basis_gates
)
isa_circuit = pm_basis.run(circuit)
isa_bp_circuit = pm_basis.run(bp_circuit)
Krok 3: Spuštění pomocí Qiskit primitiv
Nejprve vytvoříme dva Primitive Unified Blocs (PUBs) odpovídající původnímu obvodu a zpětně propagovanému obvodu. Poté spustíme PUBs na ideálním Estimatoru a získáme střední hodnoty.
pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]
rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)
Krok 4: Následné zpracování a vrácení výsledku do požadovaného klasického formátu
Nyní získáme střední hodnoty původního a zpětně propagovaného obvodu.
primitive_result = job.result()
circuit_expval = primitive_result[0].data.evs.item()
bp_circuit_expval = primitive_result[1].data.evs.item()
methods = [
"No backpropagation",
"Backpropagation",
]
values = [circuit_expval, bp_circuit_expval]
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylim([0.6, 0.92])
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')
Jak se očekávalo, obě střední hodnoty se shodují. Protože provozujeme bezešumý stavový vektorový simulátor, je zpětná propagace přesnou transformací páru obvod–pozorovatelná veličina, takže původní i zpětně propagovaný postup musí produkovat stejnou hodnotu . Přínos zpětné propagace se projeví až na hlučném hardwaru, kde kratší zpětně propagovaný obvod akumuluje menší chybu, jak je ukázáno v příkladu velkého měřítka na hardwaru níže.
Příklad na hardwaru ve velkém měřítku
Při vývoji experimentu je užitečné začít s malým obvodem, aby se usnadnily vizualizace a simulace. Nyní se zaměříme na zpětnou propagaci operátoru pro 50-qubitový Heisenbergův hamiltonián se stejnými hodnotami parametrů a a stejnou pozorovatelnou , ale pro čtyři Trotterovy kroky. Ideální střední hodnota v tomto měřítku nelze vypočítat hrubou silou, takže použijeme tenzorovou síť a získáme ideální střední hodnotu .
Spolu se zpětnou propagací v tomto rozsáhlém příkladu také představujeme zpětnou propagaci s ořezáním. Ideálně chceme zpětně propagovat co nejvíce, abychom snížili hloubku efektivního obvodu. Zpětná propagace však často vede k velkému počtu nekomutujících členů v aktualizované pozorovatelné veličině, čímž zvyšuje kvantovou režii. Proto lze eliminovat členy pozorovatelné veličiny s malými koeficienty pomocí postupu nazývaného ořezání. Ačkoli ořezání umožňuje hlubší propagaci snížením počtu členů v aktualizované pozorovatelné veličině, zavádí také jistou aproximaci. Je proto nutné omezit ořezání v určitých mezích tak, aby chyba aproximace nepřevážila snížení šumu díky hlubší zpětné propagaci.
K omezení množství ořezání přidělíme chybový rozpočet pro každý řez i celkový chybový rozpočet pro celý zpětně propagovaný obvod pomocí funkce setup_budget. Tím je zajištěno, že ořezání je řízeno jak pro každý řez, tak pro celý obvod. Viz také tento návod pro jiné způsoby alokace rozpočtu.
num_qubits = 50
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
# Generate a time evolution circuit for the Hamiltonian
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=4),
)
# Define the observable to measure
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits,
)
slices = slice_by_depth(circuit, max_slice_depth=1)
# Define the maximum number of qwc groups allowed in the
# backpropagated observable,
# and the truncation error budget
op_budget = OperatorBudget(max_qwc_groups=15)
truncation_error_budget = setup_budget(
max_error_total=0.03, max_error_per_slice=0.005
)
# First backpropagation without truncation
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
bp_circuit = combine_slices(remaining_slices)
# Now backpropagate with truncation, using the same operator budget and
# the defined truncation error budget
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=False
)
# Now we transpile the original circuit and the two backpropagated circuits,
# and apply the layout to the corresponding observables
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
isa_circuit = pm.run(circuit)
isa_bp_circuit = pm.run(bp_circuit)
isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)
isa_bp_observable_trunc = bp_obs_trunc.apply_layout(
isa_bp_circuit_trunc.layout
)
# Compare the 2-qubit depth of each transpiled circuit to see how much
# depth backpropagation saved
print(
f"2-qubit depth without backpropagation: "
f"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation: "
f"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation and truncation: "
f"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}"
)
pubs = [
(isa_circuit, isa_observable),
(isa_bp_circuit, isa_bp_observable),
(isa_bp_circuit_trunc, isa_bp_observable_trunc),
]
# Now we instantiate the Estimator primitive for the hardware with
# ZNE and measurement error
# mitigation and compute the three circuits and observables
options = EstimatorOptions()
options.default_precision = 0.01
options.resilience_level = 2
options.resilience.zne.noise_factors = [1, 1.2, 1.4]
options.resilience.zne.extrapolator = ["linear"]
estimator = EstimatorV2(mode=backend, options=options)
estimator.options.environment.job_tags = ["TUT_OBP"]
job = estimator.run(pubs)
# Retrieve the results and the standard deviations
result_no_bp = job.result()[0].data.evs.item()
result_bp = job.result()[1].data.evs.item()
result_bp_trunc = job.result()[2].data.evs.item()
std_no_bp = job.result()[0].data.stds.item()
std_bp = job.result()[1].data.stds.item()
std_bp_trunc = job.result()[2].data.stds.item()
2-qubit depth without backpropagation: 24
2-qubit depth with backpropagation: 20
2-qubit depth with backpropagation and truncation: 18
print(f"Expectation value without backpropagation: {result_no_bp}")
print(f"Backpropagated expectation value: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")
Expectation value without backpropagation: 0.9543907942381811
Backpropagated expectation value: 0.9445337385406468
Backpropagated expectation value with truncation: 0.934050286970965
# Plot the results
methods = [
"No backpropagation",
"Backpropagation",
"Backpropagation w/ truncation",
]
values = [result_no_bp, result_bp, result_bp_trunc]
error_bars = [std_no_bp, std_bp, std_bp_trunc]
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.errorbar(methods, values, yerr=error_bars, fmt="o", color="r", capsize=5)
plt.axhline(0.89)
ax.set_ylim([0.8, 0.98])
plt.text(0.25, 0.895, "Exact result")
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')
Další kroky
Pokud tě tato práce zaujala, možná tě budou zajímat následující materiály:
- Přibližná kvantová kompilace pro obvody časového vývoje
- Vzorce více produktů pro snížení Trotterovy chyby
pauli-prop, rustově akcelerovaný balíček pro propagaci Pauliho operátorů, s tutoriály pokrývajícími OBP, klasický odhad střední hodnoty a simulaci se šumem